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

时间:2019-01-27 20:44:52

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

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

我知道我可以在解析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
}

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

这应该怎么做?

2 个答案:

答案 0 :(得分:9)

除了恐慌之外,目前还有两种方法可以报告来自宏程序的错误: 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!调用的代码段。为此,使用了compile_error!调用的范围。但是由于我们将跨度设置为指向错误的输入令牌ty,所以编译器会显示该令牌下划线的代码段。

syn也使用此技巧来打印好的错误。实际上,如果您仍在使用syn,则可以使用其Error类型,尤其是Error::to_compile_error method,它返回的正是我们用quote_spanned!手动创建的令牌流: / p>

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

答案 1 :(得分:1)

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