原來 Rust 當然 Lint 是這樣工作的
Rustc 原始碼學習 - Lint 與 LintPass
- 時間:2022.8.15
- 撰稿: 張正 @KusionStack開發組
- 轉載請註明原文連結: http://mp.weixin.qq.com/s/kmXNRgDZxkOgtxZtAyDSOA
背景
在 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 最主要的結構有兩個, Lint
和 LintPass
。首先需要區分 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
的定義做唯一性的檢查。而 LintPass
是 Lint
的具體實現,是在檢查時呼叫的 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,但 Lint
和 LintPass
之間並不是 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
分為了更為具體的兩類: EarlyLintPass
和 LateLintPass
。其主要區別在於檢查的元素是否帶有型別資訊,即在型別檢查之前還是之後執行。例如, 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),*) {})* ) }
這樣的設計好處有以下幾點:
- 因為 LintPass 是一個 trait,每一個 LintPass 的定義都需要實現其內部定義的所有方法。但 early lint 和 late lint 發生在編譯的不同階段,函式入參也不一致(AST 和 HIR)。因此,LintPass 的定義只包含了
fn name()
和fn get_lints()
這兩個通用的方法。而執行檢查函式則定義在了更為具體的EarlyLintPass
和LateLintPass
中。 - 同樣的,對於
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 中,除了 Lint
和 LintPass
外,還有一些 *Pass
的命名,如 Mir
和 MirPass
、 rustc_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 的設計和實踐,期待繼續關注。
參考連結
- KusionStack: http://github.com/KusionStack
- Rustc: http://github.com/rust-lang/rust
- rustc-dev-guide: http://rustc-dev-guide.rust-lang.org/
- Rust Visitor: http://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast/visit/index.html
- Rust Clippy: http://github.com/rust-lang/rust-clippy
- 爆肝整理5000字!HTAP的關鍵技術有哪些?| StoneDB學術分享會#3
- Java併發程式設計解析 | 基於JDK原始碼解析Java領域中ReentrantLock鎖的設計思想與實現原理 (一)
- 【程式碼級】全鏈路壓測的整體架構設計,以及5種實現方案(流量染色、資料隔離、介面隔離、零侵入、服務監...
- 電商行業:全鏈路監測廣告投放效果,用資料驅動業務增長
- 如何給玩偶建模並讓它跳個舞?
- 原來 Rust 當然 Lint 是這樣工作的
- 基於 Zadig 的 GitOps 實踐
- What's new in dubbo-go-pixiu 0.5.1
- 負載均衡原理分析與原始碼解讀
- 利用 SonarScanner 靜態掃描 Rainbond 上的 Maven 專案
- 分散式鏈路追蹤Jaeger 微服務Pig在Rainbond上的實踐分享
- 收藏!0基礎開源資料視覺化平臺FlyFish大屏開發指南
- 從開源的視角,解析SAP經典ERP “三十年不用變”的架構設計 薦 轉
- 從碼農轉型大音樂家,你需要這些音樂製作處理工具
- 五分鐘給你的 gRPC 服務加上 HTTP 介面
- 【詳細教程】一文參透MongoDB聚合查詢 原 薦
- 【超詳細】手把手教你搭建MongoDB叢集 原 薦
- 從伺服器到雲託管,到底經歷了什麼?
- 敏捷需求管理篇|如何從0-1寫好一個使用者故事
- go-zero微服務實戰系列(四、CRUD熱熱身)