如何使用引用宏报告程序宏中的错误?

How to report errors in a procedural macro using the quote macro?

我正在编写一个工作正常的程序宏,但我无法以符合人体工程学的方式报告错误。使用 panic! "works" 但不够优雅,无法很好地向用户显示错误消息。

我知道我可以在解析 TokenStream 时报告良好的错误,但我需要在解析后遍历 AST 时产生错误。

宏调用如下所示:

attr_test! {
    #[bool]
    FOO
}

并且应该输出:

const FOO: bool = false;

这是宏代码:

extern crate proc_macro;
use quote::quote;
use syn::parse::{Parse, ParseStream, Result};
use syn::{Attribute, parse_macro_input, Ident, Meta};

struct AttrTest {
    attributes: Vec<Attribute>,
    name: Ident,
}

impl Parse for AttrTest {
    fn parse(input: ParseStream) -> Result<Self> {
        Ok(AttrTest {
            attributes: input.call(Attribute::parse_outer)?,
            name: input.parse()?,
        })
    }
}

#[proc_macro]
pub fn attr_test(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let test: AttrTest = parse_macro_input!(tokens);
    let name = test.name;
    let first_att = test.attributes
        .get(0)
        .and_then(|att| att.parse_meta().ok());
    if let Some(Meta::Word(ty)) = first_att {
        if ty.to_string() != "bool" {
            panic!("expected bool");
        }
        let output = quote! {
            const #name: #ty = false;
        };
        output.into()
    } else {
        panic!("malformed or missing metadata")
    }
}

如果在属性中指定了 bool 以外的任何内容,我想产生一个错误。比如这样输入:

attr_test! {
    #[something_else]
    FOO
}

结果应该是这样的:

error: expected bool
attr_test! {
    #[something_else]
      ^^^^^^^^^^^^^^ expected bool
    FOO
}

在解析过程中,有一个 Result,它有很多有用的信息,包括 span,因此产生的错误可以突出显示宏调用中有问题的确切部分。但是一旦遍历AST,就找不到好的报错方式了。

应该怎么做?

除了 panicing,目前有两种方法可以报告来自 proc-macro 的错误:the unstable Diagnostic API 和“compile_error! 技巧”。目前,后者主要被使用,因为它可以稳定运行。让我们看看它们是如何工作的。

compile_error! 技巧

自 Rust 1.20 以来,the compile_error! macro exists in the standard library。它接受一个字符串并在编译时导致错误。

compile_error!("oopsie woopsie");

这导致 (Playground):

error: oopsie woopsie
 --> src/lib.rs:1:1
  |
1 | compile_error!("oopsie woopsie");
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

为两种情况添加了此宏:macro_rules! 宏和 #[cfg]。在这两种情况下,如果用户不正确地使用宏或具有错误的 cfg 值,库作者可以添加更好的错误。

但是 proc-macro 程序员有一个有趣的想法。正如您可能知道的那样,您可以根据自己的喜好创建程序宏中的 TokenStream 和 return。这包括这些标记的跨度:您可以将任何您喜欢的跨度附加到输出标记。所以主要思想是这样的:

发出包含 compile_error!("your error message"); 的令牌流,但将这些令牌的跨度设置为导致错误的输入令牌的跨度。 [=] 中甚至还有一个宏 quote 这使得这更容易:quote_spanned!。对于您的情况,我们可以这样写:

let output = if ty.to_string() != "bool" {
    quote_spanned! {
        ty.span() =>
        compile_error!("expected bool");
    }
} else {
    quote! {
        const #name: #ty = false;
    }
};

对于您的错误输入,编译器现在打印如下:

error: expected bool
 --> examples/main.rs:4:7
  |
4 |     #[something_else]
  |       ^^^^^^^^^^^^^^

为什么这行得通?那么:compile_error! 的错误显示了包含 compile_error! 调用的代码片段。为此,使用了 compile_error! 调用的跨度。但是由于我们将跨度设置为指向错误的输入标记 ty,编译器会显示该标记下划线的代码段。

这个技巧也被syn用来打印漂亮的错误。事实上,如果你无论如何都在使用 syn,你可以使用它的 Error 类型,特别是 Error::to_compile_error method ,它 return 正是我们用 [= 手动创建的令牌流26=]:

syn::Error::new(ty.span(), "expected bool").to_compile_error()

Diagnostic API

因为这仍然不稳定,所以只是一个简短的例子。诊断 API 比上面的技巧更强大,因为您可以有多个跨度、警告和注释。

Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();

在那行之后,错误被打印出来,但你仍然可以在你的 proc-macro 中做一些事情。通常,您只需要 return 一个空令牌流。

接受的答案提到了不稳定的 Diagnostic API,它比常规的 compile_error 给你更多的权力和控制。在 Diagnostic API 稳定之前,which probably will not be any time soon, you can use the proc_macro_error crate. It provides a Diagnostic 类型被设计为 API 与不稳定的 proc_macro::Diagnostic 兼容。整个API没有实现,只有stable上可以合理实现的部分。您可以通过简单地将提供的注释添加到您的宏来使用它:

#[proc_macro_error]
#[proc_macro]
fn my_macro(input: TokenStream) -> TokenStream {
    // ...
    Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();
}

proc_macro_error 还提供了一些有用的宏来发出错误:

abort! { input,
    "I don't like this part!";
        note = "A notice message...";
        help = "A help message...";
}

但是,您可能要考虑坚持使用 Diagnostic 类型,因为当它稳定后,它会更容易迁移到官方 Diagnostic API。