How to use syn v2 to parse an attritube like this: `#[attr("a", "b", "c")]`?

2.3k Views Asked by At

In syn v1, there is NestedMeta which is very convenient for parsing nested meta. But since syn v2, it's somehow been removed.

For example,

trait Hello {
    fn hello();
}

#[derive(Hello)]
#[hello("world1", "world2")]
struct A;

fn main() {
    A::hello();
}

I want the code above to print Hello world1, world2! on the screen. My proc-macro can be implemented using syn v1 like below

use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, Meta};

#[proc_macro_derive(Hello, attributes(hello))]
pub fn hello_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();

    let name = &ast.ident;

    let mut target: Vec<String> = vec![];

    for attr in ast.attrs {
        if let Some(attr_meta_name) = attr.path.get_ident() {
            if attr_meta_name == "hello" {
                let attr_meta = attr.parse_meta().unwrap();

                match attr_meta {
                    Meta::List(list) => {
                        for p in list.nested {
                            match p {
                                NestedMeta::Lit(lit) => match lit {
                                    Lit::Str(lit) => {
                                        target.push(lit.value());
                                    },
                                    _ => {
                                        panic!("Incorrect format for using the `hello` attribute.")
                                    },
                                },
                                NestedMeta::Meta(_) => {
                                    panic!("Incorrect format for using the `hello` attribute.")
                                },
                            }
                        }
                    },
                    _ => panic!("Incorrect format for using the `hello` attribute."),
                }
            }
        }
    }

    if target.is_empty() {
        panic!("The `hello` attribute must be used to set at least one target.");
    }

    let target = target.join(", ");

    let expanded = quote! {
        impl Hello for #name {
            fn hello() {
                println!("Hello {}.", #target);
            }
        }
    };

    expanded.into()
}

But when I am trying to re-implement it with syn v2, I stuck on the parse_nested_meta method which seems to reject the literals.

use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, Meta};

#[proc_macro_derive(Hello, attributes(hello))]
pub fn hello_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();

    let name = &ast.ident;

    let mut target: Vec<String> = vec![];

    for attr in ast.attrs {
        if let Some(attr_meta_name) = attr.path().get_ident() {
            if attr_meta_name == "hello" {
                let attr_meta = attr.meta;

                match attr_meta {
                    Meta::List(list) => {
                        list.parse_nested_meta(|meta| {
                            // I don't know how to handle this
                            Ok(())
                        })
                        .unwrap();
                    },
                    _ => panic!("Incorrect format for using the `hello` attribute."),
                }
            }
        }
    }

    if target.is_empty() {
        panic!("The `hello` attribute must be used to set at least one target.");
    }

    let target = target.join(", ");

    let expanded = quote! {
        impl Hello for #name {
            fn hello() {
                println!("Hello {}.", #target);
            }
        }
    };

    expanded.into()
}

How to use syn v2 to parse the attribute?

3

There are 3 best solutions below

1
On
#[proc_macro_derive(Hello, attributes(hello))]
pub fn hello_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();

    let name = &ast.ident;

    let mut target: Vec<String> = vec![];

    for attr in ast.attrs {
        match &attr.meta {
            syn::Meta::List(list) if attr.path().is_ident("hello") => target.extend(
                list.parse_args::<Greetings>()
                    .unwrap()
                    .0
                    .into_iter()
                    .map(|lit| lit.value()),
            ),
            _ => panic!(),
        }
    }

    if target.is_empty() {
        panic!("The `hello` attribute must be used to set at least one target.");
    }

    let target = target.join(", ");

    let expanded = quote! {
        impl Hello for #name {
            fn hello() {
                println!("Hello {}.", #target);
            }
        }
    };

    expanded.into()
}

struct Greetings(Punctuated<LitStr, Token![,]>);

impl syn::parse::Parse for Greetings {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        Ok(Self(Punctuated::<LitStr, Token![,]>::parse_terminated(
            input,
        )?))
    }
}
0
On

with syn v2 things have changed a little bit, so in your code:

    for attr in ast.attrs {
        if let Some(attr_meta_name) = attr.path().get_ident() {
            if attr_meta_name == "hello" {
                let attr_meta = attr.meta;

                match attr_meta {
                    Meta::List(list) => {
                        list.parse_nested_meta(|meta| {
                            // I don't know how to handle this
                            Ok(())
                        })
                        .unwrap();
                    },
                    _ => panic!("Incorrect format for using the `hello` attribute."),
                }
            }
        }
    }

you would want do use the Punctuated::parse_terminated() function like this:

list.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)

Putting the things together your for loop would then look like this:

    for attr in ast.attrs {
        match &attr {
            Meta::List(list) if list.path.is_ident("hello") => {
                list.parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)
                    .map_err(|_| {
                        // returning a specific syn::Error to teach the right usage of your macro 
                        syn::Error::new(
                            list.span(),
                            // this indoc macro is just convenience and requires the indoc crate but can be done without it
                            indoc! {r#"
                                The `hello` attribute expects string literals to be comma separated

                                = help: use `#[hello("world1", "world2")]`
                            "#}
                        )
                    })?;
            }
            meta => {
                // returning a syn::Error would help with the compiler diagnostics and guide your macro users to get it right
                return Err(syn::Error::new(
                    meta.span(),
                    indoc! {r#"
                        The `hello` attribute is the only supported argument

                        = help: use `#[hello("world1")]`
                    "#})
                );
            }
        }
    }

Notes:

0
On

It seems that there are no other built-in struct which is implemented the Parse trait can do that. So we have to create one on our own.

use proc_macro::TokenStream;
use quote::quote;
use syn::{
    parse::{Parse, ParseStream},
    DeriveInput, LitStr, Meta, Token,
};

struct MyParser {
    v: Vec<String>,
}

impl Parse for MyParser {
    #[inline]
    fn parse(input: ParseStream) -> Result<Self, syn::Error> {
        let mut v: Vec<String> = vec![];

        loop {
            if input.is_empty() {
                break;
            }

            v.push(input.parse::<LitStr>()?.value());

            if input.is_empty() {
                break;
            }

            input.parse::<Token!(,)>()?;
        }

        Ok(MyParser {
            v,
        })
    }
}

#[proc_macro_derive(Hello, attributes(hello))]
pub fn hello_world_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();

    let name = &ast.ident;

    let mut target: Vec<String> = vec![];

    for attr in ast.attrs {
        if attr.path().is_ident("hello") {
            match attr.meta {
                Meta::List(list) => {
                    let parsed: MyParser = list.parse_args().unwrap();

                    target.extend_from_slice(&parsed.v);
                },
                _ => panic!("Incorrect format for using the `hello` attribute."),
            }
        }
    }

    if target.is_empty() {
        panic!("The `hello` attribute must be used to set at least one target.");
    }

    let target = target.join(", ");

    let expanded = quote! {
        impl Hello for #name {
            fn hello() {
                println!("Hello {}.", #target);
            }
        }
    };

    expanded.into()
}