Why does a nested proc macro in a declarative macro make the ident have a hygiene issue?

One other thing to note is, to verify the spans, you can also run the below

// Inside proc macro crate
use proc_macro::TokenStream;

#[proc_macro]
pub fn just_ret_let(v: TokenStream) -> TokenStream {
    let ident = v.to_string();
    let ts: proc_macro2::TokenStream = format!(r#"let {ident} = "just_ret_let";"#).parse().unwrap();
    for token in ts.clone().into_iter() {
        println!(
            "Token: {:?}, Span Start: {:?}, Span End: {:?}, Source File: {:?}",
            token,
            token.span().start(),
            token.span().end(),
            token.span().source_file(),
        );
    }
    ts.into()
}

// `main.rs` Inside main crate
macro_rules! test_me {
    ($name:ident) => {
        proc_macro_test::just_ret_let!($name);
    };
}

fn main() {
    test_me!(ddd);
    // Commented out since this will not compile
    // println!("{ddd}");
}

with the command (using nightly features and configurations to obtain the span locations and source file),

RUSTFLAGS='--cfg procmacro2_semver_exempt' cargo +nightly run

This will give an output like

Token: Ident { sym: let, span: #6 bytes(54..91) }, Span Start: LineColumn { line: 3, column: 8 }, Span End: LineColumn { line: 3, column: 45 }, Source File: SourceFile { path: "main/src/main.rs", is_real: true }
...

which in this case illustrates that the span of the tokens in ts resolve to line 3 in main.rs which is inside the test_me! macro. This means the ddd identifier is not in scope in main() due to macro hygiene.

You can then run a similar command with the alternative suggestion to see the spans resolve inside the main() function scope.

1 Like