rust-lang / rust-lang/rust

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

Open
#141,005 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-lints C-bug L-dead_code T-compiler
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

.
├── 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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the discrepancy with the workspace files and the two variants in the-library/src/lib.rs by running cargo +nightly check --all. Compare dead_code behavior for the direct derive and the invoke! declarative-macro form; done means both forms handle the generated visibility consistently without spurious lint errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.