I am writing a procedural macro which works fine, but I am having trouble reporting errors in an ergonomic way. Using panic!
"works" but is not elegant and does not present the error message to the user nicely.
I know that I can report good errors while parsing a TokenStream
, but I need to produce errors while traversing the AST after it has been parsed.
The macro invocation looks like this:
attr_test! {
#[bool]
FOO
}
And should output:
const FOO: bool = false;
This is the macro code:
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")
}
}
I would like to produce an error if anything other than bool
is specified in the attribute. For example, input like this:
attr_test! {
#[something_else]
FOO
}
should result in something like:
error: expected bool
attr_test! {
#[something_else]
^^^^^^^^^^^^^^ expected bool
FOO
}
During parsing, there is a Result
, which has lots of useful information including a span
, so the resulting errors can highlight the exact parts of the macro call that have a problem. But once I'm traversing the AST, I can't see a good way to report errors.
How should this be done?
Apart from panicking, there are currently two ways to reports errors from a proc-macro: the unstable
Diagnostic
API and "thecompile_error!
trick". Currently, the latter is mostly used because it works on stable. Let's see how they both work.The
compile_error!
trickSince Rust 1.20, the
compile_error!
macro exists in the standard library. It takes a string and leads to an error at compile time.Which leads to (Playground):
This macro has been added for two cases:
macro_rules!
macros and#[cfg]
. In both cases, library authors can add better errors if the user uses the macro incorrectly or has the wrongcfg
values.But proc-macro programmers had an interesting idea. As you might know, the
TokenStream
you return from your procedural macro can be created however you like. That includes the spans of those tokens: you can attach any spans you like to your output tokens. So the main idea is this:Emit a tokenstream containing
compile_error!("your error message");
but set the span of those tokens to the span of the input token that caused the error. There is even a macro inquote
which makes this easier:quote_spanned!
. In your case, we can write this:For your faulty input, the compiler now prints this:
Why exactly does this work? Well: the error for
compile_error!
shows the code snippet containing thecompile_error!
invocation. For that, the span of thecompile_error!
invocation is used. But since we set the span to point to the faulty input tokenty
, the compiler shows the snippet underlining that token.This trick is also used by
syn
to print nice errors. In fact, if you are usingsyn
anyway, you can use itsError
type and in particular theError::to_compile_error
method which returns exactly the token stream we manually created withquote_spanned!
:The
Diagnostic
APIAs this is still unstable, just a short example. The diagnostic API is more powerful than the trick above as you can have multiple spans, warnings and notes.
After that line, the error is printed, but you can still do stuff in your proc-macro. Usually, you would just return an empty token stream.