基于编译器插件定制 clippy lint
作者: 吴翱翔@pymongo / 后期编辑: 张汉东
本文基于 Rust 月刊先前文章 华为 | 如何定制 Rust Clippy
上述文章中提供了改 clippy 源码或 dylint 库的两个开发定制 lint 的方案,
但 dylint 不能跨平台且以动态库形式分发难以使用,改 clippy 源码不方便与官方 lint 同步
基于上述困难,我便有了以下 lint 框架的设计需求:
- 一定要跨平台,同时支持 windows/mac/linux 等主流操作系统
- 不要有任何依赖,dylint 依赖 clippy 导致 rust-toolchain 被迫绑定跟 clippy 一样的版本
- 代码足够简单 50 行足以加新的 Lint,不用任何宏导致 IDE 看宏代码时变成"瞎子"
- 定制的 lint 工具对 用户/使用者 而言要易于使用
首先用 rustc_private 编译器模块将自己 lint 框架的库编译为库(以下简称 lints 库),然后可通过三种渠道运行
- rustc plugin 编译器动态库补丁
- ui_test
- 改 rust 源码引入 lints 库并编译为 toolchains
- RUSTC=/path/to/my_rustc cargo check
RUSTC 环境变量
最简单的定制 lint 的方法,将自己写的 lint 注入到 rustc 中假设将自己修改的 rustc 可执行文件叫 my_rustc
然后运行 "RUSTC=my_rustc" cargo check
就能运行自己定制的 lint 的检查了
所以我们将问题简化为 如何改 rustc 源码 + 如何编译 rustc 两个部分(当然这么运行会有问题,后文再展开)
rustc_driver
如果说 rust 源码路径下的 compiler/rustc/src/main.rs 是最终的编译器二进制文件
那么 rustc_driver 模块就是整个编译器的入口和"大脑",可以通过 rustc_private feature 引入这些编译器的模块
对比 rustc_driver 源码和 rustc 源码在 compiler/rustc/src/main.rs
里面也一行调用 rustc_driver::main()
发现只需要一行代码就能让 自己的代码编译出 rustc 编译器
fn main() { rustc_driver::RunCompiler::new(&std::env::args().collect::<Vec<_>>(), &mut DefaultCallback).run().unwrap(); }
rustc_driver::RunCompiler.run()
这就是 rustc 编译器的入口函数,我们将自己可执行文件的参数原封不动的转发给 rustc 就能让"编译器"跑起来
编译源码的子问题被简化为直接调用 RunCompiler,修改源码子问题转换成改 DefaultCallback 即可
如何引入 rustc_driver 库
- rustup component add rustc-dev 然后就可以用 feature rustc_private
- crates.io rustc-ap-rustc_driver 这个是 Rust 官方将编译器模块同步上传的版本
- 自己下载 rustc 源码通过路径引入或者通过 github 链接引入
trait rustc_driver::Callback
无论是官方的 clippy/miri 工具还是第三方的 flowistry(类似 rust-analyzer 的 Rust vscode 插件)或者 c2rust 等静态分析相关的项目
都是通过重写编译器回调 trait 的各个回调钩子方法,注入自己静态分析代码的逻辑
可以参考 flowistry 静态分析的这行代码:
rustc_driver 找不到标准库
rustc_driver::RunCompiler::new
这样编译出来的 rustc 运行时报错找不到标准库
"can't find crate for
std
"
有个不完美的解决方案,在 build.rs 中加入一行,编译时"链接"标准库(以下为 Linux 示例)
println!("cargo:rustc-link-search=/home/w/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib");
build.rs 的 cargo:rustc-link-search=
默认库搜索类型是 all 也包含 Rust ABI 的 rlib 格式库文件
build.rs 方案 部分解决了「链接标准库」问题,某些时候也能正常运行,但 ldd 依然提示标准库找不到
[w@ww lints]$ ldd ~/.cargo/bin/lints
librustc_driver-e1b628cff3afb6ed.so => not found
libstd-d6566390077dd5f5.so => not found
相比 build.rs 中链接标准库,更可靠的做法是运行时给 rustc 加一个 -L 参数链接标准库
例如以下是我 lint 框架的 ui 测试部分代码,我就是通过 rustc 运行时 -L 参数解决标准库找不到的问题
#![allow(unused)] fn main() { let stderr = std::process::Command::new("cargo") .arg("run") .arg("--") .arg("--emit=metadata") .arg("--crate-type=lib") .arg("--allow=dead_code") .arg("-L") .arg(env!("STD_DYLIB_PATH")) .arg(rs_file_path) .output() }
env!("STD_DYLIB_PATH")
来自 build.rs,具体可看我 lint 框架源码: https://github.com/pymongo/lints
即便运行时加上 -L 参数,但这样的 rustc 依然不容易进行 cargo build
或 codegen,建议仅用于分析代码
45 行代码写个编译器补丁/插件
例如我想添加一个检测函数名为 foo 的 lint 检查规则,首先 cargo 新建一个库:
cargo new --lib rustc_plugin_my_lints
然后在 Cargo.toml 将库的类型设置成 dylib
[lib]
crate-type = ["dylib"]
然后在 src/lib.rs 中写下以下代码:
#![allow(unused)] #![feature(rustc_private)] fn main() { extern crate rustc_ast; extern crate rustc_driver; extern crate rustc_interface; extern crate rustc_lint; extern crate rustc_span; #[no_mangle] fn __rustc_plugin_registrar(reg: &mut rustc_driver::plugin::Registry) { reg.lint_store.register_early_pass(|| Box::new(FnNameIsFoo)); } struct FnNameIsFoo; impl FnNameIsFoo { const LINT: rustc_lint::Lint = { let mut lint = rustc_lint::Lint::default_fields_for_macro(); lint.name = "fn_name_is_foo"; lint.default_level = rustc_lint::Level::Warn; lint }; } impl rustc_lint::LintPass for FnNameIsFoo { fn name(&self) -> &'static str { "fn_name_is_foo" } } impl rustc_lint::EarlyLintPass for FnNameIsFoo { fn check_fn( &mut self, cx: &rustc_lint::EarlyContext<'_>, fn_kind: rustc_ast::visit::FnKind<'_>, span: rustc_span::Span, _: rustc_ast::NodeId, ) { if let rustc_ast::visit::FnKind::Fn(_, ident, ..) = fn_kind { if ident.as_str() == "foo" { rustc_lint::LintContext::struct_span_lint(cx, &Self::LINT, span, |diagnostic| { diagnostic.build("foo is a bad name for function").emit(); }); } } } } }
然后新建一个 examples/test_plugin.rs 文件测试我们写好的编译器插件/补丁
#![feature(plugin)] #![plugin(rustc_plugin_my_lints)] fn foo() { } fn main() { foo(); }
最后可以运行下编译器补丁的测试:
cargo run --example test_plugin
然后得到了这样的输出:
warning: foo is a bad name for function
--> examples/test_plugin.rs:3:1
|
3 | / fn foo() {
4 | |
5 | | }
| |_^
|
= note: `#[warn(fn_name_is_foo)]` on by default
lints 框架的设计思路
由于 plugin feature 已经 deprecated 了,以后可能会被删掉,我的 lint 框架的设计思路是只做 lint 检查规则的功能测试和 ui 测试,定期集成到 rust 源码中编译一套自己的工具链做长远使用,同时提供 my_rustc 可执行文件或者编译器插件库进行使用
在我读了很多 rust-analyzer/rust-clippy/dylint/flowistry 等静态分析相关项目源码后设计了 lints 静态分析框架
这是 lints 项目源码的链接: https://github.com/pymongo/lints
以下是 lints 项目的源码结构和解读:
.
├── build.rs
├── Cargo.lock
├── Cargo.toml
├── examples
│ └── compiler_plugin.rs # 编译器插件的示例
├── README.md # crate lints 的文档
├── rust-toolchain.toml # 定义所需 rustup components
├── src
│ ├── bin
│ │ └── rustc_.rs # 用于 ui 测试,也能通过 RUSTC=/path/to/rustc_ cargo c 运行在简单项目
│ ├── lib.rs
│ └── lints # early_lint + late_lint 的定义和逻辑
│ ├── check_enum_size.rs
│ ├── fn_name_is_foo.rs
│ └── mod.rs
├── tests # 测试代码
│ └── ui_test.rs # ui 测试
└── ui_test # ui 测试用例: 输入+输出+期待值
├── check_enum_size.rs
├── check_enum_size.stderr
├── fn_name_is_foo.rs
└── fn_name_is_foo.stderr
添加一条 lint 检查规则非常简单只需在 src/lints 下加一个文件,定义一个新的结构体并注册在 src/lints/mod.rs 即可
最后,用 rustc plugin 插件不光能定制 lint 还可以做帮组源码中的 enum 排序等等有趣的事情