Passing through errors in an attribute macro

Hello all,

Let's say I have a very simple attribute macro (in its own macros crate, where it needs to be):

use std::str::FromStr;
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn example_macro(_attr: TokenStream, code: TokenStream) -> TokenStream {
    TokenStream::from_str(&format!("#[allow(dead_code)]\n{code}")).unwrap()
}

And I have a src/lib.rs that has this:

use macros::example_macro;

struct FuncResult(u64);

#[example_macro]
pub fn get_result() -> FuncResult {
    FuncResult(1)
}

Then that works great. What does not work great is if there's an error in the underlying code:

#[example_macro]
pub fn get_result_faulty() -> FuncResult {
    FuncResult(-1)
}

leads to this slightly hard-to-interpret output:

error[E0600]: cannot apply unary operator `-` to type `u64`
  --> src/lib.rs:10:1
   |
10 | #[example_macro]
   | ^^^^^^^^^^^^^^^^
   | |
   | cannot apply unary operator `-`
   | help: you may have meant the maximum value of `u64`: `u64::MAX`

What I would like is for the context of the error to show the original code context, not the macro.

My understanding is that the syn::parse_macro_input is the correct way to make this error message clearer:

#[proc_macro_attribute]
pub fn example_macro(_attr: TokenStream, code: TokenStream) -> TokenStream {
    parse_macro_input!(code as FuncResult);
...

but for this to be used, FuncResult needs to be imported into the macro crate, which feels awkward since the macro crate is being imported into the main crate. This then also limits which functions the #[example_macro] attribute can be applied to, to those that return this type. Is there a better way to do this?

You do want parse_macro_input!, but not as the result type of the annotated function, but rather as ItemFn. You're telling syn what kind of syntax you want to parse the token stream as.

The more important part is actually quote!, though. If you write quote!(#[allow(dead_code)] #code), then that avoids stringifying the code, meaning that span information for errors (and name hygiene) is preserved.

1 Like

Thanks - if I try that:

#[proc_macro_attribute]
pub fn example_macro(_attr: TokenStream, code: TokenStream) -> TokenStream {
    let code_ = code.clone();
    parse_macro_input!(code_ as ItemFn);
    TokenStream::from_str(&format!("#[allow(dead_code)]\n{code}")).unwrap()
}

Then it does compile, but I still get the same error:

error[E0600]: cannot apply unary operator `-` to type `u64`
  --> src/lib.rs:10:1
   |
10 | #[example_macro]
   | ^^^^^^^^^^^^^^^^
   | |
   | cannot apply unary operator `-`
   | help: you may have meant the maximum value of `u64`: `u64::MAX`

The solution involving quote! sounds good, but my real-life example is more complex than just adding the #[allow(dead_code)] and so doesn't lend itself well to that.

Have you seen what interpolations quote! supports? They are quite powerful.

OK. So, probably the best way to deal with this is to get better at using quote! and its related macros? I was trying to see in other projects' source how this tends to be done, I assume it's a fairly common problem.

Thanks, this has actually worked out well for me. As you say, quote! is the important part. My macro was quite fiddly but a combination of iterating through the TokenTrees in the TokenStream and using quote! works fine.