Skip to content

The dead_code lint behaves differently on code inside a declarative macro that uses a procedural macro which accepts user-provided tokens for visibility qualifiers #141005

Open
@shepmaster

Description

@shepmaster
.
├── Cargo.toml
├── the-derive
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── the-library
    ├── Cargo.toml
    └── src
        └── lib.rs

Cargo.toml

[workspace]
resolver = "3"
members = ["the-derive", "the-library"]

the-derive/Cargo.toml

[package]
name = "the-derive"
version = "0.1.0"
edition = "2024"

[lib]
proc-macro = true

the-derive/src/lib.rs

#![feature(proc_macro_quote)]

extern crate proc_macro;

use proc_macro::{quote, TokenStream, TokenTree};

#[proc_macro_derive(GenerateStuff, attributes(vis))]
pub fn generate_stuff(item: TokenStream) -> TokenStream {
    let tokens = item.into_iter().collect::<Vec<_>>();

    let TokenTree::Group(attr_group) = &tokens[1] else { panic!("malformed") };
    let attr_group_tokens = attr_group.stream().into_iter().collect::<Vec<_>>();

    let TokenTree::Group(vis_group) = &attr_group_tokens[1] else { panic!("malformed") };
    let vis_group_tokens = vis_group.stream();

    quote! {
        $vis_group_tokens struct GeneratedType;

        impl GeneratedType {
            $vis_group_tokens fn unused_method(&self) {}
        }
    }
}

the-library/Cargo.toml

[package]
name = "the-library"
version = "0.1.0"
edition = "2024"

[dependencies]
the-derive = { path = "../the-derive" }

the-library/src/lib.rs

#![deny(dead_code)]

// This version works
#[derive(the_derive::GenerateStuff)]
#[vis(pub(crate))]
struct Parent;

// This version fails
// macro_rules! invoke {
//     () => {
//         #[derive(the_derive::GenerateStuff)]
//         #[vis(pub(crate))]
//         struct Parent;
//     };
// }
// invoke!();

pub fn usage() {
    // Assume that the base type is used somewhere
    let _ = Parent;
}

Execution

% cargo +nightly check --all
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s

Then switch to the version inside the declarative macro:

% cargo +nightly check --all
    Checking the-library v0.1.0 (/private/tmp/repro/the-library)
error: struct `GeneratedType` is never constructed
  --> the-library/src/lib.rs:16:1
   |
16 | invoke!();
   | ^^^^^^^^^
   |
note: the lint level is defined here
  --> the-library/src/lib.rs:1:9
   |
1  | #![deny(dead_code)]
   |         ^^^^^^^^^
   = note: this error originates in the macro `invoke` (in Nightly builds, run with -Z macro-backtrace for more info)

error: method `unused_method` is never used
  --> the-library/src/lib.rs:16:1
   |
16 | invoke!();
   | ^^^^^^^^^ method in this implementation
   |
   = note: this error originates in the macro `invoke` (in Nightly builds, run with -Z macro-backtrace for more info)

Expected

I expected the behavior of the dead_code lint to be consistent if the procedural macro is invoked from inside a declarative macro or not.

Actual

The behavior of the dead_code lint is not consistent, it changes if the procedural macro is invoked from inside a declarative macro or not.

Notes

  • Changing the procedural macro to hard-code the pub(crate) as a string that is parsed causes both forms to behave the same:

    #[proc_macro_derive(GenerateStuff, attributes(vis))]
    pub fn generate_stuff(_item: TokenStream) -> TokenStream {
        r##"
            pub(crate) struct GeneratedType;
    
            impl GeneratedType {
                pub(crate) fn unused_method(&self) {}
            }
    "##.parse().unwrap()
    }
  • Changing the procedural macro to hard-code the pub(crate) inside the quote! macro causes both forms to produce a surprising (unrelated?) error:

    pub fn generate_stuff(_item: TokenStream) -> TokenStream {
        quote! {
            pub(crate) struct GeneratedType;
    
            impl GeneratedType {
                pub(crate) fn unused_method(&self) {}
            }
        }
    }
    error[E0742]: visibilities can only be restricted to ancestor modules
     --> the-library/src/lib.rs:4:10
      |
    4 | #[derive(the_derive::GenerateStuff)]
      |          ^^^^^^^^^^^^^^^^^^^^^^^^^
      |
      = note: this error originates in the derive macro `the_derive::GenerateStuff` (in Nightly builds, run with -Z macro-backtrace for more info)
    

Meta

rustc --version --verbose:

rustc 1.88.0-nightly (2fa8b11f0 2025-04-06)
binary: rustc
commit-hash: 2fa8b11f0933dae9b4e5d287cc10c989218e8b36
commit-date: 2025-04-06
host: aarch64-apple-darwin
release: 1.88.0-nightly
LLVM version: 20.1.2

For the reproduction case above, I'm using nightly features. In my real case, I'm using the syn/proc-macro2/quote crates and the problem occurs on stable Rust:

rustc +stable --version --verbose
rustc 1.86.0 (05f9846f8 2025-03-31)
binary: rustc
commit-hash: 05f9846f893b09a1be1fc8560e33fc3c815cfecb
commit-date: 2025-03-31
host: aarch64-apple-darwin
release: 1.86.0
LLVM version: 19.1.7

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-lintsArea: Lints (warnings about flaws in source code) such as unused_mut.C-bugCategory: This is a bug.L-dead_codeLint: dead_codeT-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