添加一个新的 lint

您来到这里很可能是因为您想向 Clippy 添加一个新的 lint。如果这是您第一次为 Clippy 贡献代码,本文档将指导您从头开始创建一个示例 lint。

首先,我们将创建一个 lint,用于检测名为 foo 的函数,因为这显然是一个描述性不足的名称。

设置

请参阅 Basics 文档。

开始入门

创建新的 lint 时,需要设置一些样板代码。幸运的是,您可以使用 Clippy 开发工具来处理这些。我们将新的 lint 命名为 foo_functions(lint 通常以蛇形命名法编写),并且我们不需要类型信息,因此它将具有 early 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 中。

“type” 只是 clippy_lints/src 中目录的名称,例如示例命令中的 functions。这些是具有共同行为的 lint 的分组,因此如果您的 lint 属于其中一种类型,最好将其添加到该类型中。

测试位置

这两个命令都将创建一个文件:tests/ui/foo_functions.rs。对于 cargo lints,默认情况下将在 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 lints

对于 cargo lints,测试过程的不同之处在于我们对 Cargo.toml manifest 文件感兴趣。我们还需要一个与该 manifest 关联的最小 crate。

如果我们的新 lint 被命名为例如 foo_categories,在运行 cargo dev new_lint --name=foo_categories --type=cargo --category=cargo 后,我们默认会找到两个新的 crate,每个 crate 都有其 manifest 文件

  • 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 声明。Lints 使用 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_passregister_late_pass 之一,则不会运行所讨论的 lint pass。

cargo dev update_lints 不会自动执行此步骤的一个原因是,多个 lints 可以使用相同的 lint pass,因此在添加新的 lint 时可能已经完成了 lint pass 的注册。此步骤未自动化的另一个原因是,pass 的注册顺序决定了 pass 实际运行的顺序,这反过来会影响任何发出的 lints 的输出顺序。

Lint pass

编写一个仅检查函数名称的 lint 意味着我们只需要处理 AST,而无需处理类型系统。这很好,因为它使编写这个特定的 lint 不那么复杂。

我们必须为每个新的 Clippy lint 做出这个决定。它归结为使用 EarlyLintPassLateLintPass

简而言之,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 lints。

对于 lint 实现来说,这应该就足够了。现在运行 cargo test 应该通过。

指定 lint 的最低支持 Rust 版本 (MSRV)

有时,lint 会提出需要特定 Rust 版本的建议。例如,manual_strip lint 建议使用 str::strip_prefixstr::strip_suffix,它们仅在 Rust 1.45 之后可用。在这种情况下,您需要确保为项目配置的 MSRV >= 所需 Rust 功能的 MSRV。如果需要多个功能,只需使用 MSRV 较低的功能即可。

首先,在 clippy_utils::msrvs 中为所需的功能添加 MSRV 别名。稍后可以将其作为 msrvs::STR_STRIP_PREFIX 访问,例如。

#![allow(unused)] fn main() { msrv_aliases! { .. 1,45,0 { STR_STRIP_PREFIX } } }

为了访问项目配置的 MSRV,您需要在 LintPass struct 中有一个 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 添加配置 中所述更新本书的文档。

Author lint

如果您在实现 lint 时遇到问题,还可以使用内部 author lint 来生成检测违规模式的 Clippy 代码。它并非适用于所有 Rust 语法,但可以提供一个良好的起点。

使用它的最快方法是 Rust playground: play.rust-lang.org。将要 lint 的代码放入编辑器中,并在项目上方添加 #[clippy::author] 属性。然后通过 Tools -> Clippy 运行 Clippy,您应该在下面的输出中看到生成的代码。

这里 是 playground 上的一个示例。

如果命令执行成功,您可以将代码复制到您正在实现 lint 的位置。

要实现 lint,首先了解 rustc 使用的内部表示形式很有帮助。Clippy 具有 #[clippy::dump] 属性,该属性打印附加到该属性的项目、语句或表达式的 High-Level Intermediate Representation (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 的内容不一定是“坏的”,而更多的是一种风格选择,那么请将“Why is this bad?”部分标题替换为“Why restrict this?”,以避免写成“Why is this bad? It isn't, but ...”。

一旦您的 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 部分中包含调试输出。

冲突的 lints

有几个 lints 处理相同的模式,但建议不同的方法。换句话说,一些 lints 可能会建议修改,这些修改与某些其他 lints 已经为同一代码提出的建议方向相反,从而产生冲突的诊断。

当您创建一个最终会出现在这种情况下的 lint 时,应鼓励以下提示来指导分类

  • 它们应该属于同一类别的唯一情况是该类别是 restriction。例如,semicolon_inside_blocksemicolon_outside_block
  • 对于所有其他情况,它们应属于具有不同允许级别的不同类别。例如,implicit_return (restriction, allow) 和 needless_return (style, warn)。

对于属于不同类别的 lints,还建议至少其中一个应属于 restriction 类别。这样做的原因是 restriction 组是唯一一个我们不建议启用整个集合,而是从中挑选 lints 的组。

PR 检查清单

在提交 PR 之前,请确保您已遵循所有基本要求

  • [ ] 遵循 lint 命名约定
  • [ ] 添加了通过的 UI 测试(包括提交的 .stderr 文件)
  • [ ] cargo test 在本地通过
  • [ ] 执行了 cargo dev update_lints
  • [ ] 添加了 lint 文档
  • [ ] 运行了 cargo dev fmt

为 lint 添加配置

Clippy 支持使用 clippy.toml 文件配置 lints 值,该文件在以下位置搜索

  1. CLIPPY_CONF_DIR 环境变量指定的目录,或
  2. CARGO_MANIFEST_DIR 环境变量指定的目录,或
  3. 当前目录。

为 lint 添加配置对于阈值或约束某些行为可能很有用,对于某些用户来说,这些行为可能被视为误报。添加配置按以下步骤完成

  1. clippy_config::conf 添加新的配置条目,如下所示

    /// Lint: LINT_NAME. /// /// <The configuration field doc comment> (configuration_ident: Type = DefaultValue),

    文档注释会自动添加到列出的 lints 的文档中。默认值将使用类型的 Debug 实现进行格式化。

  2. 将配置值添加到 lint impl struct

    1. 这首先需要定义 lint impl struct。Lint impl structs 通常使用 declare_lint_pass! 宏生成。需要手动定义此 struct 以向其添加某种元数据

      #![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 ]); }
    2. 接下来,添加配置值和相应的创建方法,如下所示

      #![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, } } } }
  3. 将配置值传递给 lint impl struct

    首先在 clippy_lints lib 文件 中找到 struct 构造。现在将配置值克隆或复制到本地值中,然后将其传递给 impl struct,如下所示

    // Default generated registration: store.register_*_pass(|| box module::StructName); // New registration with configuration value store.register_*_pass(move || box module::StructName::new(conf));

    恭喜,工作几乎完成。现在可以在 lint 代码中通过 self.configuration_ident 访问配置值。

  4. 添加测试

    1. 默认配置值可以像 tests/ui 中的任何普通 lint 一样进行测试。
    2. 配置本身将在 tests/ui-toml 中单独测试。只需添加一个名称合适的子文件夹。此文件夹包含一个带有配置值的 clippy.toml 文件和一个应由 Clippy lint 的 rust 文件。否则,测试可以像往常一样编写。
  5. 更新 Lint 配置

    运行 cargo bless --test config-metadata 为本书生成文档更改。

速查表

以下是一些您可能在每个 lint 中都需要的指针

对于 EarlyLintPass lints

对于 LateLintPass lints

虽然 Clippy 的大多数 lint utils 都有文档记录,但 rustc 的大多数内部结构目前都缺乏文档。这很不幸,但在大多数情况下,您可能可以通过从现有类似 lints 中复制内容来解决问题。如果您遇到困难,请随时在 Zulip 上或在 issue/PR 中提问。