Rust代码检查工具Clippy原理浅析

一、用途

使用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仍在开发中的新lintallow
clippy::cargo适用于cargo的lintallow

二、原理

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 StreamToken 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

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值