- 工夫:2022.8.11
- 撰稿:张正 @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 最次要的构造有两个,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: &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),*) {})*
)
}
这样的设计益处有以下几点:
- 因为 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: https://github.com/KusionStac…
- Rustc: https://github.com/rust-lang/…
- rustc-dev-guide: https://rustc-dev-guide.rust-…
- Rust Visitor: https://doc.rust-lang.org/nig…
- Rust Clippy: https://github.com/rust-lang/…