Proc macro attribute makes compiler shout at me when #[should_panic] is involved

I am writing a proc macro attribute which is intended to be used as an attribute for #[test]-functions. Briefly what happens is: depending on the position that I choose for my attribute in a function that is marked both #[test] and #[should_panic], the compiler will complain that #[should_panic] is an unused attribute, although the tests behave as they should.

Minimal Working Example
I have created a repo with an MWE here. It defines a proc macro attribute #[dummy], that parses a function and then spits it out unaltered like so:

#[proc_macro_attribute]
pub fn dummy(_args: TokenStream, input: TokenStream) -> TokenStream {
    input
}

Now if I use this macro in tests from another crate, then the compiler will complain that #[should_panic] is an unused attribute in these tests:

#[test]
#[dummy]
#[should_panic]
fn test3() {
    assert_eq!(true,false);
}

#[test]
#[should_panic]
#[dummy]
fn test4() {
    assert_eq!(true,false);
}

However, both tests pass when I run them, indicating that #[should_panic] does what it is supposed to do.

Only if I stick my attribute at the top of the list of attributes will the compiler be happy:

#[dummy]
#[test]
#[should_panic]
fn test2() {
    assert_eq!(true,false);
}

I experienced this behavior with both the compiler and clippy for toolchains 1.48.0 and 1.52.1. What is happening here?

EDIT: I simplified the proc macro by removing the dependency to the syn and quote crates. The behavior I am experiencing has nothing to do with the tokens being fed to the syn and quote crates.

EDIT 2: This topic reports the same problem. I did not find it during my first search. However, there seems to be no brilliant solution except to stick the custom attribute at the top. The linked topic is almost 2 years old. Is this still the case?

1 Like

Here is a Playground repro of your issue. Not that your presentation of the issue wasn't good, quite the opposite! But rather, so that we can more easily test stuff.

A few observations:

  • The compiler is not "shouting", but it is indeed "warning" against something that does not deserve such a warning;

  • Despite the warning, the test is still run, and in a #[should_panic] fashion.

  • If the proc macro removes the #[should_panic], then:

    • the warning disappears;

    • the should_panic-ness of the test is still there since #[test] witnessed it.

    • Demo.

From all this, the least bad solution right now would be to scan the attrs, and:

  • if #[test] is encountered, leave them be.

  • else, if #[should_panic…] or #[ignore] is encountered, remove them.

The other approach would be to simply emit a #[allow(unused_attributes)] :grimacing:

Note however, if the code is as in the playground, then this won't work, since #[test] is before custom attribute and therefore not accessible to it, AFAIK.

Yes, I was being a bit melodramatic with the "shouting" bit :smiley:

Thanks for the comprehensive response. I would never have thought of removing the #[should_panic] attribute inside my macro. How is it possible that the #[test] still sees the attribute if I removed it inside my macro? I naively imagined that my #[dummy] attribute would just take the whole function including attributes, then return a new token stream which would be the only thing that the attributes above it ever saw. How is that not the case?

At any rate, that solution would fix the compiler warnings in test3. Is there even a way to fix test3 and why would the compiler complain about that one in the first place, because it seems the most clear-cut to me?

EDIT changed "Is there even a way to fix test2" to "Is there even a way to fix test3" in the last paragraph.

Yes, I confirmed that the custom attributes only see the attributes in the lines below them.

Yes, I am aware of that, hence my logic. Basically there are two configurations:

#[my_attr]
#[test] /* <-> */ #[should_panic/ignore]

and

#[test]
#[my_attr] /* <-> */ #[should_panic/ignore]

In the former case, test hasn't been processed yet, and so neither has the inert should_panic. #[my_attr] is the first attribute that thus sees all the others. #[my_attr] thus ought to just forward these attrs so that they are actually taken into account. Note that this is a configurations which triggers no lint.

In the latter case, the #[test] function has been registered, and the inert should_panic has been "observed". The input that #[test] received is then forwarded to #[my_attr], with, thus, the #[test] attribute no longer visible. So #[should_panic/ignore] can (and should, to remove the warning) be then removed by #[my_attr]

1 Like

Thank you. I tried your solution and it solves all of my problems. I used the syn and quote crates for parsing in my dummy macro and implemented the logic you suggested (in a quick&dirty fashion). It now looks like this:

pub fn dummy(_args: TokenStream, input: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(input as ItemFn);
    if func.attrs.iter().find(|attr|attr.path.segments.first().unwrap().ident == "test").is_none() {
        func.attrs.retain(|attr|attr.path.segments.first().unwrap().ident != "should_panic");
    }
    TokenStream::from(quote!(#func))
}

This makes the warning disappear in all cases. I am still confused as to why this works for test4, because I assumed my custom attribute would only see the attributes defined below it. This true for the #[test] attribute, i.e. my attribute only sees #[test] when it is written below it. However, #[should_panic] is visible to my proc macro in all cases, regardless if it is defined above or below it.

@Yandros: Do you have any idea if this is expected behavior?

NOTE: The solution above should also incorporate #[ignore], but I kept it simple here.

1 Like

This is because #[should_panic] is an inert attribute.

  • AFAIK, there are only two cases of inert attributes: built-in ones such as should_panic, doc comments, etc.

  • derive helpers: reference.

Basically the compiler might have a an algorithm such as:

// PSEUDO-CODE!
let mut items: Box<dyn Iterator<Item = Item>> = …;
while let Some(mut item) = items.next() {
    let attrs: &mut Vec<Attribute> = &mut item.attrs;
    if let Some(idx) = attrs.iter().position(|attr| attr.is_inert().not()) {
        let attr = attrs.remove(idx).unwrap();
        let output = process_proc_macro(attr, item.to_token_stream())?;
        items = Box::new(Iterator::chain(
            output.parse::<Vec<Item>>()?.into_iter(),
            items,
        ));
    }
}

So, in the case of test4, we have the following loop iterations:

  1. Situation: #[test] #[should_panic] #[dummy].
    idx = 0, #[test] is stripped from the attrs and processes #[should_panic] #[dummy] …, as an identity function but one which registers the function within Rust test suite internals;

  2. Situation: #[should_panic] #[dummy].
    idx = 1, #[dummy] is stripped from the attrs and processes #[should_panic] ….
    This is where #[should_panic] can be stripped.

1 Like

Thanks, you have been immensely helpful.

1 Like