添加新的 lint
您来到这里很可能是因为您想为 Clippy 添加一个新的 lint。如果这是您第一次为 Clippy 做出贡献,本文档将指导您从头开始创建一个示例 lint。
首先,我们将创建一个 lint 来检测名为 foo
的函数,因为这显然是一个非描述性的名称。
设置
请参阅基础知识文档。
入门
创建新的 lint 时,需要设置一些样板代码。幸运的是,您可以使用 Clippy 开发工具来为您处理此问题。我们将新的 lint 命名为 foo_functions
(lint 通常以蛇形命名法书写),并且我们不需要类型信息,因此它将具有早期 pass 类型(稍后会详细介绍)。如果您不确定选择的名称是否适合 lint,请查看我们的lint 命名指南。
定义我们的 Lint
要开始,有两种方法来定义我们的 lint。
独立
命令:cargo dev new_lint --name=foo_functions --pass=early --category=pedantic
(如果未提供,类别将默认为 nursery)
此命令将创建一个新文件:clippy_lints/src/foo_functions.rs
,以及注册 lint。
特定类型
命令:cargo dev new_lint --name=foo_functions --type=functions --category=pedantic
此命令将创建一个新文件:clippy_lints/src/{type}/foo_functions.rs
。
请注意,此命令有一个 --type
标志,而不是 --pass
。与独立定义不同,此 lint 不会以传统意义上注册。相反,您将从类型 lint pass 中调用 lint,该 lint pass 位于 clippy_lints/src/{type}/mod.rs
中。
“类型”只是 clippy_lints/src
中目录的名称,例如示例命令中的 functions
。这些是具有常见行为的 lint 的分组,因此如果您的 lint 属于其中一个,则最好将其添加到该类型中。
测试位置
两个命令都将创建一个文件:tests/ui/foo_functions.rs
。对于 cargo lint,默认情况下会在 tests/ui-cargo
下创建两个项目层级结构(fail/pass)。
接下来,我们将打开这些文件并添加我们的 lint!
测试
让我们首先编写一些测试,以便在迭代 lint 时执行。
Clippy 使用 UI 测试进行测试。UI 测试检查 Clippy 的输出是否与预期完全一致。每个测试都只是一个纯 Rust 文件,其中包含我们要检查的代码。Clippy 的输出与 .stderr
文件进行比较。请注意,您不必自己创建此文件,我们将稍后介绍如何生成 .stderr
文件。
我们首先打开在 tests/ui/foo_functions.rs
创建的测试文件。
使用一些示例更新文件以开始
#![allow(unused)] #![warn(clippy::foo_functions)] // Impl methods struct A; impl A { pub fn fo(&self) {} pub fn foo(&self) {} pub fn food(&self) {} } // Default trait methods trait B { fn fo(&self) {} fn foo(&self) {} fn food(&self) {} } // Plain functions fn fo() {} fn foo() {} fn food() {} fn main() { // We also don't want to lint method calls foo(); let a = A; a.foo(); }
现在我们可以使用 TESTNAME=foo_functions cargo uibless
运行测试,但目前此测试没有意义。
在我们努力实现 lint 时,我们可以继续运行 UI 测试。这使我们可以通过检查每次测试运行时都会更新的 .stderr
文件来检查输出是否变为我们想要的。
运行 TESTNAME=foo_functions cargo uitest
应该自行通过。当我们提交 lint 时,我们也需要提交生成的 .stderr
文件。通常,您应该仅提交由 cargo bless
更改的文件,以便用于您正在创建/编辑的特定 lint。
注意:您可以通过指定逗号分隔的列表来运行多个测试文件:
TESTNAME=foo_functions,test2,test3
。
Cargo lint
对于 cargo lint,测试过程有所不同,因为我们对 Cargo.toml
清单文件感兴趣。我们还需要一个与该清单关联的最小 crate。
如果我们的新 lint 被命名为例如 foo_categories
,在运行 cargo dev new_lint --name=foo_categories --type=cargo --category=cargo
后,我们默认会找到两个新的 crate,每个都有其清单文件
tests/ui-cargo/foo_categories/fail/Cargo.toml
:此文件应导致新 lint 引发错误。tests/ui-cargo/foo_categories/pass/Cargo.toml
:此文件不应触发 lint。
如果您需要更多案例,可以复制其中一个 crate(在 foo_categories
下)并重命名它。
生成 .stderr
文件的过程相同,并且在 cargo uitest
前添加 TESTNAME
变量也有效。
Rustfix 测试
如果您正在处理的 lint 使用结构化建议,则测试将通过对该测试运行 rustfix 来创建 .fixed
文件。Rustfix 将 lint 中的建议应用于测试文件的代码,并将其与 .fixed
文件的内容进行比较。
使用 cargo bless
在运行测试时自动生成 .fixed
文件。
手动测试
如果您添加了一些 println!
并且测试套件输出变得难以理解,则手动针对示例文件进行测试可能会很有用。要使用本地修改来尝试 Clippy,请从 Clippy 目录运行以下命令
cargo dev lint input.rs
要在现有项目而不是单个文件上运行 Clippy,您可以使用
cargo dev lint /path/to/project
或者设置一个指向本地 Clippy 二进制文件的 rustup 工具链
cargo dev setup toolchain
# Then in `/path/to/project` you can run
cargo +clippy clippy
Lint 声明
首先,让我们打开在 clippy_lints
crate 中位于 clippy_lints/src/foo_functions.rs
的新创建的文件。这是所有 lint 代码所在的 crate。此文件已经导入了一些我们需要的初始内容
#![allow(unused)] fn main() { use rustc_lint::{EarlyLintPass, EarlyContext}; use rustc_session::declare_lint_pass; use rustc_ast::ast::*; }
下一步是更新 lint 声明。Lint 是使用 declare_clippy_lint!
宏声明的,我们只需要更新自动生成的 lint 声明以具有真实的描述,如下所示
#![allow(unused)] fn main() { declare_clippy_lint! { /// ### What it does /// /// ### Why is this bad? /// /// ### Example /// ```rust /// // example code /// ``` #[clippy::version = "1.29.0"] pub FOO_FUNCTIONS, pedantic, "function named `foo`, which is not a descriptive name" } }
- 以
///
为前缀的行部分构成了 lint 文档部分。这是默认的文档样式,将像这样显示。要在浏览器中本地呈现并打开此文档,请运行cargo dev serve
。 #[clippy::version]
属性将作为 lint 文档的一部分呈现。该值应设置为 lint 开发所在的当前 Rust 版本,可以通过在 rust-clippy 目录中运行rustc -vV
来检索。该版本列在 release 下。(使用不带-nightly
)后缀的版本。FOO_FUNCTIONS
是我们 lint 的名称。请务必在此处遵循lint 命名指南来命名您的 lint。简而言之,该名称应说明正在检查的内容,并且与allow
/warn
/deny
一起使用时应易于理解。pedantic
将 lint 级别设置为Allow
。确切的映射可以在此处找到- 最后一部分应该是一段文字,解释代码到底有什么问题
此文件的其余部分包含我们 lint pass 的空实现,在本例中为 EarlyLintPass
,应如下所示
#![allow(unused)] fn main() { // clippy_lints/src/foo_functions.rs // .. imports and lint declaration .. declare_lint_pass!(FooFunctions => [FOO_FUNCTIONS]); impl EarlyLintPass for FooFunctions {} }
Lint 注册
当使用 cargo dev new_lint
时,lint 会自动注册,无需执行更多操作。
当手动声明新的 lint 并且使用 cargo dev update_lints
时,可能必须在 clippy_lints/src/lib.rs
中的 register_lints
函数中手动注册 lint pass
store.register_early_pass(|| Box::new(foo_functions::FooFunctions));
正如人们可能期望的那样,还有一个相应的 register_late_pass
方法可用。如果不调用 register_early_pass
或 register_late_pass
中的一个,则不会运行有问题的 lint pass。
cargo dev update_lints
不会自动执行此步骤的一个原因是,多个 lint 可以使用相同的 lint pass,因此在添加新的 lint 时可能已经完成了注册 lint pass。此步骤未自动化的另一个原因是,注册 pass 的顺序决定了 pass 实际运行的顺序,这反过来又会影响任何发出的 lint 的输出顺序。
Lint pass
编写一个仅检查函数名称的 lint 意味着我们只需要处理 AST,而无需处理类型系统。这很好,因为它使编写这个特定的 lint 不那么复杂。
我们必须为每个新的 Clippy lint 做出此决定。归结为使用 EarlyLintPass
或 LateLintPass
。
简而言之,EarlyLintPass
在类型检查和 HIR 降低之前运行,而 LateLintPass
可以访问类型信息。除非您需要 EarlyLintPass
中的特定内容,否则请考虑使用 LateLintPass
。
由于我们不需要类型信息来检查函数名称,因此我们在运行新的 lint 自动化时使用了 --pass=early
,并且相应地添加了所有导入。
发出 lint
有了 UI 测试和 lint 声明,我们可以开始实现 lint 逻辑。
让我们首先为我们的 FooFunctions
实现 EarlyLintPass
impl EarlyLintPass for FooFunctions {
fn check_fn(&mut self, cx: &EarlyContext<'_>, fn_kind: FnKind<'_>, span: Span, _: NodeId) {
// TODO: Emit lint here
}
}
我们实现了 check_fn
方法,该方法来自 EarlyLintPass
trait。这使我们可以访问当前正在检查的函数的各种信息。更多内容将在下一节中介绍。让我们先忽略细节,并为每个函数定义发出 lint。
根据我们希望 lint 消息有多复杂,我们可以从各种 lint 发射函数中进行选择。它们都可以在 clippy_utils/src/diagnostics.rs
中找到。
span_lint_and_help
在这种情况下似乎最合适。它允许我们提供额外的帮助消息,并且我们无法自动建议更好的名称。它是这样的:
impl EarlyLintPass for FooFunctions {
fn check_fn(&mut self, cx: &EarlyContext<'_>, fn_kind: FnKind<'_>, span: Span, _: NodeId) {
span_lint_and_help(
cx,
FOO_FUNCTIONS,
span,
"function named `foo`",
None,
"consider using a more meaningful name"
);
}
}
现在运行我们的 UI 测试应该会产生包含 lint 消息的输出。
根据 rustc-dev-guide,文本应该实事求是,避免大写和句点,除非需要多个句子。当代码或标识符必须出现在消息或标签中时,应该用单反引号 ` 包围。
添加 lint 逻辑
你的 lint 逻辑很可能与我们的示例不同,因此本节内容较短。
使用 check_fn
方法使我们可以访问 FnKind
,它具有 FnKind::Fn
变体。它通过 Ident
提供对函数/方法名称的访问。
有了这些,我们可以扩展我们的 check_fn
方法为:
#![allow(unused)] fn main() { impl EarlyLintPass for FooFunctions { fn check_fn(&mut self, cx: &EarlyContext<'_>, fn_kind: FnKind<'_>, span: Span, _: NodeId) { if is_foo_fn(fn_kind) { span_lint_and_help( cx, FOO_FUNCTIONS, span, "function named `foo`", None, "consider using a more meaningful name" ); } } } }
我们将 lint 条件与 lint 发射分开,因为它使代码更容易阅读。在某些情况下,这种分离还可以为单独的函数编写一些单元测试(而不仅仅是 UI 测试)。
在我们的示例中,is_foo_fn
看起来像:
#![allow(unused)] fn main() { // use statements, impl EarlyLintPass, check_fn, .. fn is_foo_fn(fn_kind: FnKind<'_>) -> bool { match fn_kind { FnKind::Fn(_, ident, ..) => { // check if `fn` name is `foo` ident.name.as_str() == "foo" } // ignore closures FnKind::Closure(..) => false } } }
现在我们还应该使用 cargo test
运行完整的测试套件。此时运行 cargo test
应该会产生预期的输出。记住运行 cargo bless
来更新 .stderr
文件。
cargo test
(与 cargo uitest
相反)还将确保我们的 lint 实现本身不会违反任何 Clippy lint。
lint 实现就到此为止了。现在运行 cargo test
应该会通过。
指定 lint 的最低支持 Rust 版本 (MSRV)
有时,lint 会提出需要特定 Rust 版本的建议。例如,manual_strip
lint 建议使用 str::strip_prefix
和 str::strip_suffix
,它们仅在 Rust 1.45 之后可用。在这种情况下,你需要确保为项目配置的 MSRV >= 所需 Rust 功能的 MSRV。如果需要多个功能,只需使用 MSRV 较低的一个。
首先,在 clippy_config::msrvs
中为所需功能添加 MSRV 别名。例如,稍后可以将其作为 msrvs::STR_STRIP_PREFIX
访问。
#![allow(unused)] fn main() { msrv_aliases! { .. 1,45,0 { STR_STRIP_PREFIX } } }
为了访问项目配置的 MSRV,你需要在 LintPass 结构中包含 msrv
字段,并使用构造函数初始化该字段。msrv
值将在 clippy_lints/lib.rs
中传递给构造函数。
#![allow(unused)] fn main() { pub struct ManualStrip { msrv: Msrv, } impl ManualStrip { pub fn new(conf: &'static Conf) -> Self { Self { msrv: conf.msrv.clone() } } } }
然后可以使用 Msrv::meets
方法在 LintPass 中将项目的 MSRV 与功能 MSRV 进行匹配。
#![allow(unused)] fn main() { if !self.msrv.meets(msrvs::STR_STRIP_PREFIX) { return; } }
项目的 MSRV 也可以指定为属性,它会覆盖 clippy.toml
中的值。可以使用 extract_msrv_attr!(LintContext)
宏并传递 LateContext
/EarlyContext
来解决此问题。
impl<'tcx> LateLintPass<'tcx> for ManualStrip {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
...
}
extract_msrv_attr!(LateContext);
}
将 msrv
添加到 lint 后,应将相关的测试用例添加到 lint 的测试文件 tests/ui/manual_strip.rs
中(在本示例中)。它应该包含一个低于 MSRV 的版本的用例和一个内容相同但用于 MSRV 版本本身的用例。
...
#[clippy::msrv = "1.44"]
fn msrv_1_44() {
/* something that would trigger the lint */
}
#[clippy::msrv = "1.45"]
fn msrv_1_45() {
/* something that would trigger the lint */
}
最后一步,应将 lint 添加到 lint 文档中。这在 clippy_config/src/conf.rs
中完成。
#![allow(unused)] fn main() { define_Conf! { /// Lint: LIST, OF, LINTS, <THE_NEWLY_ADDED_LINT>. The minimum rust version that the project supports (msrv: Option<String> = None), ... } }
之后,按照向 lint 添加配置中所述更新本书的文档。
编写 lint
如果你在实现 lint 时遇到问题,还可以使用内部的 author
lint 来生成检测违规模式的 Clippy 代码。它不适用于所有 Rust 语法,但可以提供一个很好的起点。
使用它的最快方法是 Rust playground: play.rust-lang.org。将要 lint 的代码放入编辑器中,并在项目上方添加 #[clippy::author]
属性。然后通过 Tools -> Clippy
运行 Clippy,你应该会在下面的输出中看到生成的代码。
这里是在 playground 上的一个示例。
如果命令执行成功,你可以将代码复制到你实现 lint 的位置。
打印 HIR lint
要实现一个 lint,首先了解 rustc 使用的内部表示很有帮助。Clippy 具有 #[clippy::dump]
属性,该属性会打印附加该属性的项目、语句或表达式的高层中间表示 (HIR)。要将属性附加到表达式,通常需要启用 #![feature(stmt_expr_attributes)]
。
这里你可以找到一个示例,只需选择Tools并运行Clippy。
文档
提交 PR 之前的最后一件事是为我们的 lint 声明添加一些文档。
请使用类似于以下的文档注释来记录你的 lint
#![allow(unused)] fn main() { declare_clippy_lint! { /// ### What it does /// Checks for ... (describe what the lint matches). /// /// ### Why is this bad? /// Supply the reason for linting the code. /// /// ### Example /// /// ```rust,ignore /// // A short example of code that triggers the lint /// ``` /// /// Use instead: /// ```rust,ignore /// // A short example of improved code that doesn't trigger the lint /// ``` #[clippy::version = "1.29.0"] pub FOO_FUNCTIONS, pedantic, "function named `foo`, which is not a descriptive name" } }
如果 lint 在 restriction
组中,因为它 lint 的东西不一定是“坏”的,而更多是一种风格选择,则将“为什么这很糟糕?”部分标题替换为“为什么要限制它?”,以避免编写“为什么这很糟糕?它不是,但是……”。
一旦你的 lint 被合并,此文档将显示在 lint 列表中。
运行 rustfmt
Rustfmt 是一个根据样式指南格式化 Rust 代码的工具。你的代码必须在 PR 被合并之前通过 rustfmt
格式化。Clippy 在 CI 中使用 nightly rustfmt
。
可以通过 rustup
安装它
rustup component add rustfmt --toolchain=nightly
使用 cargo dev fmt
来格式化整个代码库。确保为 nightly 工具链安装了 rustfmt
。
调试
如果你想调试 lint 实现的某些部分,可以在代码中的任何位置使用 dbg!
宏。然后运行测试应将调试输出包含在 stdout
部分中。
冲突的 lint
有几个 lint 处理相同的模式,但建议使用不同的方法。换句话说,某些 lint 可能建议进行修改,其方向与某些其他 lint 针对同一代码已经提出的方向相反,从而产生冲突的诊断。
当你创建最终出现在这种情况下的 lint 时,应鼓励遵循以下提示来指导分类
- 它们应该在同一类别中的唯一情况是该类别是
restriction
。例如,semicolon_inside_block
和semicolon_outside_block
。 - 对于所有其他情况,它们应该在具有不同允许级别的不同类别中。例如,
implicit_return
(restriction,allow)和needless_return
(style,warn)。
对于属于不同类别的 lint,还建议至少其中一个应属于 restriction
类别。原因是 restriction
组是唯一一个不建议启用整个集合,而是从中挑选 lint 的组。
PR 清单
在提交 PR 之前,请确保你已遵循所有基本要求
- [ ] 遵循了 lint 命名约定
- [ ] 添加了通过的 UI 测试(包括提交的
.stderr
文件) - [ ]
cargo test
在本地通过 - [ ] 执行了
cargo dev update_lints
- [ ] 添加了 lint 文档
- [ ] 运行了
cargo dev fmt
为 lint 添加配置
Clippy 支持使用 clippy.toml
文件配置 lint 值,该文件在以下位置中搜索:
CLIPPY_CONF_DIR
环境变量指定的目录,或- CARGO_MANIFEST_DIR 环境变量指定的目录,或
- 当前目录。
向 lint 添加配置对于阈值或约束某些行为(对于某些用户来说可能被视为误报)很有用。添加配置分以下几个步骤进行:
-
向
clippy_config::conf
添加新的配置条目,如下所示:/// Lint: LINT_NAME. /// /// <The configuration field doc comment> (configuration_ident: Type = DefaultValue),
文档注释会自动添加到列出的 lint 的文档中。默认值将使用类型的
Debug
实现进行格式化。 -
将配置值添加到 lint impl 结构中
-
这首先需要定义一个 lint impl 结构。Lint impl 结构通常使用
declare_lint_pass!
宏生成。为了向其添加某种元数据,需要手动定义此结构#![allow(unused)] fn main() { // Generated struct definition declare_lint_pass!(StructName => [ LINT_NAME ]); // New manual definition struct pub struct StructName {} impl_lint_pass!(StructName => [ LINT_NAME ]); }
-
接下来添加配置值和相应的创建方法,如下所示:
#![allow(unused)] fn main() { pub struct StructName { configuration_ident: Type, } // ... impl StructName { pub fn new(conf: &'static Conf) -> Self { Self { configuration_ident: conf.configuration_ident, } } } }
-
-
将配置值传递给 lint impl 结构
首先在
clippy_lints
lib 文件中查找结构构造。现在,配置值将被克隆或复制到本地值中,然后传递给 impl 结构,如下所示:// Default generated registration: store.register_*_pass(|| box module::StructName); // New registration with configuration value store.register_*_pass(move || box module::StructName::new(conf));
恭喜,工作即将完成。现在可以通过
self.configuration_ident
在 lint 代码中访问配置值。 -
添加测试
- 可以在
tests/ui
中像任何普通 lint 一样测试默认配置值。 - 配置本身将在
tests/ui-toml
中单独进行测试。只需添加一个名称合适的子文件夹。此文件夹包含一个具有配置值的clippy.toml
文件和一个应该由 Clippy lint 的 rust 文件。否则,可以像往常一样编写测试。
- 可以在
-
更新 Lint 配置
运行
cargo bless --test config-metadata
以生成本书的文档更改。
速查表
这里有一些指针,指向你可能需要为每个 lint 做的事情
- Clippy utils - 各种辅助函数。你可能需要的函数可能已经在这里了(
is_type_diagnostic_item
、implements_trait
、snippet
等) - Clippy 诊断
- Let 链
from_expansion
和in_external_macro
Span
适用性
- 编写 lint 的常用工具可以帮助进行常用操作
- rustc-dev-guide 解释了许多内部编译器概念
- nightly rustc 文档,本指南中已多次链接到该文档
对于 EarlyLintPass
lint
对于 LateLintPass
lint
虽然 Clippy 的大部分 lint 工具都有文档,但目前 rustc 的大部分内部结构都缺乏文档。这很遗憾,但在大多数情况下,你可能可以通过复制现有类似 lint 的代码来解决问题。如果你遇到困难,请随时在 Zulip 或在 issue/PR 中提问。