02-添加 Lint
12 分钟阅读
译文 · 基于 Clippy Documentation
添加 Lint
原文链接: https://doc.rust-lang.org/nightly/clippy/development/adding_lints.html
你很可能是因为想为 Clippy 添加新 lint 才来到这里。若这是你第一次为 Clippy 做贡献,本文将引导你从零开始创建一个示例 lint。
我们将创建一个检测名为 foo 的函数的 lint,因为显然这不是一个描述性的名称。
设置
请参阅基础文档。
入门
创建新 lint 时需要设置一些样板代码。幸运的是,你可以使用 Clippy 开发工具来处理这些。我们将新 lint 命名为 foo_functions(lint 名称通常使用 snake_case),且不需要类型信息,因此它将使用 early pass 类型(稍后会详细介绍)。若不确定所选名称是否适合该 lint,请查看我们的 lint 命名指南。
定义我们的 Lint
要入门,有两种方式定义我们的 lint。
独立 lint
命令:cargo dev new_lint --name=foo_functions --pass=early --category=pedantic
(若未提供 category,默认为 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,该 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 打开创建的测试文件。
用一些示例更新该文件以入门:
| |
注意我们在期望报错的行上添加了带 lint 名称的注释标注。除非常特殊的情况(//@check-pass),测试文件必须至少包含一个错误标记才会被接受。
实现 lint 后,可运行 TESTNAME=foo_functions cargo uibless 生成 .stderr 文件。若 lint 使用了结构化建议,该命令还会生成对应的 .fixed 文件。
在实现 lint 的过程中,可以持续运行 UI 测试。每次测试运行都会更新 .stderr 文件,便于检查输出是否符合预期。
实现 lint 后,运行 TESTNAME=foo_functions cargo uitest 应能单独通过。提交 lint 时,还需提交生成的 .stderr 文件,以及(如适用).fixed 文件。一般而言,只应提交由 cargo bless 为正在创建/编辑的特定 lint 更改的文件。
注意: 可通过逗号分隔的列表指定多个测试文件:
TESTNAME=foo_functions,test2,test3。
Cargo lint
cargo lint 的测试流程不同,我们关注的是 Cargo.toml manifest 文件,还需要与之关联的最小 crate。
若新 lint 名为 foo_categories,运行 cargo dev new_lint --name=foo_categories --type=cargo --category=cargo 后,默认会找到两个新 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 目录中运行:
| |
要对现有项目而非单个文件运行 Clippy,可使用:
| |
或设置指向本地 Clippy 二进制文件的 rustup toolchain:
| |
Lint 声明
先在 clippy_lints crate 中打开新创建的文件 clippy_lints/src/foo_functions.rs。所有 lint 代码都在该 crate 中。该文件已导入一些初始所需内容:
| |
下一步是更新 lint 声明。Lint 使用 declare_clippy_lint! 宏声明,我们只需将自动生成的 lint 声明更新为真实描述,类似如下:
| |
- 以
///开头的行构成 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,应类似如下:
| |
Lint 注册
使用 cargo dev new_lint 时,lint 会自动注册,无需其他操作。
手动声明新 lint 并使用 cargo dev update_lints 时,可能需通过在 clippy_lints/src/lib.rs 的 early_lint_methods! 宏调用中、// add early passes here 标记处添加条目来手动注册 lint pass:
FooFunctions: foo_functions::FooFunctions = foo_functions::FooFunctions,
如你所料,也有对应的 late_lint_methods! 宏。若未在 early_lint_methods! 或 late_lint_methods! 中添加条目,对应的 lint pass 将不会运行。
cargo dev update_lints 不自动化此步骤的原因之一是,多个 lint 可共用同一 lint pass,添加新 lint 时 lint pass 可能已注册。另一原因是所列 pass 的顺序决定实际运行顺序,进而影响发出 lint 的输出顺序。
Lint 遍历
编写只检查函数名称的 lint 意味着我们只需处理 AST,完全不必处理类型系统。这很好,因为使该 lint 的实现更简单。
每个新 Clippy lint 都要做此决定。归根结底是使用 EarlyLintPass 还是 LateLintPass。
EarlyLintPass 在类型检查和 HIR 降级之前运行,而 LateLintPass 在这些阶段之后运行,可访问类型信息。cargo dev new_lint 命令默认使用推荐的 LateLintPass,若 lint 只需 AST 级分析,可指定 --pass=early。
由于检查函数名不需要类型信息,我们在运行新 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: 在此发出 lint
}
}
我们实现 EarlyLintPass trait 的 check_fn 方法。这让我们能访问当前被检查函数的各种信息。下一节会详述。先不管细节,先对每个函数定义发出 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 方法为:
| |
我们将 lint 条件与 lint 发出分离,使代码更易读。某些情况下这种分离还允许为独立函数编写单元测试(而不仅是 UI 测试)。
在我们的示例中,is_foo_fn 如下:
| |
现在还应使用 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_utils::msrvs 中为所需特性添加 MSRV 别名。例如之后可访问为 msrvs::STR_STRIP_PREFIX。
| |
要访问项目配置的 MSRV,需在 LintPass struct 中有 msrv 字段,以及初始化该字段的构造函数。msrv 值在 clippy_lints/lib.rs 中传给构造函数。
| |
然后可在 LintPass 中使用 Msrv::meets 方法将项目 MSRV 与特性 MSRV 匹配。
| |
Early lint pass 应改用 MsrvStack 配合 extract_msrv_attr!()
将 msrv 添加到 lint 后,应在 lint 测试文件(本例为 tests/ui/manual_strip.rs)中添加相关测试用例。应包含低于 MSRV 的版本用例,以及相同内容但针对 MSRV 版本本身的用例。
...
#[clippy::msrv = "1.44"]
fn msrv_1_44() {
/* 会触发该 lint 的代码 */
}
#[clippy::msrv = "1.45"]
fn msrv_1_45() {
/* 会触发该 lint 的代码 */
}
最后一步,应将 lint 添加到 lint 文档。这在 clippy_config/src/conf.rs 中完成:
| |
之后按为 lint 添加配置中的说明更新 book 文档。
编写 lint 说明
若实现 lint 遇到困难,还有内部 author lint 可生成检测违规模式的 Clippy 代码。它并非适用于所有 Rust 语法,但可给出良好起点。
最快用法是 Rust playground:play.rust-lang.org。将要 lint 的代码放入编辑器,在项上方添加 #[clippy::author] 属性。然后通过 Tools -> Clippy 运行 Clippy,输出中应能看到生成的代码。
此处有 playground 示例。
若命令执行成功,可将代码复制到实现 lint 的位置。
Print HIR lint
实现 lint 时,先理解 rustc 使用的内部表示很有帮助。Clippy 有 #[clippy::dump] 属性,会打印属性所附项、语句或表达式的[高级中间表示(HIR)]。要为表达式附加属性,通常需启用 #![feature(stmt_expr_attributes)]。
此处有示例,选择 Tools 并运行 Clippy 即可。
文档
提交 PR 前的最后一步是为 lint 声明添加文档。
请用类似以下的 doc 注释记录 lint:
| |
若 lint 因 lint 的内容不一定是“不好”而更多是风格选择,属于 restriction 组,则将“为何不好?”小节标题替换为“为何限制?”,避免写“为何不好?其实不算不好,但 …”。
lint 合并后,该文档会出现在 lint 列表 中。
运行 rustfmt
Rustfmt 是按风格指南格式化 Rust 代码的工具。PR 合并前代码必须经过 rustfmt 格式化。Clippy 在 CI 中使用 nightly rustfmt。
可通过 rustup 安装:
| |
使用 cargo dev fmt 格式化整个代码库。确保 nightly toolchain 已安装 rustfmt。
调试
若要调试 lint 实现的部分,可在代码任意处使用 dbg! 宏。运行测试时调试输出会出现在 stdout 部分。
冲突的 lint
有些 lint 处理相同模式但建议不同做法。换言之,某些 lint 可能建议的修改与另一些 lint 对同一代码的建议方向相反,产生冲突的诊断。
当你创建的 lint 处于这种场景时,以下建议可指导分类:
- 它们应处于同一 category 的唯一情况是 category 为
restriction。例如semicolon_inside_block和semicolon_outside_block。 - 其他所有情况,它们应处于不同 category,且 allow 级别不同。例如
implicit_return(restriction,allow)和needless_return(style,warn)。
对于处于不同 category 的 lint,还建议至少其中一个应在 restriction category。原因是 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. /// /// <配置字段 doc 注释> (configuration_ident: Type = DefaultValue),doc 注释会自动添加到所列 lint 的文档中。默认值会使用类型的
Debug实现格式化。将配置值添加到 lint impl struct:
首先需要定义 lint impl struct。Lint impl struct 通常由
declare_lint_pass!宏生成。需手动定义 struct 以添加某种元数据:1 2 3 4 5 6 7 8 9 10 11// 生成的 struct 定义 declare_lint_pass!(StructName => [ LINT_NAME ]); // 新的手动 struct 定义 pub struct StructName {} impl_lint_pass!(StructName => [ LINT_NAME ]);接下来添加配置值及对应的创建方法:
1 2 3 4 5 6 7 8 9 10 11 12 13pub struct StructName { configuration_ident: Type, } // ... impl StructName { pub fn new(conf: &'static Conf) -> Self { Self { configuration_ident: conf.configuration_ident, } } }
将配置值传给 lint impl struct:
先在
clippy_lintslib file 中找到 struct 构造。配置值现在被 clone 或 copy 到局部变量,然后传给 impl struct:// 默认生成的注册: store.register_*_pass(|| box module::StructName); // 带配置值的新注册 store.register_*_pass(move || box module::StructName::new(conf));恭喜,工作几乎完成。配置值现在可通过
self.configuration_ident在 lint 代码中访问。添加测试:
- 默认配置值可像普通 lint 一样在
tests/ui中测试。 - 配置本身会在
tests/ui-toml中单独测试。只需添加名称合适的新子文件夹。该文件夹包含带配置值的clippy.toml文件,以及应由 Clippy lint 的 rust 文件。测试写法可照常进行。
- 默认配置值可像普通 lint 一样在
更新 Lint 配置
运行
cargo bless --test config-metadata为 book 生成文档变更。
速查表
以下是每个 lint 可能需要的参考:
- Clippy utils - 各种辅助函数。也许所需函数已存在(
implements_trait、snippet等) - Clippy diagnostics
- Let chains
from_expansion和in_external_macroSpanApplicability- 编写 lint 的常用工具 帮助常见操作
- rustc-dev-guide 解释许多编译器内部概念
- nightly rustc 文档 本指南中多处链接
对于 EarlyLintPass lint:
对于 LateLintPass lint:
Clippy 的大多数 lint 工具都有文档,但 rustc 内部大多目前缺乏文档。这很遗憾,但多数情况下你可以从现有类似 lint 复制。若卡住,欢迎在 Zulip 或 issue/PR 中提问。