原來 Rust 當然 Lint 是這樣工作的

語言: CN / TW / HK

Rustc 原始碼學習 - Lint 與 LintPass

背景

在 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: *::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: *::Param);
            fn check_ident(a: *::Ident);
            fn check_crate(a: *::Crate);
            fn check_crate_post(a: *::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 的設計和實踐,期待繼續關注。

參考連結