How to handle array literal in macros attributes?

In writing my derive macro Default, I ran into a problem.
Below I will use my own Default macro.

#[derive(Default)]
pub struct TextStyle {
    #[default_field = "white"]
    pub colour: &'static str,

    #[default_field = "0.3px"]
    pub font_size: &'static str,

    #[default_field = [0.1; 2]]
    pub offsets: [f64; 2],
}

An error occurs in the offsets field:

attribute value must be a literal

if attr.path.is_ident("default_field") {
    match attr.parse_meta() {
        Ok(Meta::NameValue(meta)) => {
            if let Lit::Str(lit) = meta.lit {
                return Some(quote! { #lit });
            }
        }
        Ok(Meta::List(MetaList { nested, .. })) => {
            let nested_meta = nested
                .into_iter()
                .map(|nested_meta| match nested_meta {
                    NestedMeta::Lit(Lit::Float(lit)) => quote! { #lit },
                    _ => quote! {}
                })
                .collect::<Vec<_>>();
            return Some(quote! { [#(#nested_meta),*] });
        }
        _ => {}
    }
}

Here is the specific code in the macro that handles the default_field attribute:

Here I thought that Ok(Meta::List would handle the literal array, but apparently I misunderstood the meaning of Meta::List, because the macros still gives the error:

attribute value must be a literal

Is it impossible to pass other literals to the macro attribute besides those described in enum Lit?

You seem to use syn1.0, which is quite outdated.

This is less common. Yeah, that syntax seems good, but FYI even in Variant attributes · Serde , a string literal is preferred. #[serde(bound = "T: MyTrait")] instead of #[serde(bound = T: MyTrait)], #[serde(deserialize_with = "path::to_path")] instead of #[serde(deserialize_with = path::to_path)].

Then if you really want the syntax, it'd be a bit harder to do: manually collect tokens after the equal sign, but what if you write multple metadata in one attribute, like #[default_field = [0.1; 2], other = xxx].

If you only allow string literal, there is an inherent range for parsing: for #[default_field = "[0.1; 2]"]

  • parse a litstr
  • then call .parse on litstr
  • neat and done!
  • a lot easier for more complicated case like #[default_field = "[0.1; 2]", other = xxx] (well, you would say this case is never allowed, then another story. Just to inform you of it.)
1 Like

Sure, I can parse a string literal and use prarse(). But I would also have to deal with the array being of the right type (too much work).
That's why I wanted to know if it's possible to work with an array literal. And thanks for that

use syn::*; // syn 2.0
fn main() -> Result<()> {
    let attr: Attribute = parse_quote!(#[default_field = [0.1; 2]]);
    dbg!(parse_meta(&attr)?);

    let attr2: Attribute = parse_quote!(#[default_field = "[0.1; 2]"]);
    dbg!(parse_meta_str(&attr2)?);
    Ok(())
}

fn parse_meta(attr: &Attribute) -> Result<&ExprRepeat> {
    if let Meta::NameValue(meta) = &attr.meta {
        if meta.path.is_ident("default_field") {
            if let Expr::Repeat(rep) = &meta.value {
                return Ok(rep);
            }
        }
    }
    todo!();
}

fn parse_meta_str(attr: &Attribute) -> Result<ExprRepeat> {
    if let Meta::NameValue(meta) = &attr.meta {
        if meta.path.is_ident("default_field") {
            if let Expr::Lit(ExprLit {
                lit: Lit::Str(lit), ..
            }) = &meta.value
            {
                return lit.parse();
            }
        }
    }
    todo!();
}

Rust Playground

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.