Skip to content

Forbidding lints doesn't really work in macros #110613

@WaffleLapkin

Description

@WaffleLapkin

Story-driven explanation

I was writing a macro that implements ADT <=> int conversions:

TW: macros
trait Convert {
    fn into_u32(self) -> u32;

    fn from_u32(v: u32) -> Self;
}

macro_rules! impl_convert {
    (
        for $Self:ty;

        // This basically matches `Type::Variant { fields... } <=> 0,`
        $( $($path:ident)::* $( { $( $fields:tt )* })? <=> $int:literal, )*
    ) => {
        impl $crate::Convert for $Self {
            fn into_u32(self) -> u32 {
                #[forbid(unreachable_patterns)]
                match self {
                    $( $($path)::* $( { $( $fields )* } )? => $int, )*
                }
            }

            fn from_u32(v: u32) -> Self {
                #[forbid(unreachable_patterns)]
                match v {
                    $( $int => $($path)::* $( { $( $fields )* } )?, )*
                    _ => panic!("invalid value: {v:?}"),
                }
            }
        }
    };
}

Used like this:

enum Type {
    A,
    B { v: bool },
}

impl_convert! {
    for Type;
    Type::A <=> 0,
    Type::B { v: false } <=> 1,
    Type::B { v: true  } <=> 2,
}

I tested it, it worked perfectly. #[forbid(unreachable_patterns)] even ensures that the mapping is 1 to 1 (duplicate patterns would make the lint go off).

However, when I've made a compile_fail doc test to ensure that duplicate patterns are always rejected, it failed (meaning the compilation passed). At first I though that the problem is in rustdoc, but latter testing showed that this is actually the rustc's behavior: if a lint is forbidden inside a macro and is issued inside the macro, then it is not shown and does not abort compilation in case of #[forbid].

This is a shame! I would expect #[forbid(unreachable_patterns)] actually work and catch wrong macro uses. This behavior seems hard to anticipate, in other words "footgun-y". This is especially annoying given that the calls in the same crate, where you are most likely to test your macros, do produce lints.

More precise explanation

Given these two crates:

// crate `a`

#[macro_export]
macro_rules! example {
    () => {
        #[forbid(unused_variables)]
        const _: () = { let a = 0; };
    }
}

example! {} // <-- invocation in `a`
// crate `b`

a::example! {} // <-- invocation in `b`

(play)

Invocation in a fails the compilation because of the forbid. However, if you comment it out, the invocation in b compiles successfully, the lint and forbid are ignored. This is inconsistent, hard to test and error prone (especially if the lint is actually caused by the user input, as in my example in the story above).

Proposal?

I'd like to "fix" this, however, it's not clear what exact rules here should be, how much of a breaking change this is, etc. I'm opening this issue to hear the feedback.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-lintsArea: Lints (warnings about flaws in source code) such as unused_mut.A-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions