dtolnay / quote

Rust quasi-quoting
Apache License 2.0
1.32k stars 90 forks source link

Derive proc macro inside a `macro_rules!` combined with `quote_spanned!` leads to an error #257

Closed brylee10 closed 1 year ago

brylee10 commented 1 year ago

I created a procedural macro ImplTraitMacro to implement a trait ImplTrait on a struct. The below works (the implementation of the ImplTraitMacro is at the end of this post).

use custom_proc_macro::ImplTraitMacro;

#[derive(ImplTraitMacro)]
pub struct Struct {
  field1: u64,
}

pub trait ImplTrait {
  fn f(argument: u64) -> u64;  
}

I then created a macro_rules! to programatically set the field name of a struct, while still implementing a trait on the struct via a proc macro, as shown below.

use custom_proc_macro::ImplTraitMacro;

macro_rules! declarative_macro {
  ($field_name:ident) => {
      #[derive(ImplTraitMacro)]
      pub struct Struct {
        $field_name : u64,
      }
  };
}

declarative_macro!(field_name);

However, this encounters an error.

error[E0425]: cannot find value `argument` in this scope
  --> main/./main.rs:23:20
   |
23 | declarative_macro!(field_name);
   |                    ^^^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.

Specifically, the derive proc macro is implemented like the below:

use syn::{parse_macro_input, DeriveInput, DataStruct, spanned::Spanned};
use quote::{quote, quote_spanned};
use proc_macro2::Span;

#[proc_macro_derive(ImplTraitMacro)]
pub fn implement_trait(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // Boilerplate to obtain an arbitrary `Span`
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    let span = if let syn::Data::Struct(DataStruct {
        fields: syn::Fields::Named(named),
        ..
    }) = &input.data {
        let fields: Vec<Span> = named.named.iter().map(|f| f.span()).collect();
        fields[0]
    } else {
        panic!("Invalid input");
    };
    // Erroneous code
    let multiply_by_2 = quote_spanned!{span=>
        argument * 2
    };  
    quote! {
        impl impl_trait::ImplTrait for #name {
            fn f(argument: u64) -> u64 {
                #multiply_by_2
            } 
        }
    }.into()
}

It seems using a quote_spanned! whose tokens refer to an argument of a function (in this case called argument) causes the proc macro to be unable to find the argument variable when the proc macro is used inside a macro_rules!. This is fixed when either

  1. quote! is used instead of quote_spanned!
  2. When the quote_spanned! does not use a function argument as tokens (e.g. like the below)
    let multiply_by_2 = quote_spanned!{span=>
    123
    };  
    quote! {
    impl impl_trait::ImplTrait for #name {
        fn f(argument: u64) -> u64 {
            // Returning argument instead of `#multiply_by_2` also works
            // argument
            #multiply_by_2
        } 
    }
    }.into(

I'm using the following dependency versions

[dependencies]
syn = "2.0.18"
quote = "1.0.28"
proc-macro2 = "1.0.60"

The above code would constitute my minimal reproducible example. Let me know if you can reproduce this error, and why this would occur.

dtolnay commented 1 year ago

Yep, this is how Spans work. This is behaving correctly as far as I can tell.