📛

derive macro の helper attribute を試す

に公開

前回は derive macro を書いてみました 。今回は前回の derive macro の例に helper attribute を追加します。

前回の derive macro の例

前回は、これが

#[derive(derive1::VariantsFn)]
enum E1 {
    A,
    B(i32),
    C { b: bool },
}

こうなる

enum E1 {
    A,
    B(i32),
    C { b: bool },
}
impl E1 {
    pub fn variants() -> &'static [&'static str] {
        &["A", "B", "C"]
    }
}

VariantsFn という derive macro を定義しました。

今回は rename 属性を追加する

今回はここに rename という derive macro helper attribute を追加します。rename 属性が指定された variant は識別子の文字列表現の代わりに rename で指定された文字列リテラルを使用します。

次のようなイメージです。

#[derive(derive1::VariantsFn)]
enum E1 {
    #[rename = "X"]
    A,
    B(i32),
    C { b: bool },
}

fn main() {
    // "A" ではなく "X" が使用される
    assert_eq!(E1::variants(), &["X", "B", "C"]);
}

define macro helper attributes の定義

proc_macro_derive 属性には attributes キーで derive macro helper attributes を指定できます。

公式のリファレンスにある例です。

https://doc.rust-lang.org/reference/procedural-macros.html#derive-macro-helper-attributes

#[proc_macro_derive(HelperAttr, attributes(helper))]
pub fn derive_helper_attr(_item: TokenStream) -> TokenStream {
    TokenStream::new()
}
#[derive(HelperAttr)]
struct Struct {
    #[helper] field: ()
}

前回の例への追加

前回から変更されたファイルを示します。今回のソースコードの肝心な部分は #[proc_macro_deri(..., attributes(helper))] の箇所です。

crates/derive1/src/lib.rs

use proc_macro::TokenStream;
use syn::spanned::Spanned;

#[proc_macro_derive(VariantsFn, attributes(rename))]
pub fn derive_variants_fn(input: TokenStream) -> TokenStream {
    let input = syn::parse_macro_input!(input as syn::DeriveInput);

    let data_enum = if let syn::Data::Enum(data_enum) = &input.data {
        data_enum
    } else {
        return TokenStream::from(
            syn::Error::new(input.span(), "VariantsFn can only be derived for enums")
                .to_compile_error(),
        );
    };

    let enum_ident = input.ident;
    let enum_variant_names = match data_enum
        .variants
        .iter()
        .map(|variant| {
            match variant
                .attrs
                .iter()
                .find(|attr| attr.path().is_ident("rename"))
            {
                None => Ok(variant.ident.to_string()),
                Some(attr) => {
                    let meta_name_value = attr.meta.require_name_value().map_err(|_| {
                        syn::Error::new(attr.span(), "expected `#[rename = \"name\"]` attribute")
                    })?;
                    match &meta_name_value.value {
                        syn::Expr::Lit(syn::ExprLit {
                            lit: syn::Lit::Str(lit_str),
                            ..
                        }) => Ok(lit_str.value()),
                        _ => Err(syn::Error::new(
                            meta_name_value.span(),
                            "expected string literal for rename value",
                        )),
                    }
                }
            }
        })
        .collect::<syn::Result<Vec<String>>>()
    {
        Ok(enum_variant_names) => enum_variant_names,
        Err(e) => return TokenStream::from(e.to_compile_error()),
    };
    let output = quote::quote! {
        impl #enum_ident {
            pub fn variants() -> &'static [&'static str] {
                &[#(#enum_variant_names,)*]
            }
        }
    };

    TokenStream::from(output)
}

まずは肝心な部分。 #[proc_macro_derive(VariantsFn, attributes(rename))]VariantsFn という derive macro の helper attribute として rename を使えるようにしています。繰り返しますが、この 1 行がこの記事のメインです。

あとは #[rename = "..."] と指定できるように処理を変更しています。 variant ごとに属性を調べ、その path が rename のものについて NameValue (name = value) で expr が Lit の Str "..." であればそれを使い、そうでなければエラーを返しています。

このあたりはやりたいことによって変わるので、都度調べればいいのかなと思います。ぼくは syn crate が提供する構造体を調べながら適当に入れました。

個人的には衝突が気になるので #[variants_fn(rename = "...")] みたいな形式にしようかと思ったのですが、例としてはごちゃつくのでボツにしました。

ところで、 proc_macro_derive の helper attribute として rename を追加しないとどうなるのか。もし追加していないと次のようなエラーが表示されます。

error: cannot find attribute `rename` in this scope

属性が他の macro などによって導入されていない場合こういうエラーになります。

おわりに

今回は derive macro helper attributes について書きました。前回のおまけみたいな内容でした。

次回は改めて trybuild crate について書きます。

参考

GitHubで編集を提案
ドクターメイト

Discussion