【问题标题】:How to report errors in a procedural macro using the quote macro?如何使用引用宏报告程序宏中的错误?
【发布时间】:2019-06-20 21:46:28
【问题描述】:

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

我知道我可以在解析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,我就看不到报告错误的好方法。

这应该怎么做?

【问题讨论】:

    标签: error-handling rust rust-macros rust-proc-macros


    【解决方案1】:

    除了恐慌之外,目前有两种方法可以从 proc 宏报告错误: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。这包括这些令牌的跨度:您可以将任何您喜欢的跨度附加到您的输出令牌。所以主要思想是这样的:

    发出一个包含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! 调用的代码 sn-p。为此,使用了 compile_error! 调用的跨度。但是由于我们将 span 设置为指向错误的输入标记 ty,因此编译器会显示在该标记下划线的 sn-p。

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

    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 中执行操作。通常,您只会返回一个空的令牌流。

    【讨论】:

      【解决方案2】:

      接受的答案提到了不稳定的Diagnostic API,它比常规的compile_error 为您提供更多的权力和控制。在Diagnostic API 稳定之前,which probably will not be any time soon,您可以使用proc_macro_error crate。它提供了一个Diagnostic 类型,旨在与不稳定的proc_macro::Diagnostic 兼容API。整个 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。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多