Rust源码学习 - Lint 与 LintPass

简介: - 时间:2022.8.19- 作者:[黑怕](https://github.com/He1pa)@KusionStack开发组---## 背景KusionStack是我们团队对云原生开放协同技术探索与实践
  • 时间:2022.8.19
  • 作者:黑怕@KusionStack开发组

背景

KusionStack 是我们团队对云原生开放协同技术探索与实践。在 KusionStack 中, KCL 配置策略语言是重要的组成部分之一。为了帮助用户更好的编写 KCL 代码,我们也为 KCL 语言开发了一些语言工具,Lint 就是其中一种。Lint 工具帮助用户检查代码中潜在的问题和错误,同时也可以用于自动化的代码检查,保障仓库代码规范和质量。因为 KCL 语言由 Rust 实现,一些功能也学习和参考了 Rustc。本文是在学习 Rustc 过程中的一些思考和沉淀,在这里做一些分享。

Rustc

Rustc 是 Rust Compiler 的简称,即 Rust 编程语言的编译器。Rust 的编译器是自举的,即 Rustc 由 Rust 语言编写而成,可以通过旧版本编译出新版本。因此,Rustc 可以说是用 Rust 语言编写编译器的最佳实践。

Lint 工具

Lint 是代码静态分析工具的一种,最早是来源于 C 语言。Lint 工具通常会检查代码中潜在的问题和错误,包括(但不限于)编程风格(缩进、空行、空格)、代码质量(定义未使用的变量、文档缺失)以及错误代码(除0错误、重复定义、循环引用)等问题。通常来说,Lint 工具除了标识错误外,还会带有一定的 fix/refactor suggest 和 auto-fix 的能力。在工程中引入 Lint 工具可以有效的减少错误,提高整体的工程质量。此外,对一种编程语言来说,Lint 工具通常也是其他工具研发的前置条件,例如 IDE 插件的错误提示,CI 的 Pipeline 检测等。

Lint vs. LintPass

概念与关系

Rustc 中关于 Lint 最主要的结构有两个, LintLintPass。首先需要区分 Lint 和 LintPass 的概念。Rustc 的很多文档中都将它们统称为 Lint,这很容易造成混淆。关于这两者之间的区别,rustc-dev-guide 给出的解释是:

Lint declarations don't carry any "state" - they are merely global identifiers and descriptions of lints. We assert at runtime that they are not registered twice (by lint name).
Lint passes are the meat of any lint.

从定义方面, Lint 是对所定义的 lint 检查的静态描述,例如 name, level, description, code 等属性,与检查时的状态无关,Rustc 用 Lint 的定义做唯一性的检查。而 LintPassLint 的具体实现,是在检查时调用的 check_* 方法。
在具体的代码实现方法, Lint定义为一个 Struct,所有 lint 的定义都是此类型的一个实例/对象。而 LintPass 则对应为一个 trait。trait 类似于 java/c++ 中的接口,每一个 lintpass 的定义都需要实现该接口中定义的方法。

/// Specification of a single lint.
#[derive(Copy, Clone, Debug)]
pub struct Lint {
    pub name: &'static str,
    /// Default level for the lint.
    pub default_level: Level,
    /// Description of the lint or the issue it detects.
    ///
    /// e.g., "imports that are never used"
    pub desc: &'static str,
    ...
}

pub trait LintPass {
    fn name(&self) -> &'static str;
}

需要注意的是,尽管刚刚的描述中说到trait 类似于接口而 Lint 是一个 struct,但 LintLintPass 之间并不是 OO 中一个“类”和它的“方法”的关系。而是在声明 LintPass 会生成一个实现了该 trait 的同名的 struct,该 struct 中的 get_lints() 方法会生成对应的 Lint 定义。

这与 rustc-dev-guide 的描述也保持了一致:

A lint might not have any lint pass that emits it, it could have many, or just one -- the compiler doesn't track whether a pass is in any way associated with a particular lint, and frequently lints are emitted as part of other work (e.g., type checking, etc.).

Lint 与 LintPass 的宏定义

Rustc 为 Lint 和 LintPass 都提供了用于定义其结构的宏。
定义 Lint 的宏declare_lint 比较简单,可以在rustc_lint_defs::lib.rs中找到。declare_lint 宏解析输入参数,并生成名称为 $NAME 的 Lint struct。

#[macro_export]
macro_rules! declare_lint {
    ($(#[$attr:meta])* $vis: vis $NAME: ident, $Level: ident, $desc: expr) => (
        $crate::declare_lint!(
            $(#[$attr])* $vis $NAME, $Level, $desc,
        );
    );
    ($(#[$attr:meta])* $vis: vis $NAME: ident, $Level: ident, $desc: expr,
     $(@feature_gate = $gate:expr;)?
     $(@future_incompatible = FutureIncompatibleInfo { $($field:ident : $val:expr),* $(,)*  }; )?
     $($v:ident),*) => (
        $(#[$attr])*
        $vis static $NAME: &$crate::Lint = &$crate::Lint {
            name: stringify!($NAME),
            default_level: $crate::$Level,
            desc: $desc,
            edition_lint_opts: None,
            is_plugin: false,
            $($v: true,)*
            $(feature_gate: Some($gate),)*
            $(future_incompatible: Some($crate::FutureIncompatibleInfo {
                $($field: $val,)*
                ..$crate::FutureIncompatibleInfo::default_fields_for_macro()
            }),)*
            ..$crate::Lint::default_fields_for_macro()
        };
    );
    ($(#[$attr:meta])* $vis: vis $NAME: ident, $Level: ident, $desc: expr,
     $lint_edition: expr => $edition_level: ident
    ) => (
        $(#[$attr])*
        $vis static $NAME: &$crate::Lint = &$crate::Lint {
            name: stringify!($NAME),
            default_level: $crate::$Level,
            desc: $desc,
            edition_lint_opts: Some(($lint_edition, $crate::Level::$edition_level)),
            report_in_external_macro: false,
            is_plugin: false,
        };
    );
}

LintPass 的定义涉及到两个宏:

  • declare_lint_pass:生成一个名为$name 的 struct,并且调用 impl_lint_pass 宏。
macro_rules! declare_lint_pass {
    ($(#[$m:meta])* $name:ident => [$($lint:expr),* $(,)?]) => {
        $(#[$m])* #[derive(Copy, Clone)] pub struct $name;
        $crate::impl_lint_pass!($name => [$($lint),*]);
    };
}
  • impl_lint_pass:为生成的 LintPass 结构实现fn name()fn get_lints() 方法。
macro_rules! impl_lint_pass {
    ($ty:ty => [$($lint:expr),* $(,)?]) => {
        impl $crate::LintPass for $ty {
            fn name(&self) -> &'static str { stringify!($ty) }
        }
        impl $ty {
            pub fn get_lints() -> $crate::LintArray { $crate::lint_array!($($lint),*) }
        }
    };
}

EarlyLintPass 与 LateLintPass

前面关于 LintPass 的宏之中,只定义了fn name()fn get_lints() 方法,但并没有定义用于检查的 check_* 函数。这是因为 Rustc 中将 LintPass 分为了更为具体的两类:EarlyLintPassLateLintPass。其主要区别在于检查的元素是否带有类型信息,即在类型检查之前还是之后执行。例如, WhileTrue 检查代码中的 while true{...} 并提示用户使用 loop{...} 去代替。这项检查不需要任何的类型信息,因此被定义为一个  EarlyLint(代码中 impl EarlyLintPass for WhileTrue

declare_lint! {
    WHILE_TRUE,
    Warn,
    "suggest using `loop { }` instead of `while true { }`"
}

declare_lint_pass!(WhileTrue => [WHILE_TRUE]);

impl EarlyLintPass for WhileTrue {
    fn check_expr(&mut self, cx: &EarlyContext<'_>, e: &ast::Expr) {
        ...
    }
}

Rustc 中用了3个宏去定义 EarlyLintPass

  • early_lint_methods:early_lint_methods 中定义了 EarlyLintPass 中需要实现的 check_*函数,并且将这些函数以及接收的参数 $args传递给下一个宏。
macro_rules! early_lint_methods {
    ($macro:path, $args:tt) => (
        $macro!($args, [
            fn check_param(a: &ast::Param);
            fn check_ident(a: &ast::Ident);
            fn check_crate(a: &ast::Crate);
            fn check_crate_post(a: &ast::Crate);
            ...
        ]);
    )
}
  • declare_early_lint_pass:生成trait EarlyLintPass 并调用宏 expand_early_lint_pass_methods
macro_rules! declare_early_lint_pass {
    ([], [$($methods:tt)*]) => (
        pub trait EarlyLintPass: LintPass {
            expand_early_lint_pass_methods!(&EarlyContext<'_>, [$($methods)*]);
        }
    )
}
  • expand_early_lint_pass_methods:为check_*方法提供默认实现,即空检查。
macro_rules! expand_early_lint_pass_methods {
    ($context:ty, [$($(#[$attr:meta])* fn $name:ident($($param:ident: $arg:ty),*);)*]) => (
        $(#[inline(always)] fn $name(&mut self, _: $context, $(_: $arg),*) {})*
    )
}

这样的设计好处有以下几点:

  1. 因为 LintPass 是一个 trait,每一个 LintPass 的定义都需要实现其内部定义的所有方法。但 early lint 和 late lint 发生在编译的不同阶段,函数入参也不一致(AST 和 HIR)。因此,LintPass 的定义只包含了 fn name()fn get_lints() 这两个通用的方法。而执行检查函数则定义在了更为具体的 EarlyLintPassLateLintPass 中。
  2. 同样的,对于 EarlyLintPass, 每一个 lintpass 的定义都必须实现其中的所有方法。但并非每一个 lintpass 都需要检查 AST 的所有节点。 expand_early_lint_pass_methods 为其内部方法提供了默认实现。这样在定义具体的 lintpass 时,只需要关注和实现其相关的检查函数即可。例如,对于 WhileTrue 的定义,因为 while true { }这样的写法只会出现在 ast::Expr 节点中,因此只需要实现 check_expr 函数即可。在其他任何节点调用 WhileTrue 的检查函数,如在检查 AST 上的标识符节点时,调用 WhileTrue.check_ident(),则根据宏 expand_early_lint_pass_methods 中的定义执行一个空函数。

pass 的含义

在 Rustc 中,除了 LintLintPass 外,还有一些 *Pass 的命名,如 MirMirPassrustc_passes 包等。编译原理龙书中对Pass有对应的解释:

1.2.8 将多个步骤组合成趟
前面关于步骤的讨论讲的是一个编译器的逻辑组织方式。在一个特定的实现中,多个步骤的活动可以被组合成一趟(pass)。每趟读入一个输入文件并产生一个输出文件。

在声明 LintPass 的宏 declare_lint_pass 中,其第二个参数为一个列表,表示一个 lintpass 可以生成多个 lint。Rustc 中还有一些 CombinedLintPass 中也是将所有 builtin 的 lint 汇总到一个 lintpass 中。这与龙书中“趟”的定义基本一致:LintPass 可以组合多个 Lint 的检查,每个 LintPass 读取一个 AST 并产生对应的结果。

Lint 的简单实现

在 LintPass 的定义中,给每一个 lintpass 的所有 check_* 方法都提供了一个默认实现。到这里为止,基本上已经可以实现 Lint 检查的功能。

struct Linter { }
impl ast_visit::Visitor for Linter {
    fn visit_crate(a: ast:crate){
        for lintpass in lintpasses{
            lintpass.check_crate(a)
        }
        walk_crate();
    }
    fn visit_stmt(a: ast:stmt){
        for lintpass in lintpasses{
            lintpass.check_stmt(a)
        }
        walk_stmt();
    }
    ...
}

let linter = Linter::new();

for c in crates{
    linter.visit_crate(c);
}

Visitor 是遍历 AST 的工具,在这里为 Linter 实现其中的 visit_* 方法,在遍历时调用所有 lintpass 的 check_* 函数。walk_* 会继续调用其他的 visit_* 函数,遍历其中的子节点。因此,对于每一个 crate, 只需要调用 visit_crate() 函数就可以遍历 AST 并完成检查。

总结

本文简单介绍了 Rustc 源码中关于 Lint 的几个重要结构。并以 WhileTrue 为例说明了 Rustc 如何中定义和实现一个 Lint,最后基于这些结构,提供了一个简易的 Lint 检查的实现方式。希望能够对理解 Rustc 及 Lint 有所帮助,如有错误,欢迎指正。KCL 的 Lint 工具也参考了其中部分设计, 由文末简易的 Linter 结构改进而成。将在后续的文章将继续介绍 Rustc 中 Lint 在编译过程中的注册和执行过程,如何继续优化上述 Linter 的实现,以及 KCL Lint 的设计和实践,期待继续关注。

Ref

相关文章
|
19天前
|
存储 Rust 网络协议
【Rust学习】10_定义枚举
在这一章我们学习 枚举(enumerations),也被称作 enums。枚举允许你通过列举可能的 成员(variants) 来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做 Option,它代表一个值要么是某个值要么什么都不是。然后会讲到在 match 表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后,我们将学习 if let 结构,另一个简洁方便处理代码中枚举的结构。
31 7
|
2月前
|
Rust 算法 安全
学习Rust
【10月更文挑战第13天】学习Rust
53 8
|
2月前
|
Rust 安全 算法
Rust的学习
【10月更文挑战第12天】Rust的学习
28 2
|
2月前
|
Rust 算法 安全
如何学习Rust编程?
【10月更文挑战第12天】如何学习Rust编程?
44 1
|
2月前
|
Rust API
【Rust学习】09_方法语法
结构体让你可以创建出在你的领域中有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名它们,这样可以使得代码更加清晰。在 impl 块中,你可以定义与你的类型相关联的函数,而方法是一种相关联的函数,允许您指定结构体的实例具有的行为。 但是结构体并不是创建自定义类型的唯一方式:让我们转向 Rust 的 enum 功能,将另一个工具添加到你的工具箱中。
18 0
|
3月前
|
Rust 索引
【Rust学习】08_使用结构体代码示例
为了了解我们何时可能想要使用结构体,让我们编写一个计算长方形面积的程序。我们将从使用单个变量开始,然后重构程序,直到我们改用结构体。
92 2
|
3月前
|
存储 Rust 编译器
【Rust学习】07_结构体说明
**struct**或 ***structure***是一种自定义数据类型,允许您命名和包装多个相关的值,从而形成一个有意义的组合。如果您熟悉面向对象的语言,那么**struct**就像对象中的数据属性。在本章中,我们将比较和对比元组与结构体,在您已经知道的基础上,来演示结构体是对数据进行分组的更好方法。
27 1
|
4月前
|
存储 Rust 安全
【Rust学习】06_切片
所有权、借用和切片的概念确保了 Rust 程序在编译时的内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。
26 1
|
3月前
|
Rust Linux Go
Rust/Go语言学习
Rust/Go语言学习
|
5月前
|
存储 Rust 安全
【Rust学习】04_所有权
所有权是 Rust 最独特的特性,对语言的其余部分有着深远的影响。它使 Rust 能够在不需要垃圾收集器的情况下保证内存安全,因此了解所有权的运作方式非常重要。在本章中,我们将讨论所有权以及几个相关功能:借用、切片以及 Rust 如何在内存中布局数据。
29 1