Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strange Behavior When Lacking the Specifier # in quote! {...} In a Proc-Macro #287

Closed
jnoorchashm37 opened this issue Jan 17, 2025 · 1 comment

Comments

@jnoorchashm37
Copy link

Strange Behavior When Lacking the Specifier # in quote! {...}

quote = "1.0.37"
proc-macro2 = "1.0.86"

I am decoding structs in enum variants based off the variant name as a lowercase string, these are the types I am dealing with:

#[derive(Debug, Clone, PartialEq, Eq, Hash, NeonObject)]
pub enum OrderFilterNeon {
    ByPoolId { params: OrderFilterByPoolIdParamsNeon },
    ByTokens { params: OrderFilterByTokensParamsNeon },
    None
}

#[derive(Debug, Clone, NeonObject, PartialEq, Eq, Hash)]
struct OrderFilterByPoolIdParamsNeon {
    pool_id: PoolId
}

#[derive(Debug, Clone, NeonObject, PartialEq, Eq, Hash)]
struct OrderFilterByTokensParamsNeon {
    token0: Address,
    token1: Address
}

When forgetting to use the # specifier in the case below, the variable variant_name_str would always match to the 1st variant of the enum even though the string was directing to the 2nd variant (and variant_name_str was only defined in the scope of the proc macro fn, nowhere else). Strangely, the print statement seemed to work correctly, printing out the string variable of the passed in variant:

quote::quote! {
    variant_name_str => {
        println!("IS VARIANT: {}", variant_name_str);
        #(#fields_from_set)*
        Ok(Self::#variant_name { #(#field_names),* })
    }
}

giving:
Image

when adding the # specifier, however, the matching worked as expected:

quote::quote! {
    #variant_name_str => {
        println!("IS VARIANT: {}", #variant_name_str);
        #(#fields_from_set)*
        Ok(Self::#variant_name { #(#field_names),* })
    }
}

giving:
Image


Full Code:

fn parse_enum(item: &DeriveInput, data_enum: &DataEnum) -> syn::Result<TokenStream> {
    let name = &item.ident;
    let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();

    let (variant_to_tokens, variant_from_tokens): (Vec<_>, Vec<_>) = data_enum
        .variants
        .iter()
        .map(|variant| {
            let variant_name = &variant.ident;
            let variant_name_str = variant_name.to_string().to_lowercase();
            let fields = &variant.fields;
            let (fields_to_set, fields_from_set): (Vec<_>, Vec<_>) = fields
                .iter()
                .map(|field| field_to_neon_value(field, true).zip(field_from_neon_value(field)))
                .map(|v| v.map(|(a, b)| (Some(a), Some(b))).unwrap_or_default())
                .unzip();

            let field_names = fields
                .iter()
                .map(|field| {
                    field
                        .ident
                        .clone()
                        .expect("enum cannot have unnamed parameters in their variants")
                })
                .collect::<Vec<_>>();

            if field_names.len() > 1 {
                panic!(
                    "for parsing simplicity, enums can have 0 or 1 named params in their \
                     variants. If there are multiple, create a struct containing the params and \
                     set a `param` parameter as the only named param in the enum variant"
                );
            }

            (
                quote::quote! {
                    #name::#variant_name { #(#field_names),* } => {
                        let val = neon::prelude::JsString::new(cx, #variant_name_str.to_string());
                        obj.set(cx, "type", val)?;
                        #(#fields_to_set)*
                    }
                },
                quote::quote! {
                    variant_name_str => {
                        println!("IS VARIANT: {}", variant_name_str);
                        #(#fields_from_set)*
                        Ok(Self::#variant_name { #(#field_names),* })
                    }
                }
            )
        })
        .unzip();

    let trait_impl = quote::quote! {
        impl #impl_generics crate::js_utils::MakeNeonObject for #name #ty_generics #where_clause {
            fn make_object<'a, C: neon::prelude::Context<'a>>(&self, cx: &mut C) -> neon::prelude::NeonResult<neon::prelude::Handle<'a, neon::prelude::JsObject>> {
                let obj = neon::context::Context::empty_object(cx);
                let me: Self = self.clone();
                match me {
                    #(#variant_to_tokens)*
                };

                Ok(obj)

            }
        }

        impl #impl_generics crate::js_utils::AsNeonValue for #name #ty_generics #where_clause {
            type NeonValue = neon::prelude::JsObject;

            fn as_neon_value<'a, C: neon::prelude::Context<'a>>(
                &self,
                cx: &mut C
            ) -> neon::prelude::NeonResult<neon::prelude::Handle<'a, Self::NeonValue>> {
                crate::js_utils::MakeNeonObject::make_object(self, cx)
            }

            fn from_neon_value<'a, C: neon::prelude::Context<'a>>(
                value: neon::prelude::Handle<'a, Self::NeonValue>,
                cx: &mut C
            ) -> neon::prelude::NeonResult<Self>
            where
                Self: Sized {
                    let variant_name = value
                        .get::<neon::types::JsString, _, _>(cx, "type")?
                        .value(cx);


                    match variant_name.to_lowercase().as_str() {
                        #(#variant_from_tokens)*
                        _ => unreachable!("'{variant_name}' is not a valid variant")
                    }


                }
        }
    };

    Ok(trait_impl)
}

pub(super) fn field_to_neon_value(field: &Field, is_enum: bool) -> Option<TokenStream> {
    field.ident.as_ref().map(|field_name| {
        let name_str = field_name.to_string();
        let field_ident = if is_enum {
            quote::quote! {#field_name}
        } else {
            quote::quote! {self.#field_name}
        };
        quote::quote! {
            let val = crate::js_utils::AsNeonValue::as_neon_value(&#field_ident, cx)?;
            obj.set(cx, #name_str, val)?;
        }
    })
}

pub(super) fn field_from_neon_value(field: &Field) -> Option<TokenStream> {
    field.ident.as_ref().map(|field_name| {
        let field_name_str = field_name.to_string();
        let field_ty = &field.ty;
        let field_ty_str = format!("{:?}", field_ty);
        quote::quote! {
            let field_name_obj = value.get::<<#field_ty as crate::js_utils::AsNeonValue>::NeonValue, _, _>(cx, #field_name_str).expect(&format!("could not get field name {}", #field_name_str));
            println!("converting {}: {}", #field_name_str, #field_ty_str); 
            // println!("converting {}", #field_name_str);
            let #field_name = crate::js_utils::AsNeonValue::from_neon_value(field_name_obj, cx).expect(&format!("could not convert field name {}", #field_name_str));
        }
    })
}
@dtolnay
Copy link
Owner

dtolnay commented Jan 18, 2025

As far as I can tell, quote is behaving correctly. You would need to minimize this further if you want to demonstrate that there is a quote issue.

@dtolnay dtolnay closed this as completed Jan 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants