24

我正在编写一个运行良好的程序宏,但我无法以符合人体工程学的方式报告错误。使用panic!“有效”但并不优雅,也不能很好地向用户显示错误消息。

我知道我可以在解析 a 时报告好的错误TokenStream,但是在解析 AST 后我需要在遍历 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
}

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

这应该怎么做?

4

2 回答 2

36

除了恐慌之外,目前有两种方法可以从 proc-macro 报告错误:不稳定的DiagnosticAPI和“compile_error!技巧”。目前,后者主要使用,因为它可以稳定运行。让我们看看它们是如何工作的。

compile_error!诀窍_

从 Rust 1.20 开始,compile_error!存在于标准库中。它需要一个字符串并在编译时导致错误。

compile_error!("oopsie woopsie");

这导致(游乐场):

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

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

但是 proc-macro 程序员有一个有趣的想法。您可能知道,TokenStream您可以根据自己的喜好创建从程序宏返回的内容。这包括这些令牌的跨度:您可以将任何您喜欢的跨度附加到您的输出令牌。所以主要思想是这样的:

发出一个令牌流,其中包含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!使用了调用的跨度。但是由于我们将 span 设置为指向错误的输入标记ty,编译器会显示在该标记下划线的片段。

这个技巧也被syn用来打印漂亮的错误。事实上,如果你仍然syn使用它,你可以使用它的Error类型,特别是返回我们手动创建的令牌流的Error::to_compile_error方法quote_spanned!

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

DiagnosticAPI _

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

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

在该行之后,将打印错误,但您仍然可以在 proc-macro 中执行操作。通常,您只会返回一个空的令牌流。

于 2019-01-27T23:51:25.360 回答
2

接受的答案提到了不稳定的DiagnosticAPI,它比常规的compile_error. 在DiagnosticAPI 稳定之前,可能不会很快,您可以使用proc_macro_errorcrate。它提供了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类型,因为当它稳定时,它会更容易迁移到官方DiagnosticAPI。

于 2021-02-11T22:45:11.533 回答