处理宏和展开
有时我们在使用 Clippy 时可能会遇到 Rust 宏展开。虽然宏展开不如宇宙的膨胀那样戏剧性和深刻,但它们肯定会给代码和逻辑的有序世界带来混乱。
一般的经验法则是,我们在使用 Clippy 时应该忽略包含宏展开的代码,因为代码的行为可能是动态的,我们难以或不可能预见。
误报
难以预见的行为动态到底是什么意思?
宏是在 EarlyLintPass
级别展开的,因此会生成抽象语法树(AST)来代替宏。这意味着我们在 Clippy 中使用的代码已经是展开后的代码。
如果我们编写了一个新的 lint,那么该 lint 有可能会在宏生成的代码中触发。由于这个展开后的宏代码不是宏的用户编写的,而是由宏的作者编写的,因此用户不应该也不必负责修复触发 lint 的问题。
此外,宏中的 Span 可以由宏的作者更改。因此,任何与行或列相关的 lint 检查都应该避免,因为它们可能会随时更改并成为不可靠或不正确的信息。
由于这些不可预见或不稳定的行为,宏展开通常不应被视为稳定 API 的一部分。这也是为什么大多数 lint 在向最终用户发出建议之前,都会检查它们是否在宏内部,以避免误报。
如何使用宏
有几个函数可用于处理宏。
Span.from_expansion
方法
我们可以利用 span
的 from_expansion
方法,该方法可以检测 span
是否来自宏展开/去语法糖。这是 lint 中非常常见的第一个步骤
#![allow(unused)] fn main() { if expr.span.from_expansion() { // We most likely want to ignore it. return; } }
Span.ctxt
方法
span
的上下文,由方法 ctxt
给出并返回 SyntaxContext,表示 span 是否来自宏展开,如果是,则表示哪个宏调用展开了这个 span。
有时,检查两个 span 的上下文是否相等很有用。例如,假设我们有以下代码行,它将展开为 1 + 0
#![allow(unused)] fn main() { // The following code expands to `1 + 0` for both `EarlyLintPass` and `LateLintPass` 1 + mac!() }
假设我们将 1
表达式收集为变量 left
,将 0
/mac!()
表达式收集为变量 right
,我们可以简单地比较它们的上下文。如果上下文不同,那么我们很可能正在处理宏展开,应该忽略它
#![allow(unused)] fn main() { if left.span.ctxt() != right.span.ctxt() { // The code author most likely cannot modify this expression return; } }
注意:不是来自展开的代码处于“根”上下文中。因此,任何
from_expansion
返回false
的 span 都可以被认为具有相同的上下文。因此,使用span.from_expansion()
通常就足够了。
更深入一点,在像 a == b
这样的简单表达式中,a
和 b
具有相同的上下文。但是,在 macro_rules!
中使用 a == $b
,$b
会展开为一个与 a
具有不同上下文的表达式。
看一下下面的宏 m
#![allow(unused)] fn main() { macro_rules! m { ($a:expr, $b:expr) => { if $a.is_some() { $b; } } } let x: Option<u32> = Some(42); m!(x, x.unwrap()); }
如果展开 m!(x, x.unwrap());
行,我们将得到两个展开的表达式
x.is_some()
(来自m
宏中的$a.is_some()
行)x.unwrap()
(对应于m
宏中的$b
)
假设 x.is_some()
表达式的 span 与 x_is_some_span
变量相关联,而 x.unwrap()
表达式的 span 与 x_unwrap_span
变量相关联,我们可以假设这两个 span 不共享相同的上下文
#![allow(unused)] fn main() { // x.is_some() is from inside the macro // x.unwrap() is from outside the macro assert_ne!(x_is_some_span.ctxt(), x_unwrap_span.ctxt()); }
in_external_macro
函数
rustc_middle::lint
提供了一个函数(in_external_macro
),该函数可以检测给定的 span 是否来自在外部 crate 中定义的宏。
因此,如果我们真的想让一个新的 lint 与宏生成的代码一起工作,这是避免当前 crate 中未定义的宏的下一道防线,因为如果 Clippy lint 用户无法更改的代码,这对用户是不公平的。
例如,假设我们有以下正在由 Clippy 检查的代码
#![allow(unused)] fn main() { #[macro_use] extern crate a_foreign_crate_with_macros; // `foo` macro is defined in `a_foreign_crate_with_macros` foo!("bar"); }
还假设我们获得了 foo
宏调用的相应变量 foo_span
,如果 in_external_macro
的结果为 true
,我们可以决定不进行 lint (请注意,cx
可以是 EarlyContext
或 LateContext
)
#![allow(unused)] fn main() { if in_external_macro(cx.sess(), foo_span) { // We should ignore macro from a foreign crate. return; } }