作者:华为可信软件工程和开源2012实验室


Clippy 是什么

Clippy 是 Rust 官方提供的 代码检查 lint 工具,通过静态分析,来检查代码中有问题或不符合指定规范的代码。

项目地址:https://github.com/rust-lang/rust-clippy

安装

  1. rustup component add clippy

使用

  1. cargo clippy

配置

可以在项目中添加 clippy.toml.clippy.toml 来指定使用的 Lints 。

类似于:

  1. avoid-breaking-exported-api = false
  2. blacklisted-names = ["toto", "tata", "titi"]
  3. cognitive-complexity-threshold = 30

Cargo Clippy 中目前包含超过 450 个 Lint

Rust 编译器内置 Lint 介绍

在 Rust 编译器 中 lint 包含四种级别:

  • allow ,编译器
  • warn
  • deny
  • forbid

每个 lint 都有一个 默认级别。下面是一个分类:

编译器内置 Lint 主要是围绕 Rust 语言特性。开发者可以通过配置文件来修改 Lints 等级。

Clippy 中的 Lints

Clippy 中的 Lints 级别包括:

  • Allow
  • Warn
  • Deny
  • Deprecated

Clippy 中的 lints 分类如下表:

分类 描述 默认级别
clippy::all all lints that are on by default (correctness, style, complexity, perf)
所有默认的 Lints 都会被开启(正确性、风格、复杂性、性能)
warn/deny
clippy::correctness code that is outright wrong or very useless
代码是完全错误或根本无用的
deny
clippy::style code that should be written in a more idiomatic way
代码应该用更惯用的方式来写
warn
clippy::complexity code that does something simple but in a complex way
代码把简单的事情写复杂了
warn
clippy::perf code that can be written to run faster
代码的写法在性能上还可以改进
warn
clippy::pedantic lints which are rather strict or might have false positives
这些 lints 相当严格或可能有误报
allow
clippy::nursery new lints that are still under development
仍然在开发中的新 lints
allow
clippy::cargo lints for the cargo manifest
用于cargo manifest 的 lints
allow

总的来说,Clippy 对代码的检查主要是包括下面五个方面:

  • 代码正确性(Correctness)。检查代码中不正确的写法。
  • 代码风格(Style)。相比于 rustfmt,clippy 更偏向于代码实践中的惯用法检查。
  • 代码复杂性(Complexity)。检查过于复杂的写法,用更简洁的写法代替。
  • 代码不灵动 (Pedantic)。写法过于教条。
  • 代码性能(Perf)。

代码正确性

Lint 示例: absurd_extreme_comparisons (荒谬的极值比较)

检查关系中的一方是其类型的最小值或最大值的比较,如果涉及到永远是真或永远是假的情况,则发出警告。只有整数和布尔类型被检查。

代码示例:

  1. let vec: Vec<isize> = Vec::new();
  2. if vec.len() <= 0 {}
  3. if 100 > i32::MAX {} // 这里会报错:Deny ,因为 100 不可能大于 i32::MAX

代码风格

Lint 示例: assertions_on_constants (对常量的断言)

用于检查 assert!(true) 和 assert!(false) 的情况。

代码示例:

  1. assert!(false)
  2. assert!(true)
  3. const B: bool = false;
  4. assert!(B) // 会被编译器优化掉。

代码复杂性

Lint 示例: bind_instead_of_map

检查 _.and_then(|x| Some(y)), _.and_then(|x| Ok(y)) or _.or_else(|x| Err(y)) 这样的用法,建议使用更简洁的写法 _.map(|x| y) or _.map_err(|x| y)

代码示例:

  1. // bad
  2. let _ = opt().and_then(|s| Some(s.len()));
  3. let _ = res().and_then(|s| if s.len() == 42 { Ok(10) } else { Ok(20) });
  4. let _ = res().or_else(|s| if s.len() == 42 { Err(10) } else { Err(20) });
  5. // good
  6. let _ = opt().map(|s| s.len());
  7. let _ = res().map(|s| if s.len() == 42 { 10 } else { 20 });
  8. let _ = res().map_err(|s| if s.len() == 42 { 10 } else { 20 });

代码不灵动

Lints 示例: cast_lossless

用于检查可以被安全转换(conversion)函数替代的数字类型之间的转换( cast )。

as强制转换与From转换从根本上不同。 From转换是“简单和安全”,而as强制转换纯粹是“安全”。在考虑数字类型时,仅在保证输出相同的情况下才存在From转换,即,不会丢失任何信息(不会出现截断或下限或精度下降)。 as强制转换没有此限制。

代码示例:

  1. // bad
  2. fn as_u64(x: u8) -> u64 {
  3. x as u64
  4. }
  5. // good
  6. fn as_u64(x: u8) -> u64 {
  7. u64::from(x) // from内部其实也是as,但只要是实现 from 的,都是无损转换,在代码可读性、语义上更好
  8. }

代码性能

Lints 示例: append_instead_of_extend

检查动态数组中是否出现 extend,建议使用 append代替。

代码示例:

  1. let mut a = vec![1, 2, 3];
  2. let mut b = vec![4, 5, 6];
  3. // Bad
  4. a.extend(b.drain(..));
  5. // Good
  6. a.append(&mut b); // 用 append 代替 extend 更加高效和简洁。

还有一些其他分类,比如包括一些「约束性(Restriction)」建议、对 cargo.toml 的检查、以及正在开发中的Lints 等。

如何定制 Clippy Lint

定制 Clippy Lint 有两种办法:

  1. 方法一:fork rust-clippy 项目,自己维护。因为使用了不稳定的接口,所以维护和使用不太方便。
  2. 方法二:使用第三方 Dylint 工具。维护自定义 lint 比方法一更方便。

方法一:fork clippy

在 fork Clippy 定制自己的 LInt 之前,还需要了解 Clippy 的 工作机制。

Clippy 工作机制

clippy.png

Clippy 通过 rust_driverrustc_interface 这两个库,可以把 rustc 作为库来调用。

rustc_driver 本质上就像是整个rustc 编译器的main函数(入口)。它使用在rustc_interface crate中定义的接口以正确的顺序运行编译器。

rustc_interface crate为外部用户提供了一个(未稳定的)API,用于在编译过程中的特定时间运行代码,允许第三方(例如RLSrustdoc)有效地使用rustc的内部结构作为分析crate 或 模拟编译器过程的库。

对于那些使用 rustc 作为库的人来说,rustc_interface::run_compiler()函数是进入编译器的主要入口。它接收一个编译器的配置和一个接收编译器的闭包。run_compiler从配置中创建一个编译器并将其传递给闭包。在闭包中,你可以使用编译器来驱动查询,以编译一个 crate 并获得结果。这也是 rustc_driver 所做的。

rustc_interface 组件库中定义了Compiler 结构体,持有 register_lints 字段。该 Compiler结构体就是编译器会话实例,可以通过它传递编译器配置,并且运行编译器。

register_lints 是 持有 LintStore 可变借用的闭包,其类型签名是 Option<Box<dyn Fn(&Session, &mut LintStore) + Send + Sync>>

LintStorerustc_lint 组件库中定义的类型。

  1. pub struct LintStore {
  2. /// Registered lints.
  3. lints: Vec<&'static Lint>,
  4. // 构造不同种类的 lint pass
  5. /// Constructor functions for each variety of lint pass.
  6. ///
  7. /// These should only be called once, but since we want to avoid locks or
  8. /// interior mutability, we don't enforce this (and lints should, in theory,
  9. /// be compatible with being constructed more than once, though not
  10. /// necessarily in a sane manner. This is safe though.)
  11. pub pre_expansion_passes: Vec<Box<dyn Fn() -> EarlyLintPassObject + sync::Send + sync::Sync>>,
  12. pub early_passes: Vec<Box<dyn Fn() -> EarlyLintPassObject + sync::Send + sync::Sync>>,
  13. pub late_passes: Vec<Box<dyn Fn() -> LateLintPassObject + sync::Send + sync::Sync>>,
  14. /// This is unique in that we construct them per-module, so not once.
  15. pub late_module_passes: Vec<Box<dyn Fn() -> LateLintPassObject + sync::Send + sync::Sync>>,
  16. /// Lints indexed by name.
  17. by_name: FxHashMap<String, TargetLint>,
  18. // lint group,通过一个名字触发多个警告,把lint分组
  19. /// Map of registered lint groups to what lints they expand to.
  20. lint_groups: FxHashMap<&'static str, LintGroup>,
  21. }

可以注册的 lint pass 还分好几类:

  • early_passes:表示该类型的 lint pass对应的是 EarlyContext,是在 AST 层级的 lint 检查,还未到 HIR 层面。
  • late_passes:表示该类型的 lint pass对应的是 LateContext,是在 类型检查之后的 lint 检查。意味着这样的检查需要获取类型信息。类型检查是在 HIR 层级做的。

rust_interface 中,还定义了相应的 check 方法:early_lint_methods! 定义的很多check方法late_lint_methods

声明一个 lint pass 需要使用 declare_late_lint_pass! 宏 中定义的 rustc_lint::LateLintPass trait

再来看 run_compiler函数。

  1. pub fn run_compiler<R: Send>(mut config: Config, f: impl FnOnce(&Compiler) -> R + Send) -> R {
  2. tracing::trace!("run_compiler");
  3. let stderr = config.stderr.take();
  4. util::setup_callbacks_and_run_in_thread_pool_with_globals(
  5. config.opts.edition,
  6. config.opts.debugging_opts.threads,
  7. &stderr,
  8. || create_compiler_and_run(config, f), // 设置一个回调函数
  9. )
  10. }
  11. // 回调函数
  12. pub fn create_compiler_and_run<R>(config: Config, f: impl FnOnce(&Compiler) -> R) -> R {
  13. let registry = &config.registry;
  14. let (mut sess, codegen_backend) = util::create_session(
  15. config.opts,
  16. config.crate_cfg,
  17. config.diagnostic_output,
  18. config.file_loader,
  19. config.input_path.clone(),
  20. config.lint_caps,
  21. config.make_codegen_backend,
  22. registry.clone(),
  23. );
  24. // 。。。省略
  25. let compiler = Compiler {
  26. sess,
  27. codegen_backend,
  28. input: config.input,
  29. input_path: config.input_path,
  30. output_dir: config.output_dir,
  31. output_file: config.output_file,
  32. register_lints: config.register_lints, // 配置 register_lints
  33. override_queries: config.override_queries,
  34. };
  35. }

再看看 rustc_driver库,其中定义了 [Callbacks trait](https://github.com/rust-lang/rust/blob/master/compiler/rustc_driver/src/lib.rs#L84)

  1. pub trait Callbacks {
  2. /// Called before creating the compiler instance
  3. fn config(&mut self, _config: &mut interface::Config) {}
  4. /// Called after parsing. Return value instructs the compiler whether to
  5. /// continue the compilation afterwards (defaults to `Compilation::Continue`)
  6. fn after_parsing<'tcx>(
  7. &mut self,
  8. _compiler: &interface::Compiler,
  9. _queries: &'tcx Queries<'tcx>,
  10. ) -> Compilation {
  11. Compilation::Continue
  12. }
  13. /// Called after expansion. Return value instructs the compiler whether to
  14. /// continue the compilation afterwards (defaults to `Compilation::Continue`)
  15. fn after_expansion<'tcx>(
  16. &mut self,
  17. _compiler: &interface::Compiler,
  18. _queries: &'tcx Queries<'tcx>,
  19. ) -> Compilation {
  20. Compilation::Continue
  21. }
  22. /// Called after analysis. Return value instructs the compiler whether to
  23. /// continue the compilation afterwards (defaults to `Compilation::Continue`)
  24. fn after_analysis<'tcx>(
  25. &mut self,
  26. _compiler: &interface::Compiler,
  27. _queries: &'tcx Queries<'tcx>,
  28. ) -> Compilation {
  29. Compilation::Continue
  30. }
  31. }

该trait中定义了在编译不同阶段要执行的回调函数。

所以,在 Clippy 的 driver.rs 中就做了如下定义:

  1. struct ClippyCallbacks {
  2. clippy_args_var: Option<String>,
  3. }
  4. // 为 ClippyCallbacks 实现 rustc_driver::Callbacks ,定义 config 方法
  5. // 该 config 方法创建编译器实例之前被执行的
  6. impl rustc_driver::Callbacks for ClippyCallbacks {
  7. fn config(&mut self, config: &mut interface::Config) {
  8. let previous = config.register_lints.take();
  9. let clippy_args_var = self.clippy_args_var.take();
  10. config.parse_sess_created = Some(Box::new(move |parse_sess| {
  11. track_clippy_args(parse_sess, &clippy_args_var);
  12. }));
  13. // 注册 lints
  14. config.register_lints = Some(Box::new(move |sess, lint_store| {
  15. // technically we're ~guaranteed that this is none but might as well call anything that
  16. // is there already. Certainly it can't hurt.
  17. if let Some(previous) = &previous {
  18. (previous)(sess, lint_store);
  19. }
  20. let conf = clippy_lints::read_conf(sess);
  21. clippy_lints::register_plugins(lint_store, sess, &conf);
  22. clippy_lints::register_pre_expansion_lints(lint_store);
  23. clippy_lints::register_renamed(lint_store);
  24. }));
  25. // FIXME: #4825; This is required, because Clippy lints that are based on MIR have to be
  26. // run on the unoptimized MIR. On the other hand this results in some false negatives. If
  27. // MIR passes can be enabled / disabled separately, we should figure out, what passes to
  28. // use for Clippy.
  29. config.opts.debugging_opts.mir_opt_level = Some(0);
  30. }
  31. }

所以,Clippy 通过 ClippyCallbacksconfig 来注册 lints 。在 config 函数内部,通过调用 clippy_lints::read_conf(sess) 来读取 clippy 配置文件里的lint。在 clippy_lints 里还定义了 register_plugins,使用 rustc_lint::LintStore 来注册 clippy 里定义的 lints。

以上就是 Clippy 的工作机制。

自定义 Clippy lints

通过了解 Clippy 工作机制,可以看得出来,如果要自定义 Clippy lints,是需要严重依赖 rustc 版本的,因为 rustc_interface 提供的接口并不稳定。所以维护成本比较高。

如果一定要通过这种方式自定义 Clippy lints ,需要按以下步骤开发。

安装配置 Clippy
  1. 下载 Clippy 源码。
  2. 执行 cargo buildcargo test 。因为Clippy 测试套件非常大,所以可以只测试部分套件,比如,cargo uitest,或 cargo test --test dogfood。如果 UITest 和预期不符,可以使用 cargo dev bless更新相关文件。
  3. Clippy 提供了一些开发工具,可以通过 cargo dev --help 查看。

UI测试的目的是捕捉编译器的完整输出,这样我们就可以测试演示的所有方面。

测试正常的话,修改Clippy 生成二进制的名字,防止影响我们开发环境中安装的 Clippy命令。

  1. Cargo.toml中修改
  1. [[bin]]
  2. name = "cargo-myclippy" // 此处原本是 "cargo-clippy"
  3. test = false
  4. path = "src/main.rs"
  5. [[bin]]
  6. name = "clippy-mydriver" // 此处原本是 "clippy-mydriver"
  7. path = "src/driver.rs"
  1. 修改 src/main.rs
  1. .with_file_name("clippy-mydriver"); // 将使用 `clippy-driver` 的地方修改为 `clippy-mydriver`

起一个有意义的名字

定义 lints 需要先起一个符合 Lints 命名规范 的名字。

Lints 命名规范的首要原则就是:lint 名字要有意义。比如 allow dead_code,这是有意义的,但是allow unsafe_code这个就有点过分了。

具体来说,有几条注意事项:

  1. Lint 名称应该标明被检查的「坏东西」。比如 deprecated,所以,#[allow(deprecated)](items)是合法的。但是 ctypes就不如improper_ctypes 更明确。
  2. 命名要简洁。比如 deprecated,就比 deprecated_item更简洁。
  3. 如果一个 lint 应用于特定的语法,那么请使用复数形式。比如使用 unused_variables而不是unused_variable
  4. 捕捉代码中不必要的、未使用的或无用的方面的行文应该使用术语unused,例如unused_importsunused_typecasts
  5. lint 命名请使用蛇形(snake case)命名,与函数名的方式相同。

设置样板代码

假如新的 lint 叫 foo_functions,因为该lint不需要用到类型信息(比如某结构体是否实现 Drop),所以需要定义 EarlyLintPass。

在 Clippy 项目根目录下,通过以下命令创建 Lint:

  1. cargo dev new_lint --name=foo_functions --pass=early --category=pedantic

如果没有提供 category ,则默认是 nursery 。

执行完该命令以后,在 Clippy-lint/src/ 目录下就会多一个 foo_functions.rs的文件,文件中包含了样板代码:

  1. use rustc_lint::{EarlyLintPass, EarlyContext};
  2. use rustc_session::{declare_lint_pass, declare_tool_lint};
  3. use rustc_ast::ast::*;
  4. // 此宏用于定义 lint
  5. declare_clippy_lint! {
  6. /// **What it does:**
  7. ///
  8. /// **Why is this bad?**
  9. ///
  10. /// **Known problems:** None.
  11. ///
  12. /// **Example:**
  13. ///
  14. /// ```rust
  15. /// // example code where clippy issues a warning
  16. ///
  1. /// Use instead:
  2. /// ```rust
  3. /// // example code which does not raise clippy warning
  4. /// ```
  5. pub FOO_FUNCTIONS, // lint 名字大写
  6. pedantic, // lint 分类
  7. "default lint description" // lint 描述

}

// 定义 lint pass。 注意,lint 和 lint pass 并不一定成对出现 declare_lint_pass!(FooFunctions => [FOO_FUNCTIONS]);

// 因为不需要使用类型信息,此处实现 EarlyLintPass impl EarlyLintPass for FooFunctions {}

  1. 除了此文件,还会创建 `test/ui/foo_functions.rs` 测试文件。
  2. 接下来,需要执行 `cargo dev update_lints` 命令来注册新 lint
  3. <a name="897741c0"></a>
  4. ###### **添加 Lint pass 内容**
  5. 先来写一些测试代码。
  6. Clippy使用UI测试进行测试。UI测试检查Clippy的输出是否与预期完全一致。每个测试都是一个普通的Rust文件,包含我们要检查的代码。Clippy的输出与一个.stderr文件进行比较。注意,你不需要自己创建这个文件,我们将进一步讨论生成.stderr文件。
  7. 我们首先打开在test/ui/foo_functions.rs创建的测试文件。
  8. 用一些例子来更新该文件,以便开始使用。
  9. ```rust
  10. #![warn(clippy::foo_functions)]
  11. // Impl methods
  12. struct A;
  13. impl A {
  14. pub fn fo(&self) {}
  15. pub fn foo(&self) {}
  16. pub fn food(&self) {}
  17. }
  18. // Default trait methods
  19. trait B {
  20. fn fo(&self) {}
  21. fn foo(&self) {}
  22. fn food(&self) {}
  23. }
  24. // Plain functions
  25. fn fo() {}
  26. fn foo() {}
  27. fn food() {}
  28. fn main() {
  29. // We also don't want to lint method calls
  30. foo();
  31. let a = A;
  32. a.foo();
  33. }

可以使用 TESTNAME=foo_functions cargo uitest来执行测试。

可以看到输出:

  1. test [ui] ui/foo_functions.rs ... ok

接下来,打开 src/foo_functions.rs 编写 Lint 代码。

  1. declare_clippy_lint! {
  2. /// **What it does:**
  3. ///
  4. /// **Why is this bad?**
  5. ///
  6. /// **Known problems:** None.
  7. ///
  8. /// **Example:**
  9. ///
  10. /// ```rust
  11. /// // example code
  12. ///
  1. pub FOO_FUNCTIONS,
  2. pedantic, // 该类型的lint 等级 默认是 Allow
  3. "function named `foo`, which is not a descriptive name" // 修改 lint 声明的描述内容

}

  1. 可以通过执行 `cargo dev serve`在本地打开网页服务,可以查到 `foo_functions`显示的描述。
  2. ![foo-fn.png](https://cdn.nlark.com/yuque/0/2021/png/114846/1625061554493-c385474e-1db9-481c-b393-c5a9806e1a9f.png#clientId=uaf7a1696-2ad4-4&from=paste&height=1154&id=u6b4c5e04&margin=%5Bobject%20Object%5D&name=foo-fn.png&originHeight=1154&originWidth=2524&originalType=binary&ratio=1&size=185373&status=done&style=none&taskId=u8736c482-22dd-480b-a95f-dfe4e8a8659&width=2524)
  3. ``Pedantic`的默认lint 等级是`allow`,定义于 [https://github.com/rust-lang/rust-clippy/blob/master/clippy_lints/src/lib.rs#L119](https://github.com/rust-lang/rust-clippy/blob/master/clippy_lints/src/lib.rs#L119)
  4. 通常在声明了lint之后,我们必须运行`cargo dev update_lints` 来更新一些文件,以便 Clippy 知道新的 Lint。由于上面是用`cargo dev new_lint ...`命令来生成lint声明,所以这是自动完成的。
  5. 虽然 update_lints自动完成了大部分工作,但它并没有自动完成所有工作。我们必须在`clippy_lints/src/lib.rs`的`register_plugins`函数中手动注册我们的`lint pass`。
  6. ```rust
  7. pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf: &Conf) {
  8. // 此处省略 2000 行代码
  9. // foo_functions
  10. store.register_early_pass(|| box foo_functions::FooFunctions);
  11. }

该函数有 2000 多行代码,维护起来可想而知多么麻烦了。

因为此 lint pass 只是检查函数名字,不涉及类型检查,所以只需要 AST 层面的处理即可。关于 EarlyLintPass 和 LateLintPass 的区别前文已经介绍过。EarlyLintPass 比 LateLintPass 更快一些,然而 Clippy 的性能并不是关注的重点。

由于我们在检查函数名时不需要类型信息,所以在运行新的lint自动化时,我们使用了--pass=early,所有的样板导入都相应地被添加了。

下一步就可以实现 Lint 的检查逻辑了。

  1. // src/foo_functions.rs
  2. impl EarlyLintPass for FooFunctions {
  3. // 此处 check_fn 是内置 EarlyLintPass trait 包含方法,前文介绍过
  4. fn check_fn(&mut self, cx: &EarlyContext<'_>, fn_kind: FnKind<'_>, span: Span, _: NodeId) {
  5. // TODO: Emit lint here 此处编写检查逻辑
  6. }
  7. }

对于如何检查函数名字,在 clippy_utils/src/diagnostics.rs中定义了一些帮助函数。经过查找,span_lint_and_help函数在此处使用比较适合。

  1. // src/foo_functions.rs
  2. use clippy_utils::diagnostics::span_lint_and_help;
  3. use rustc_span::Span;
  4. use rustc_ast::{ast::NodeId, visit::FnKind};
  5. impl EarlyLintPass for FooFunctions {
  6. fn check_fn(&mut self, cx: &EarlyContext<'_>, fn_kind: FnKind<'_>, span: Span, _: NodeId) {
  7. span_lint_and_help(
  8. cx,
  9. FOO_FUNCTIONS,
  10. span,
  11. "function named `foo`",
  12. None,
  13. "consider using a more meaningful name"
  14. );
  15. }
  16. }

执行测试代码,输出如下:

uitest.jpeg

uitest2.jpeg

诊断信息是有效果了,但是缺乏一些lint检测逻辑。所以进一步修改:

  1. impl EarlyLintPass for FooFunctions {
  2. fn check_fn(&mut self, cx: &EarlyContext<'_>, fn_kind: FnKind<'_>, span: Span, _: NodeId) {
  3. // 增加判断逻辑
  4. fn is_foo_fn(fn_kind: FnKind<'_>) -> bool {
  5. match fn_kind {
  6. FnKind::Fn(_, ident, ..) => {
  7. // check if `fn` name is `foo`
  8. ident.name.as_str() == "foo"
  9. }
  10. // ignore closures
  11. FnKind::Closure(..) => false
  12. }
  13. }
  14. // 增加判断逻辑
  15. if is_foo_fn(fn_kind) {
  16. span_lint_and_help(
  17. cx,
  18. FOO_FUNCTIONS,
  19. span,
  20. "function named `foo`",
  21. None,
  22. "consider using a more meaningful name (考虑使用一个更有意义的函数名字)"
  23. );
  24. }
  25. }
  26. }

再次执行测试输出:

uitest3.png

接下来执行:

  1. cargo dev bless 更新 .stderr文件。这个 .stderr文件是需要提交的。如果测试出现错误,记得执行这一步。
  2. cargo test

执行 cargo test 失败,因为 clippy 不允许出现 中文描述。所以,修改:

  1. if is_foo_fn(fn_kind) {
  2. span_lint_and_help(
  3. cx,
  4. FOO_FUNCTIONS,
  5. span,
  6. "function named `foo`",
  7. None,
  8. "consider using a more meaningful name (考虑使用一个更有意义的函数名字)" // 此处不允许中文,当然你也可以修改 clippy 自身的 lint 配置
  9. );
  10. }
  11. // 修改为
  12. if is_foo_fn(fn_kind) {
  13. span_lint_and_help(
  14. cx,
  15. FOO_FUNCTIONS,
  16. span,
  17. "function named `foo`",
  18. None,
  19. "consider using a more meaningful name"
  20. );
  21. }

测试成功。

最后执行 cargo dev fmt,格式化代码。

到目前为止,自定义 clippy lint 已经完成。

测试 Clippy lint 效果

因为我们自定义的 Clippy 二进制名字已经被修改了,所以可以直接安装,不怕和已安装的clippy有冲突了。

执行以下命令安装自定义的Clippy:

  1. cargo install --bin=cargo-myclippy --bin=clippy-mydriver --path=.

然后重新使用 cargo new clippytest创建一个新项目。

src/main.rs修改为:

  1. #![warn(clippy::foo_functions)]
  2. // Impl methods
  3. struct A;
  4. impl A {
  5. pub fn fo(&self) {}
  6. pub fn foo(&self) {}
  7. pub fn food(&self) {}
  8. }
  9. // Default trait methods
  10. trait B {
  11. fn fo(&self) {}
  12. fn foo(&self) {}
  13. fn food(&self) {}
  14. }
  15. // Plain functions
  16. fn fo() {}
  17. fn foo() {}
  18. fn food() {}
  19. fn main() {
  20. // We also don't want to lint method calls
  21. foo();
  22. let a = A;
  23. a.foo();
  24. }

【如有必要】然后在 clippytest项目目录下创建 rust-toolchain 文件:

  1. [toolchain]
  2. channel = "nightly-2021-06-17"
  3. components = ["llvm-tools-preview", "rustc-dev", "rust-src"]

这个文件里的配置,要和 官方 rust-clippy 下一致,也就是你fork的那个原项目。

然后命令行执行:cargo myclippy,输出:

myclippy.png

成功!

然后回去 src/main.rs中,将 #![warn(clippy::foo_functions)] 改为 #![error(clippy::foo_functions)],再次执行 cargo myclippy,输出:

myclippy-error.jpeg

成功!

到此为止,自定义 Clippy Lint 成功!

小结

通过 fork clippy,完全可以定制自己的 Lint 。但是也有很明显的缺陷:

  1. Clippy 内置 lint 很多,需要手工注册自定义lint,想想那个 2000 行的函数就头疼。
  2. Clippy 依赖 rustc_interface 是未稳定的 API 。clippy_utils 里提供的helper方法也是依赖于编译器这个未稳定接口,这样不同编译器版本就会难以兼容。导致不能通用。
  3. 需要命名为自己的 Clippy 二进制文件,避免和原本的 Clippy 冲突。

如果自定义 Lint 可以 PR 更好,但并不是所有自定义 Lint 都可以提交到官方 PR ,必然需要维护自己的/团队的特殊场景的 Lint。就会面对上面的缺陷。

有没有更好的办法呢?

方法二:使用 Dylint

参考:Write Rust lints without forking Clippy

社区有人开发了一个工具: Dylint 。它的特点:

  1. 以动态库的方式来提供 lint 。而 Clippy 是静态库。Clippy 的所有 lint 都使用相同的编译器版本,因此只需要 rustc_driver
  2. Dylint 用户可以选择从不同编译器版本的库中加载 lint。

dylint.png

Dylint 可以动态构建 rustc_driver。换句话说,如果用户想要 A 版本的编译器库中加载 lint,并且找不到 A 版本的 rustc_driver,Dylint 将构建一个新的 A 版本的rustc_driverrustc_driver缓存在用户的主目录中,因此仅在必要时重建它们。

Dylint 根据它们使用的编译器版本对库进行分组,使用相同编译器版本的库一起加载,并和它们的 lint 一起运行。这允许在 lint 之间共享中间编译结果(如:符号解析,类型检查,trait求解等)。

在上图中,如果库 U 和 V 都使用了 A 版本的编译器,这两个库将被放到同一个分组中。A 版本编译器的rustc_driver将只被调用一次。rustc_driver在将控制权移交给 Rust 编译器之前会在库 U 和库 V 中注册 lint。

安装和配置

通过下面命令安全 dylint:

  1. cargo install cargo-dylint
  2. cargo install dylint-link

然后获取模版项目:

  1. git clone https://github.com/trailofbits/dylint-template

或者使用 cargo-generate来创建模版

  1. cargo generate --git https://github.com/trailofbits/dylint-template

将项目命名为 :mylints

然后进入到项目根目录,执行:

  1. cargo build
  2. cargo dylint fill_me_in --list

编写 lint

因为生成的模版其实和 上面 fork clippy 自定义生成的代码模版类似,所以直接将上面的 lint 代码复制过来。

创建新文件 src/foo_functions.rs

  1. use clippy_utils::diagnostics::span_lint_and_help;
  2. use rustc_ast::{ast::NodeId, visit::FnKind};
  3. use rustc_lint::{EarlyContext, EarlyLintPass};
  4. use rustc_span::Span;
  5. use rustc_lint::LateLintPass;
  6. use rustc_session::{declare_lint, declare_lint_pass};
  7. declare_lint! {
  8. /// **What it does:**
  9. /// 检查 以 foo 命名的函数,并给予警告
  10. /// **Why is this bad?**
  11. /// 因为该命名没有意义
  12. /// **Known problems:** None.
  13. ///
  14. /// **Example:**
  15. ///
  16. /// ```rust
  17. /// // example code where clippy issues a warning
  18. ///
  1. /// Use instead:
  2. /// 考虑使用一个更有意义的函数名字
  3. /// ```rust
  4. /// // example code which does not raise clippy warning
  5. /// ```
  6. pub FOO_FUNCTIONS,
  7. Warn, // 注意:这里和 fork Clippy 略有不同
  8. "function named `foo`, which is not a descriptive name"

}

declare_lint_pass!(FooFunctions => [FOO_FUNCTIONS]);

impl EarlyLintPass for FooFunctions { fn checkfn(&mut self, cx: &EarlyContext<’>, fnkind: FnKind<’>, span: Span, : NodeId) { fn is_foo_fn(fn_kind: FnKind<’>) -> bool { match fnkind { FnKind::Fn(, ident, ..) => { // check if fn name is foo ident.name.as_str() == “foo” }, // ignore closures FnKind::Closure(..) => false, } }

  1. if is_foo_fn(fn_kind) {
  2. span_lint_and_help(
  3. cx,
  4. FOO_FUNCTIONS,
  5. span,
  6. "function named `foo`",
  7. None,
  8. "consider using a more meaningful name",
  9. );
  10. }
  11. }

}

  1. 代码复制完毕之后,在 `src/lib.rs` 中添加:
  2. ```rust
  3. mod foo_functions;
  4. #[no_mangle]
  5. pub fn register_lints(_sess: &rustc_session::Session, lint_store: &mut rustc_lint::LintStore) {
  6. lint_store.register_lints(&[foo_functions::FOO_FUNCTIONS]);
  7. lint_store.register_early_pass(|| Box::new(foo_functions::FooFunctions));
  8. }

注意:需要配置当前项目下 .cargo/config.toml 中针对当前架构平台的 target 指定的链接器,否则会报 找不到库 之类的错误。

  1. [target.aarch64-apple-darwin]
  2. linker = "dylint-link"
  3. [target.x86_64-apple-darwin]
  4. linker = "dylint-link"
  5. [target.x86_64-unknown-linux-gnu]
  6. linker = "dylint-link"

然后执行 cargo build 编译成功。

接下来需要设置几个环境变量:

  1. export MY_LINTS_PATH=/Work/Projects/myworkspace/mylints
  2. export DYLINT_LIBRARY_PATH=$MY_LINTS_PATH/target/debug

然后执行 cargo test。可以看到 uitest 的输出。

但是 dylint 有个缺点,就是 uitest 无法像 clippy那样(cargo dev bless) 更新引用。所以 cargo test 会测试失败。

但是可以在 src/lib.rs 中,添加:

  1. #[allow(dead_code)]
  2. fn foo() {}

然后在 mylints项目下执行: cargo dylint --all 。就能看到 lint 生效了。

以上是我们编写了独立的 lints。

测试独立项目

随便创建一个 新的项目 myproject,将 src/main.rs 换成和前面测试 clippy 时候用的代码。

基于前面设置好的 mylints ,我们只需要直接使用 cargo dylint --all 命令即可。

然后在该项目根目录下执行:

  1. cargo dylint --all -- --manifest-path=/Work/Projects/myproject/Cargo.toml

然后就可以正常执行 lint 检测了。

小结

使用 dylint 比较麻烦的是,文档不是很全,测试不支持更新引用,不如 fork clippy 方便测试。

但是 dylint 确实比较小巧,只需要维护我们自定义的lint 即可,不再需要维护 2000 多行的注册lint代码。

使用 dylint 的时候,因为也依赖了 clippy 的 clippy_utils,所以需要和 clippy 的 rustc 版本保持一致。

总结

上面总结了两种定制 Clippy Lints 的方法,各有优劣。

一个观点:

  • 第一种方法比较适合 大公司/大团队,因为第一种方法比较完善,功能齐备,只是需要一个专门的团队来维护这个 lints。并且还有可能给上游去发 PR (如果需要),形成正向反馈,让工具更加完善。另外,也许可以给 Clippy 提供一个 Plugin 机制,方便维护定制的 Lint。
  • 第二种方法适合小团队,没有多余的人力去维护,只需要定制自己的一些 lints 使用即可。

欢迎在评论区留言交流。

有用的参考资源:

以下资源对你编写 lint 将很有帮助: