一、用途
使用Rust的开发者大部分都接触过Clippy,正如使用C或Python的的开发者接触过Pclint和Pylint一样,那么这些工具是做什么的呢?Clippy的readme一句话讲的很清楚,Clippy是用于捕获常见错误和改进Rust代码的lint集合。这里的lint是一种代码静态检查项,触发后发出编译告警。lint检查是对用户的约束,有些lint是需要强制遵守的,有些是可选的,lint跟编译器的其他静态检查还有一个重要的区别是很多lint检查的问题是不影响编译和运行的。
Clippy lint分类如下表所示,告警级别分为warn:发出告警,建议修改;deny:禁止的严重问题;allow:允许的情况,只是建议。
Clippy的lint分组如下:
分组 | 描述 | 默认级别 |
---|---|---|
clippy::all | 默认的所有lint (correctness, suspicious, style, complexity, perf) | warn/deny |
clippy::correctness | 完全错误或无用的代码 | deny |
clippy::suspicious | 很可能是错误或无用的代码 | warn |
clippy::style | 应该以更习惯的方式编写的代码 | warn |
clippy::complexity | 以复杂的方式完成简单工作的代码 | warn |
clippy::perf | 可以被写得运行更快的代码 | warn |
clippy::pedantic | 相当严格的lint,但偶尔可能有误报 | allow |
clippy::nursery | 仍在开发中的新lint | allow |
clippy::cargo | 适用于cargo的lint | allow |
二、原理
2.1 编译器
说到Clippy,不得不提Rust的编译器前端Rustc,Clippy基于Rustc提供的插件机制,将Clippy中的lints注册到Rustc的lint集合中,Rustc有专门的编译过程来执行这些lint检查。
Rust的编译器Rustc根据编译过程的不同功能将编译器代码分为了60+个Crate,其中crate:rustc_driver充当Rustc执行的驱动程序,可以理解为main函数,它通过调用crate:rustc_interface提供的接口,以正确的顺序运行编译器的各个阶段。
lint的检查是在编译器的中间表示上执行的,下表是编译过程中包含的一些重要的中间表示:
名称 | 描述 |
---|---|
Token Stream | Token Stream是词法分析器lexer直接从源代码生成的令牌流。对于解析器parse来说,这个标记流比原始文本更容易处理。 |
Abstract Syntax Tree (AST) | 抽象语法树(AST)是根据lexer生成的令牌流构建的。它几乎完全代表了用户写的内容。有助于做一些语法完整性检查(例如,检查用户编写的类型是否符合预期)。 |
High-level IR (HIR) | 这是一种不加糖的AST。它在语法上仍然接近于用户编写的内容,但它包含了一些隐含的内容,比如一些被省略的生命周期等。 |
Typed HIR (THIR) | 这是一种介于HIR和MIR之间的中间体。它类似于HIR,但是完全类型化的。 |
Middle-level IR (MIR) | 这个IR基本上是一个控制流图(CFG)。CFG是一种图表,它显示了程序的基本块,以及控制流如何在它们之间流动。同样地,MIR也有一堆基本块,里面有简单的类型化语句(例如赋值,简单的计算等)和控制流边缘到其他基本块(例如,调用,删除值)。MIR用于借用检查和其他重要的基于数据流的检查,例如检查未初始化的值。它还用于一系列优化和持续评估(通过MIRI)。因为MIR仍然是通用的 |
LLVM IR | 这是所有输入到LLVM编译器的标准形式。LLVM IR是一种具有大量注释的类型化汇编语言。它是所有使用LLVM的编译器使用的标准格式(例如,Clang编译器也输出LLVM IR)。LLVM IR被设计成其他编译器可以很容易地发出,同时也足够丰富,可以让LLVM在它上面运行大量编译优化。 |
Rust的编译过程会将源码转为中间表示,并执行检查或优化,最终通过LLVM生成可执行文件,Clippy的lint检查在中间表示AST和HIR上执行:
2.2 lint插件机制
在编译器Rustc中lint被保存在LintStore这个结构中。分为early_lint和late_lint,early_lint在中间表示AST上做检查,late_lint在中间表示HIR上做检查。
lint注册和执行的驱动方式如下图:
在crate:rust_interface中调用crate:rustc_lint的函数注册和驱动执行。如调用函数pub fn register_lints(&mut self, lints: &[&'static Lint])注册lints到LintStore中,只有注册到这个结构体中的lint才能被执行。
early_lint_methods是检查函数集合, early_lint实现其中的函数,在AST上检查某个问题,如fn check_expr(a: &ast::Expr)表示在代码的AST中间表示的每个表达式(Expr)上进行检测,检测的具体内容和告警内容由lint的开发者实现在函数中:
late_lint_methods是检查函数集合,late_lint实现这些函数,在中间表示HIR上检查某方面的问题,如fn check_expr(a: &$hir hir::Expr<$hir>)表示在代码HIR表示的每个表达式(Expr)上进行检测,检测的具体内容和告警内容由lint的开发者实现在函数中:
2.3 Clippy 的lint
如下例子是Clippy中一个非常简单的early_passes,叫做AS_CONVERSIONS,他检查类型转化 as 的使用,建议用户替换为更好的转换函数 try_into():
具体实现:
这个例子的含义是在每个表达式(Expr)上扫描,忽略在外部crate的宏中的表达式,检测形如a as u16
的转换表达式,“as”转换将执行多种类型的转换,包括“无声”的有损转换和危险的强制转换,建议替换为具有更清晰语义的 a.try_into()?
。 在某些情况下,使用' as '是有意义的,因此这个lint默认为Allow。
三、动手实现一个Clippy lint
了解了clippy的基本原理后,本文通过late_lint:needless_splitn来介绍如何实现一个lint,该lint检测可以用str::split(str::rsplit)替换 str::splitn(str::rsplitn)的场景,由作者在21年贡献:
1. 首先声明这个lint:
2. 这个lint的入口在late_lint:Methods中,Methods实现了check_expr函数,检查每个函数调用表达式,跟needless_splitn有关的是我们在图中红框标注的函数调用check_methods,将函数调用表达式的入口收敛到一个lint中的好处是一遍扫描就能处理想检查的所有函数调用:
3. check_methods函数定义如下图,他会检测当前正在处理的表达式是否是一个函数调用语句,然后根据函数名到对应的lint做进一步检查:
4. 在check_methods函数中我们重点关注处理"splitn" 或 "rsplitn"函数的地方,如下图所示:
5. 下面是late_lint:needless_splitn的具体实现,请参考注释:
pub(super) fn check_needless_splitn(
cx: &LateContext<'_>,
method_name: &str,
expr: &Expr<'_>,
self_arg: &Expr<'_>,
pat_arg: &Expr<'_>,
count: u128,
) {
// 检查是否对字符串调用的splitn函数
if !cx.typeck_results().expr_ty_adjusted(self_arg).peel_refs().is_str() {
return;
}
let ctxt = expr.span.ctxt();
let mut app = Applicability::MachineApplicable;
// 根据结果具体是splitn 还是 rsplitn设置反馈给用户的warning信息
let (reverse, message) = if method_name == "splitn" {
(false, "unnecessary use of `splitn`")
} else {
(true, "unnecessary use of `rsplitn`")
};
if_chain! {
if count >= 2;
if check_iter(cx, ctxt, cx.tcx.hir().parent_iter(expr.hir_id), count);
then {
span_lint_and_sugg(
cx,
NEEDLESS_SPLITN,
expr.span,
message,
"try this",
format!(
"{}.{}({})",
snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0,
if reverse {"rsplit"} else {"split"},
snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0
),
app,
);
}
}
}
// splitn和rsplitn是返回拆分字符串的迭代器
// 检测splitn是否可以被替换,依赖后面具体取值的函数:next() next_tuple() nth()
// 因此需要顺着Expr表达式往后查看具体的取值函数,根据情况进行判断
fn check_iter(
cx: &LateContext<'tcx>,
ctxt: SyntaxContext,
mut iter: impl Iterator<Item = (HirId, Node<'tcx>)>,
count: u128,
) -> bool {
match iter.next() {
Some((_, Node::Expr(e))) if e.span.ctxt() == ctxt => {
let (name, args) = if let ExprKind::MethodCall(name, _, [_, args @ ..], _) = e.kind {
(name, args)
} else {
return false;
};
if_chain! {
if let Some(did) = cx.typeck_results().type_dependent_def_id(e.hir_id);
if let Some(iter_id) = cx.tcx.get_diagnostic_item(sym::Iterator);
then {
match (&*name.ident.as_str(), args) {
("next", []) if cx.tcx.trait_of_item(did) == Some(iter_id) => {
return true;
},
("next_tuple", []) if count > 2 => {
return true;
},
("nth", [idx_expr]) if cx.tcx.trait_of_item(did) == Some(iter_id) => {
if let Some((Constant::Int(idx), _)) = constant(cx, cx.typeck_results(), idx_expr) {
if count > idx + 1 {
return true;
}
}
},
_ => return false,
}
}
}
},
_ => return false,
};
false
}
6. 注册lint
7. 编写测试用例tests/ui/needless_splitn.rs
8. 执行测试用例
# 代码格式化
cargo dev fmt
# 运行命令进行测试
TESTNAME=needless_splitn cargo uitest
9. 生成tests/ui/needless_splitn.stderr和tests/ui/needless_splitn.fixed文件:
cargo dev bless