作者:华为可信软件工程和开源2012实验室
Clippy 是什么
Clippy 是 Rust 官方提供的 代码检查 lint 工具,通过静态分析,来检查代码中有问题或不符合指定规范的代码。
安装
rustup component add clippy
使用
cargo clippy
配置
可以在项目中添加 clippy.toml 或 .clippy.toml 来指定使用的 Lints 。
类似于:
avoid-breaking-exported-api = false
blacklisted-names = ["toto", "tata", "titi"]
cognitive-complexity-threshold = 30
Cargo Clippy 中目前包含超过 450 个 Lint 。
Rust 编译器内置 Lint 介绍
在 Rust 编译器 中 lint 包含四种级别:
- allow ,编译器
- warn
- deny
- forbid
每个 lint 都有一个 默认级别。下面是一个分类:
- 默认允许的 Lints : 默认情况下,编译器允许的 Lints 。
- 默认警告Lints : 默认情况下,编译器会警告的 LInts 。
- 默认拒绝的 Lints : 默认情况下,编译器会拒绝的lints。
编译器内置 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 (荒谬的极值比较)
检查关系中的一方是其类型的最小值或最大值的比较,如果涉及到永远是真或永远是假的情况,则发出警告。只有整数和布尔类型被检查。
代码示例:
let vec: Vec<isize> = Vec::new();
if vec.len() <= 0 {}
if 100 > i32::MAX {} // 这里会报错:Deny ,因为 100 不可能大于 i32::MAX
代码风格
Lint 示例: assertions_on_constants (对常量的断言)
用于检查 assert!(true) 和 assert!(false) 的情况。
代码示例:
assert!(false)
assert!(true)
const B: bool = false;
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)。
代码示例:
// bad
let _ = opt().and_then(|s| Some(s.len()));
let _ = res().and_then(|s| if s.len() == 42 { Ok(10) } else { Ok(20) });
let _ = res().or_else(|s| if s.len() == 42 { Err(10) } else { Err(20) });
// good
let _ = opt().map(|s| s.len());
let _ = res().map(|s| if s.len() == 42 { 10 } else { 20 });
let _ = res().map_err(|s| if s.len() == 42 { 10 } else { 20 });
代码不灵动
Lints 示例: cast_lossless
用于检查可以被安全转换(conversion)函数替代的数字类型之间的转换( cast )。
as强制转换与From转换从根本上不同。 From转换是“简单和安全”,而as强制转换纯粹是“安全”。在考虑数字类型时,仅在保证输出相同的情况下才存在From转换,即,不会丢失任何信息(不会出现截断或下限或精度下降)。 as强制转换没有此限制。
代码示例:
// bad
fn as_u64(x: u8) -> u64 {
x as u64
}
// good
fn as_u64(x: u8) -> u64 {
u64::from(x) // from内部其实也是as,但只要是实现 from 的,都是无损转换,在代码可读性、语义上更好
}
代码性能
Lints 示例: append_instead_of_extend
检查动态数组中是否出现
extend,建议使用append代替。
代码示例:
let mut a = vec![1, 2, 3];
let mut b = vec![4, 5, 6];
// Bad
a.extend(b.drain(..));
// Good
a.append(&mut b); // 用 append 代替 extend 更加高效和简洁。
未完待续,关注专栏看下篇
还有一些其他分类,比如包括一些「约束性(Restriction)」建议、对 cargo.toml 的检查、以及正在开发中的Lints 等。
如何定制 Clippy Lint
定制 Clippy Lint 有两种办法:
- 方法一:fork rust-clippy 项目,自己维护。因为使用了不稳定的接口,所以维护和使用不太方便。
- 方法二:使用第三方 Dylint 工具。维护自定义 lint 比方法一更方便。
方法一:fork clippy
在 fork Clippy 定制自己的 LInt 之前,还需要了解 Clippy 的 工作机制。
Clippy 工作机制
Clippy 通过 rust_driver 和 rustc_interface 这两个库,可以把 rustc 作为库来调用。
rustc_driver 本质上就像是整个rustc 编译器的main函数(入口)。它使用在rustc_interface crate中定义的接口以正确的顺序运行编译器。
rustc_interface crate为外部用户提供了一个(未稳定的)API,用于在编译过程中的特定时间运行代码,允许第三方(例如RLS或rustdoc)有效地使用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>>。
LintStore 是 rustc_lint 组件库中定义的类型。
pub struct LintStore {
/// Registered lints.
lints: Vec<&'static Lint>,
// 构造不同种类的 lint pass
/// Constructor functions for each variety of lint pass.
///
/// These should only be called once, but since we want to avoid locks or
/// interior mutability, we don't enforce this (and lints should, in theory,
/// be compatible with being constructed more than once, though not
/// necessarily in a sane manner. This is safe though.)
pub pre_expansion_passes: Vec<Box<dyn Fn() -> EarlyLintPassObject + sync::Send + sync::Sync>>,
pub early_passes: Vec<Box<dyn Fn() -> EarlyLintPassObject + sync::Send + sync::Sync>>,
pub late_passes: Vec<Box<dyn Fn() -> LateLintPassObject + sync::Send + sync::Sync>>,
/// This is unique in that we construct them per-module, so not once.
pub late_module_passes: Vec<Box<dyn Fn() -> LateLintPassObject + sync::Send + sync::Sync>>,
/// Lints indexed by name.
by_name: FxHashMap<String, TargetLint>,
// lint group,通过一个名字触发多个警告,把lint分组
/// Map of registered lint groups to what lints they expand to.
lint_groups: FxHashMap<&'static str, LintGroup>,
}
可以注册的 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函数。
pub fn run_compiler<R: Send>(mut config: Config, f: impl FnOnce(&Compiler) -> R + Send) -> R {
tracing::trace!("run_compiler");
let stderr = config.stderr.take();
util::setup_callbacks_and_run_in_thread_pool_with_globals(
config.opts.edition,
config.opts.debugging_opts.threads,
&stderr,
|| create_compiler_and_run(config, f), // 设置一个回调函数
)
}
// 回调函数
pub fn create_compiler_and_run<R>(config: Config, f: impl FnOnce(&Compiler) -> R) -> R {
let registry = &config.registry;
let (mut sess, codegen_backend) = util::create_session(
config.opts,
config.crate_cfg,
config.diagnostic_output,
config.file_loader,
config.input_path.clone(),
config.lint_caps,
config.make_codegen_backend,
registry.clone(),
);
// 。。。省略
let compiler = Compiler {
sess,
codegen_backend,
input: config.input,
input_path: config.input_path,
output_dir: config.output_dir,
output_file: config.output_file,
register_lints: config.register_lints, // 配置 register_lints
override_queries: config.override_queries,
};
}
再看看 rustc_driver库,其中定义了 Callbacks trait :
pub trait Callbacks {
/// Called before creating the compiler instance
fn config(&mut self, _config: &mut interface::Config) {}
/// Called after parsing. Return value instructs the compiler whether to
/// continue the compilation afterwards (defaults to `Compilation::Continue`)
fn after_parsing<'tcx>(
&mut self,
_compiler: &interface::Compiler,
_queries: &'tcx Queries<'tcx>,
) -> Compilation {
Compilation::Continue
}
/// Called after expansion. Return value instructs the compiler whether to
/// continue the compilation afterwards (defaults to `Compilation::Continue`)
fn after_expansion<'tcx>(
&mut self,
_compiler: &interface::Compiler,
_queries: &'tcx Queries<'tcx>,
) -> Compilation {
Compilation::Continue
}
/// Called after analysis. Return value instructs the compiler whether to
/// continue the compilation afterwards (defaults to `Compilation::Continue`)
fn after_analysis<'tcx>(
&mut self,
_compiler: &interface::Compiler,
_queries: &'tcx Queries<'tcx>,
) -> Compilation {
Compilation::Continue
}
}
该trait中定义了在编译不同阶段要执行的回调函数。
所以,在 Clippy 的 driver.rs 中就做了如下定义:
struct ClippyCallbacks {
clippy_args_var: Option<String>,
}
// 为 ClippyCallbacks 实现 rustc_driver::Callbacks ,定义 config 方法
// 该 config 方法创建编译器实例之前被执行的
impl rustc_driver::Callbacks for ClippyCallbacks {
fn config(&mut self, config: &mut interface::Config) {
let previous = config.register_lints.take();
let clippy_args_var = self.clippy_args_var.take();
config.parse_sess_created = Some(Box::new(move |parse_sess| {
track_clippy_args(parse_sess, &clippy_args_var);
}));
// 注册 lints
config.register_lints = Some(Box::new(move |sess, lint_store| {
// technically we're ~guaranteed that this is none but might as well call anything that
// is there already. Certainly it can't hurt.
if let Some(previous) = &previous {
(previous)(sess, lint_store);
}
let conf = clippy_lints::read_conf(sess);
clippy_lints::register_plugins(lint_store, sess, &conf);
clippy_lints::register_pre_expansion_lints(lint_store);
clippy_lints::register_renamed(lint_store);
}));
// FIXME: #4825; This is required, because Clippy lints that are based on MIR have to be
// run on the unoptimized MIR. On the other hand this results in some false negatives. If
// MIR passes can be enabled / disabled separately, we should figure out, what passes to
// use for Clippy.
config.opts.debugging_opts.mir_opt_level = Some(0);
}
}
所以,Clippy 通过 ClippyCallbacks 的 config 来注册 lints 。在 config 函数内部,通过调用 clippy_lints::read_conf(sess) 来读取 clippy 配置文件里的lint。在 clippy_lints 里还定义了 register_plugins,使用 rustc_lint::LintStore 来注册 clippy 里定义的 lints。
以上就是 Clippy 的工作机制。