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 について書きます。
参考
- Procedural Macros - The Rust Reference https://doc.rust-lang.org/reference/procedural-macros.html
- Derive - The Rust Reference https://doc.rust-lang.org/reference/attributes/derive.html
- syn - crates.io: Rust Package Registry https://crates.io/crates/syn
Discussion