处理宏和展开

有时我们在使用 Clippy 时可能会遇到 Rust 宏展开。虽然宏展开不如宇宙的膨胀那样戏剧性和深刻,但它们肯定会给代码和逻辑的有序世界带来混乱。

一般的经验法则是,我们在使用 Clippy 时应该忽略包含宏展开的代码,因为代码的行为可能是动态的,我们难以或不可能预见。

误报

难以预见的行为动态到底是什么意思?

宏是在 EarlyLintPass 级别展开的,因此会生成抽象语法树(AST)来代替宏。这意味着我们在 Clippy 中使用的代码已经是展开后的代码。

如果我们编写了一个新的 lint,那么该 lint 有可能会在宏生成的代码中触发。由于这个展开后的宏代码不是宏的用户编写的,而是由宏的作者编写的,因此用户不应该也不必负责修复触发 lint 的问题。

此外,宏中的 Span 可以由宏的作者更改。因此,任何与行或列相关的 lint 检查都应该避免,因为它们可能会随时更改并成为不可靠或不正确的信息。

由于这些不可预见或不稳定的行为,宏展开通常不应被视为稳定 API 的一部分。这也是为什么大多数 lint 在向最终用户发出建议之前,都会检查它们是否在宏内部,以避免误报。

如何使用宏

有几个函数可用于处理宏。

Span.from_expansion 方法

我们可以利用 spanfrom_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 这样的简单表达式中,ab 具有相同的上下文。但是,在 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 可以是 EarlyContextLateContext)

#![allow(unused)]
fn main() {
if in_external_macro(cx.sess(), foo_span) {
    // We should ignore macro from a foreign crate.
    return;
}
}