无需分叉Clippy即可编写Rust lint检查工具

本文介绍Dylint工具,它允许开发者从动态库加载Rust lint规则,无需分叉Clippy即可维护自定义lint集合,解决了多版本编译器API兼容性问题,并支持项目特定lint开发。

无需分叉Clippy即可编写Rust lint检查工具

Rust linting与Clippy

像Clippy这样的工具利用了Rust编译器对lint检查的专用支持。Rust linter的核心组件称为“驱动”,它链接到名为rustc_driver的库。通过这种方式,驱动本质上成为Rust编译器的包装器。

要运行linter,需要将RUSTC_WORKSPACE_WRAPPER环境变量设置为指向驱动并运行cargo check。Cargo注意到环境变量已设置,就会调用驱动而不是调用rustc。当驱动被调用时,它会在Rust编译器的Config结构中设置回调。该回调注册一些lint,然后Rust编译器会与其内置lint一起运行这些lint。

Clippy执行一些检查以确保其启用,但其他方面以上述方式工作。虽然安装时可能不太明显,但Clippy实际上是两个二进制文件:一个Cargo命令和一个rustc驱动。您可以通过输入以下命令来验证:

1
2
which cargo-clippy
which clippy-driver

Dylint的工作原理

与Clippy类似,Dylint提供了一个Cargo命令。用户向该命令指定要从哪些动态库加载lint。Dylint以某种方式运行cargo check,确保在控制权交给Rust编译器之前注册lint。

然而,Dylint的lint注册过程比Clippy更复杂。Clippy的所有lint都使用相同的编译器版本,因此只需要一个驱动。但Dylint用户可以选择从使用不同编译器版本的库加载lint。

Dylint通过根据需要即时构建新驱动来处理这种情况。换句话说,如果用户想要从使用编译器版本A的库加载lint,并且找不到适用于编译器版本A的驱动,Dylint将构建一个新驱动。驱动缓存在用户的主目录中,因此仅在必要时重新构建。

这带来了上一节提到的额外好处。Dylint按使用的编译器版本对库进行分组。使用相同编译器版本的库被一起加载,它们的lint一起运行。这允许在lint之间共享中间编译结果(例如,符号解析、类型检查、特征求解等)。

例如,如果库U和V都使用编译器版本A,这些库将被分组在一起。编译器版本A的驱动只会被调用一次。该驱动在将控制权交给Rust编译器之前注册库U和V中的lint。

应用:项目特定lint

您知道吗?Clippy包含一些lint,其唯一目的是对Clippy的代码进行lint检查。这是真的。例如,Clippy包含lint来检查每个lint是否有相关的LintPass,是否使用了某些Clippy包装函数而不是它们包装的函数,以及每个lint是否有非默认描述。将这些lint应用于Clippy以外的代码没有意义。但没有规则要求所有lint都必须是通用的,Clippy利用了这种自由。

Dylint类似地包含主要目的是对Dylint代码进行lint检查的lint。例如,在开发Dylint时,我们发现自己编写了如下代码:

1
2
3
let rustup_toolchain = std::env::var("RUSTUP_TOOLCHAIN")?;
...
std::env::remove_var("RUSTUP_TOOLCHAIN");

这是不好的做法。为什么?因为迟早我们会打错字符串字面量:

1
std::env::remove_var("RUSTUP_TOOLCHIAN"); // 糟糕

更好的方法是使用常量而不是字符串字面量,如下面的代码:

1
2
3
const RUSTUP_TOOLCHAIN: &str = "RUSTUP_TOOLCHAIN";
...
std::env::remove_var(RUSTUP_TOOLCHAIN);

因此在开发Dylint时,我们编写了一个lint来检查这种不良做法并提出适当建议。我们将(并且仍然)将该lint应用于Dylint源代码。该lint称为env_literal,其当前实现的核心如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
impl<'tcx> LateLintPass<'tcx> for EnvLiteral {
   fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
      if_chain! {
         if let ExprKind::Call(callee, args) = expr.kind;
         if is_expr_path_def_path(cx, callee, &REMOVE_VAR)
            || is_expr_path_def_path(cx, callee, &SET_VAR)
            || is_expr_path_def_path(cx, callee, &VAR);
         if !args.is_empty();
         if let ExprKind::Lit(lit) = &args[0].kind;
         if let LitKind::Str(symbol, _) = lit.node;
         let ident = symbol.to_ident_string();
         if is_upper_snake_case(&ident);
         then {
            span_lint_and_help(
               cx,
               ENV_LITERAL,
               args[0].span,
               "referring to an environment variable with a string literal is error prone",
               None,
               &format!("define a constant `{}` and use that instead", ident),
            );
         }
      }
   }
}

以下是它可能产生的警告示例:

1
2
3
4
5
6
7
8
warning: referring to an environment variable with a string literal is error prone
 --> src/main.rs:2:27
  |
2 |     let _ = std::env::var("RUSTFLAGS");
  |                           ^^^^^^^^^^^
  |
  = note: `#[warn(env_literal)]` on by default
  = help: define a constant `RUSTFLAGS` and use that instead

开始使用lint

使用以下命令安装Dylint:

1
cargo install cargo-dylint

我们还建议安装dylint-link工具以方便链接:

1
cargo install dylint-link

编写Dylint库的最简单方法是分叉dylint-template仓库。该仓库开箱即用即可生成可加载库。您可以按以下方式验证:

1
2
3
4
git clone https://github.com/trailofbits/dylint-template
cd dylint-template
cargo build
DYLINT_LIBRARY_PATH=$PWD/target/debug cargo dylint fill_me_in --list

您只需要实现LateLintPass特性并处理需要填充的符号。

编写lint的有用资源包括:

  • 添加新lint(针对Clippy但仍然有用)
  • 编写lint的常用工具
  • rustc_hir文档

还可以考虑使用上面提到的clippy_utils crate。它包含许多低级任务的函数,例如查找符号和打印诊断消息,使编写lint变得更容易。

我们要衷心感谢Clippy作者向Rust社区提供clippy_utils crate。我们还要感谢Philipp Krones对本文早期版本提供了有益评论。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计