🦀

proc-macro-workshopの進め方(derive_builder編)

2024/11/04に公開

はじめに

Rustには、煩雑な記述を簡潔にできるマクロという言語機能があります。
マクロには大きく分けて

  • macro_rules!を用いたdeclarative(宣言的)マクロ
  • それ以外のprocedual(手続き的)マクロ

の2種類があり、手続き的マクロはさらに

  • #[derive]マクロ
  • attribute-likeマクロ
  • function-likeマクロ

の3種類に分かれます。

declarativeマクロの代表例としてはvec!マクロがあります。
#[derive]マクロの代表例にはDebugSerializeなどがあります。構造体やenumの前に書くやつです。
attribute-likeマクロの代表例には#[repr(C)]などがあります。構造体やenumの前に書くことができるほか、そのフィールドの前に書くこともできます。deriveそれ自体もattribute-likeマクロの一つです。
function-likeマクロは名前の通り関数のように使えるマクロで、SQL文を解析するマクロなどが紹介されています。

proc-macro-workshopはprocedualマクロの実装の練習ができるリポジトリです。いくつかのコースが用意されており、何段階か用意されたテストをパスするように実装を進めることでマクロの自作ができるようになっています。
https://github.com/dtolnay/proc-macro-workshop

この記事は、「proc-macro-workshopをやってみたいけど進め方がわからない」「やってみたが途中でわからなくなってしまった」といった方向けに、進め方や実装例を示すものです。

準備

以下の環境でやっていきます。

$ cargo --version
cargo 1.82.0 (8f40fc59f 2024-08-21)
$ rustc --version
rustc 1.82.0 (f6e511eec 2024-10-15)

また、proc-macro-workshopは記事執筆時点で最新のもの(390e9d0)を使用します。
まずは先ほどのリポジトリをforkし、それをgit cloneします。

エディタで開けたら、以下のコマンドを実行していくつかクレートを追加します。synクレートはextra-traitsのfeatureを追加します。

$ cargo add -p derive_builder syn -F syn/extra-traits quote proc-macro2

synは入力されたTokenStreamをRustのコードとして解釈するクレートです。本来マクロにはRustのコードでないものも渡せる(冒頭で紹介したsql!()など)のですが、多くの場合はRustのコードを扱うことになるためsynを用います。
同様に、RustのコードからTokenStreamへの変換を容易にするためにquoteクレートとproc-macro2クレートを使用します。

01-parse

builder/tests/progress.rsを開くとコメントアウトされたテストケースがたくさん並んでいます。

// t.pass("tests/01-parse.rs");

と書かれた行のコメントアウトを外してテストを実行すると以下のようなエラーが出ます。これを解消するようにマクロを実装します。

test tests/01-parse.rs ... error
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: proc-macro derive panicked
  --> tests/01-parse.rs:26:10
   |
26 | #[derive(Builder)]
   |          ^^^^^^^
   |
   = help: message: not implemented
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈

builder/tests/01-parse.rsを見ると以下のようなガイドがあります。

https://github.com/dtolnay/proc-macro-workshop/blob/390e9d02ebbb9e3f12177b09f86da12bffd862da/builder/tests/01-parse.rs#L1-L22

要約:
まずは空っぽのTokenStreamを返せるようにしましょう。
それができたら、次に進む前にマクロのinputをsyn::DeriveInputの構文木としてパースするようにしておきましょう。

builder/src/lib.rsに雛形があるのでここに実装を追加していきます。

builder/src/lib.rs
use proc_macro::TokenStream;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _ = input;

    unimplemented!()
}

前半についてはTokenStream::new()を返せばOKです。
後半についてはsynのdocsを見ると方法が書いてあります。よって以下のようにすればOKです。

解答例
builder/src/lib.rs
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let _ = parse_macro_input!(input as DeriveInput);
    TokenStream::new()
}

02-create-builder

builder/tests/progress.rsで2番目のテストのコメントアウトを外します。
2番目のテストケースの中身を見ると次のように書かれています。

https://github.com/dtolnay/proc-macro-workshop/blob/390e9d02ebbb9e3f12177b09f86da12bffd862da/builder/tests/02-create-builder.rs#L1-L34

要約:
Command構造体にbuilderメソッドを実装しましょう。
次に進む前に、CommandBuilder構造体を定義してbuilderメソッドでこれを返すようにしましょう。

ここではマクロが実装された構造体の名前(Command)を取得する必要があります。syn::DeriveInputのパース結果を確認して、これの取得方法を探ります。以下のようにしてパース結果を出力させることができます。この結果は実行時ではなくビルド時に出力されます。

builder/src/lib.rs
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    dbg!(input);
    TokenStream::new()
}
結果(長いので畳んでいます)
DeriveInput {
    attrs: [],
    vis: Visibility::Public(
        Pub,
    ),
    ident: Ident {
        ident: "Command",
        span: #0 bytes(1001..1008),
    },
    generics: Generics {
        lt_token: None,
        params: [],
        gt_token: None,
        where_clause: None,
    },
    data: Data::Struct {
        struct_token: Struct,
        fields: Fields::Named {
            brace_token: Brace,
            named: [
                Field {
                    attrs: [],
                    vis: Visibility::Inherited,
                    mutability: FieldMutability::None,
                    ident: Some(
                        Ident {
                            ident: "executable",
                            span: #0 bytes(1015..1025),
                        },
                    ),
                    colon_token: Some(
                        Colon,
                    ),
                    ty: Type::Path {
                        qself: None,
                        path: Path {
                            leading_colon: None,
                            segments: [
                                PathSegment {
                                    ident: Ident {
                                        ident: "String",
                                        span: #0 bytes(1027..1033),
                                    },
                                    arguments: PathArguments::None,
                                },
                            ],
                        },
                    },
                },
                Comma,
                Field {
                    attrs: [],
                    vis: Visibility::Inherited,
                    mutability: FieldMutability::None,
                    ident: Some(
                        Ident {
                            ident: "args",
                            span: #0 bytes(1039..1043),
                        },
                    ),
                    colon_token: Some(
                        Colon,
                    ),
                    ty: Type::Path {
                        qself: None,
                        path: Path {
                            leading_colon: None,
                            segments: [
                                PathSegment {
                                    ident: Ident {
                                        ident: "Vec",
                                        span: #0 bytes(1045..1048),
                                    },
                                    arguments: PathArguments::AngleBracketed {
                                        colon2_token: None,
                                        lt_token: Lt,
                                        args: [
                                            GenericArgument::Type(
                                                Type::Path {
                                                    qself: None,
                                                    path: Path {
                                                        leading_colon: None,
                                                        segments: [
                                                            PathSegment {
                                                                ident: Ident {
                                                                    ident: "String",
                                                                    span: #0 bytes(1049..1055),
                                                                },
                                                                arguments: PathArguments::None,
                                                            },
                                                        ],
                                                    },
                                                },
                                            ),
                                        ],
                                        gt_token: Gt,
                                    },
                                },
                            ],
                        },
                    },
                },
                Comma,
                Field {
                    attrs: [],
                    vis: Visibility::Inherited,
                    mutability: FieldMutability::None,
                    ident: Some(
                        Ident {
                            ident: "env",
                            span: #0 bytes(1062..1065),
                        },
                    ),
                    colon_token: Some(
                        Colon,
                    ),
                    ty: Type::Path {
                        qself: None,
                        path: Path {
                            leading_colon: None,
                            segments: [
                                PathSegment {
                                    ident: Ident {
                                        ident: "Vec",
                                        span: #0 bytes(1067..1070),
                                    },
                                    arguments: PathArguments::AngleBracketed {
                                        colon2_token: None,
                                        lt_token: Lt,
                                        args: [
                                            GenericArgument::Type(
                                                Type::Path {
                                                    qself: None,
                                                    path: Path {
                                                        leading_colon: None,
                                                        segments: [
                                                            PathSegment {
                                                                ident: Ident {
                                                                    ident: "String",
                                                                    span: #0 bytes(1071..1077),
                                                                },
                                                                arguments: PathArguments::None,
                                                            },
                                                        ],
                                                    },
                                                },
                                            ),
                                        ],
                                        gt_token: Gt,
                                    },
                                },
                            ],
                        },
                    },
                },
                Comma,
                Field {
                    attrs: [],
                    vis: Visibility::Inherited,
                    mutability: FieldMutability::None,
                    ident: Some(
                        Ident {
                            ident: "current_dir",
                            span: #0 bytes(1084..1095),
                        },
                    ),
                    colon_token: Some(
                        Colon,
                    ),
                    ty: Type::Path {
                        qself: None,
                        path: Path {
                            leading_colon: None,
                            segments: [
                                PathSegment {
                                    ident: Ident {
                                        ident: "String",
                                        span: #0 bytes(1097..1103),
                                    },
                                    arguments: PathArguments::None,
                                },
                            ],
                        },
                    },
                },
                Comma,
            ],
        },
        semi_token: None,
    },
}

Command構造体の定義を変えて何度か試すと、それぞれのフィールドがどのような情報を表しているのか見えてくると思います。

これを見ると、構造体の名前はinput.identで取り出せそうです。また、input.vispubかどうかを、input.genericsでジェネリクスを取得できそうです。よってbuilderメソッドの実装は以下のようにできます。quoteマクロの中では変数名の先頭に#をつけることで展開することができます。

builder/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let DeriveInput {
        ident,
        vis,
        generics,
        ..
    } = input;

    let expanded = quote! {
        impl #generics #ident #generics {
            #vis fn builder() {}
        }
    };

    TokenStream::from(expanded)
}

これでテストは通るのですが、次に進む前にCommandBuilder構造体を定義できるようにしておきます。Commandという構造体名からCommandBuilderという構造体名を作る必要がありますが、これは

let builder_ident = format_ident!("{}Builder", ident);

とすると実現できます。format_identマクロはquoteクレートに含まれています。
現時点ではbuilderのフィールド定義はハードコードで良いので、全体は以下のようになります。

解答例
builder/src/lib.rs
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let DeriveInput {
        ident,
        vis,
        generics,
        ..
    } = input;

    let builder_ident = format_ident!("{}Builder", ident);

    let expanded = quote! {
        impl #generics #ident #generics {
            #vis fn builder() -> #builder_ident #generics {
                #builder_ident {
                    executable: None,
                    args: None,
                    env: None,
                    current_dir: None,
                }
            }
        }

        #vis struct #builder_ident #generics {
            executable: Option<String>,
            args: Option<Vec<String>>,
            env: Option<Vec<String>>,
            current_dir: Option<String>,
        }
    };

    TokenStream::from(expanded)
}

03-call-setters

builderに、それぞれのフィールドに対応するsetterのメソッドを実装していきます。

Command構造体のフィールド定義を取得する必要がありますが、これはinput.dataで取り出せそうです。もしこのマクロがenumに対して実装された場合や、無名のフィールドを持つ構造体(struct Foo(i32)など)に対して実装された場合はコンパイルエラーになるようにしておきます。

let DeriveInput {
    ident,
    vis,
    generics,
    data,
    ..
} = input;

let fields = match data {
    Data::Struct(DataStruct {
        fields: Fields::Named(FieldsNamed { named, .. }),
        ..
    }) => named,
    _ => {
        return quote! {
            compile_error!("Builder derive only works on structs with named fields");
        }
        .into();
    }
};

let builder_field_definitions = fields.iter().map(|field| {
    ..
});

また、builder_field_definitionsTokenStreamをアイテムとするイテレータであるとき、quote!マクロ内では

#vis struct #builder_ident #generics {
    #(#builder_field_definitions)*
}

のようにしてイテレータを展開できます。

したがって、以下のようにするとテストが通ります。

解答例
builder/src/lib.rs
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::*;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let DeriveInput {
        ident,
        vis,
        generics,
        data,
        ..
    } = input;

    let builder_ident = format_ident!("{}Builder", ident);

    let fields = match data {
        Data::Struct(DataStruct {
            fields: Fields::Named(FieldsNamed { named, .. }),
            ..
        }) => named,
        _ => {
            return quote! {
                compile_error!("Builder derive only works on structs with named fields");
            }
            .into();
        }
    };

    let builder_defaults = fields.iter().map(|field| {
        let field_name = &field.ident;
        quote! {
            #field_name: None,
        }
    });

    let builder_field_definitions = fields.iter().map(|field| {
        let field_name = &field.ident;
        let ty = &field.ty;
        quote! {
            #field_name: Option<#ty>,
        }
    });

    let builder_setters = fields.iter().map(|field| {
        let field_name = &field.ident;
        let ty = &field.ty;
        quote! {
            #vis fn #field_name(&mut self, #field_name: #ty) -> &mut Self {
                self.#field_name = Some(#field_name);
                self
            }
        }
    });

    let expanded = quote! {
        impl #generics #ident #generics {
            #vis fn builder() -> #builder_ident #generics {
                #builder_ident {
                    #(#builder_defaults)*
                }
            }
        }

        #vis struct #builder_ident #generics {
            #(#builder_field_definitions)*
        }

        impl #generics #builder_ident #generics {
            #(#builder_setters)*
        }
    };

    TokenStream::from(expanded)
}

04-call-build, 05-method-chaining

impl CommandBuilder {
    pub fn build(&mut self) -> Result<Command, Box<dyn Error>> {
        ...
    }
}

のようなメソッドを定義してbuilderを元の構造体に変換できるようにします。
Result<_, Box<dyn Error>>について軽くおさらいしておくと、

#[derive(Debug)]
struct MyError {
    message: String
}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "MyError: {}", self.message)
    }
}

impl std::error::Error for MyError {}

fn foo() -> Result<i32, Box<dyn Error>> {
    let a = Some(42i32);
    a.ok_or_else(|| MyError { message: "error".into() }.into())
}

のようにしてErrorトレイトを実装したものならなんでも入るようなエラーを返すことができます(synクレートにもResultErrorといった型があり紛らわしいためここではあえてフルパスを記述しています)。

今回はMyErrorに相当する構造体としてStringを用います。builder構造体から値を取り出す際は

#field_ident: self.#field_ident
    .clone()
    .ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,

のようにしてOption<T>Result<T, Box<dyn Error>>に変換します。最後の行は

ok_or(Box<dyn std::error::Error> { #message.into() })?

でもほぼ同じ挙動が得られますが、ok_or_elseが遅延評価なのに対しok_orは即時評価なので、ok_or_elseを使うことが推奨されています。

解答例
builder/src/lib.rs
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::*;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let DeriveInput {
        ident,
        vis,
        generics,
        data,
        ..
    } = input;

    let builder_ident = format_ident!("{}Builder", ident);

    let fields = match data {
        Data::Struct(DataStruct {
            fields: Fields::Named(FieldsNamed { named, .. }),
            ..
        }) => named,
        _ => {
            return quote! {
                compile_error!("Builder derive only works on structs with named fields");
            }
            .into();
        }
    };

    let builder_defaults = fields.iter().map(|field| {
        let field_name = &field.ident;
        quote! {
            #field_name: None,
        }
    });

    let builder_field_definitions = fields.iter().map(|field| {
        let field_name = &field.ident;
        let ty = &field.ty;
        quote! {
            #field_name: Option<#ty>,
        }
    });

    let builder_setters = fields.iter().map(|field| {
        let field_name = &field.ident;
        let ty = &field.ty;
        quote! {
            #vis fn #field_name(&mut self, #field_name: #ty) -> &mut Self {
                self.#field_name = Some(#field_name);
                self
            }
        }
    });

    let build_attrs = fields.iter().map(|field| {
        let field_ident = field.ident.clone().unwrap();
        let message = format!("field {} isn't set", field_ident);
        quote! {
            #field_ident: self.#field_ident
                .clone()
                .ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,
        }
    });

    let expanded = quote! {
        impl #generics #ident #generics {
            #vis fn builder() -> #builder_ident #generics {
                #builder_ident {
                    #(#builder_defaults)*
                }
            }
        }

        #vis struct #builder_ident #generics {
            #(#builder_field_definitions)*
        }

        impl #generics #builder_ident #generics {
            #(#builder_setters)*
            #vis fn build(&mut self) -> Result<#ident #generics, Box<dyn std::error::Error>> {
                Ok(#ident {
                    #(#build_attrs)*
                })
            }
        }
    };

    TokenStream::from(expanded)
}

06-optional-field

そろそろderive関数が長くなってきたので分割をします。fields.iter().map(..)という記述が何度も繰り返されているのでこれを一つにまとめます。quote! {}で作成したTokenStreamを返す関数の型を記述する必要がありますが、このTokenStreamproc_macro::TokenStreamではなくproc_macro2::TokenStreamであるため注意してください。

リファクタ例
builder/src/lib.rs
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::*;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let DeriveInput {
        ident,
        vis,
        generics,
        data,
        ..
    } = input;

    let builder_ident = format_ident!("{}Builder", ident);

    let fields = match data {
        Data::Struct(DataStruct {
            fields: Fields::Named(FieldsNamed { named, .. }),
            ..
        }) => named,
        _ => {
            return quote! {
                compile_error!("Builder derive only works on structs with named fields");
            }
            .into();
        }
    };

    let mut builder_defaults = Vec::with_capacity(fields.len());
    let mut builder_field_definitions = Vec::with_capacity(fields.len());
    let mut builder_setters = Vec::with_capacity(fields.len());
    let mut build_attrs = Vec::with_capacity(fields.len());

    for field in &fields {
        builder_defaults.push(builder_default(field));
        builder_field_definitions.push(builder_field_definition(field));
        builder_setters.push(builder_setter(field, &vis));
        build_attrs.push(build_attr(field));
    }

    let expanded = quote! {
        impl #generics #ident #generics {
            #vis fn builder() -> #builder_ident #generics {
                #builder_ident {
                    #(#builder_defaults)*
                }
            }
        }

        #vis struct #builder_ident #generics {
            #(#builder_field_definitions)*
        }

        impl #generics #builder_ident #generics {
            #(#builder_setters)*
            #vis fn build(&mut self) -> Result<#ident #generics, Box<dyn std::error::Error>> {
                Ok(#ident {
                    #(#build_attrs)*
                })
            }
        }
    };

    TokenStream::from(expanded)
}

fn builder_default(Field { ident, .. }: &Field) -> TokenStream2 {
    quote! {
        #ident: None,
    }
}

fn builder_field_definition(Field { ident, ty, .. }: &Field) -> TokenStream2 {
    quote! {
        #ident: Option<#ty>,
    }
}

fn builder_setter(Field { ident, ty, .. }: &Field, vis: &Visibility) -> TokenStream2 {
    quote! {
        #vis fn #ident(&mut self, #ident: #ty) -> &mut Self {
            self.#ident = Some(#ident);
            self
        }
    }
}

fn build_attr(Field { ident, .. }: &Field) -> TokenStream2 {
    let ident = ident.clone().unwrap();
    let message = format!("field {} isn't set", ident);
    quote! {
        #ident: self.#ident
            .clone()
            .ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,
    }
}

この章では、もとの構造体のフィールドにOption<T>がある場合はそのフィールドの指定をoptionalにします。builder構造体のフィールド定義はこのままだとOption<Option<T>>になりますが、値がSome(None)になることはないのでOption<Option<T>>ではなくOption<T>になるようにします。したがってやるべきことは

  • builder構造体の定義を変更する
  • それにあわせてsetterの定義も変更する
  • それにあわせてbuildメソッドの定義も変更する

の3点です。まずはフィールドがOption<T>かどうか判定する方法から考えます。
テストケースにあるcurrent_dir: Option<String>のフィールドは以下のようにパースされます。

ty: Type::Path {
    qself: None,
    path: Path {
        leading_colon: None,
        segments: [
            PathSegment {
                ident: Ident {
                    ident: "Option",
                    span: #0 bytes(2884..2890),
                },
                arguments: PathArguments::AngleBracketed {
                    colon2_token: None,
                    lt_token: Lt,
                    args: [
                        GenericArgument::Type(
                            Type::Path {
                                qself: None,
                                path: Path {
                                    leading_colon: None,
                                    segments: [
                                        PathSegment {
                                            ident: Ident {
                                                ident: "String",
                                                span: #0 bytes(2891..2897),
                                            },
                                            arguments: PathArguments::None,
                                        },
                                    ],
                                },
                            },
                        ),
                    ],
                    gt_token: Gt,
                },
            },
        ],
    },
},

したがって次のようにして判定できます。

#[derive(Debug)]
enum FieldType<'a> {
    Optional(&'a Type),
    Required(&'a Type),
}

fn field_type(Field { ty, .. }: &Field) -> FieldType {
    if let Type::Path(TypePath {
        path: Path { segments, .. },
        ..
    }) = ty
    {
        if let Some(PathSegment {
            ident,
            arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
        }) = segments.first()
        {
            if ident == "Option" {
                if let Some(GenericArgument::Type(ty)) = args.first() {
                    return FieldType::Optional(ty);
                }
            }
        }
    }
    return FieldType::Required(ty);
}

builder構造体の定義変更は以下のようにできます。

fn builder_field_definition(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
    let ty = match field_type {
        &FieldType::Optional(ty) => ty,
        &FieldType::Required(ty) => ty,
    };
    quote! {
        #ident: Option<#ty>,
    }
}

setterおよびbuildメソッドも同様に変更できます。

解答例
builder/src/lib.rs
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::*;

#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let DeriveInput {
        ident,
        vis,
        generics,
        data,
        ..
    } = input;

    let builder_ident = format_ident!("{}Builder", ident);

    let fields = match data {
        Data::Struct(DataStruct {
            fields: Fields::Named(FieldsNamed { named, .. }),
            ..
        }) => named,
        _ => {
            return quote! {
                compile_error!("Builder derive only works on structs with named fields");
            }
            .into();
        }
    };

    let mut builder_defaults = Vec::with_capacity(fields.len());
    let mut builder_field_definitions = Vec::with_capacity(fields.len());
    let mut builder_setters = Vec::with_capacity(fields.len());
    let mut build_attrs = Vec::with_capacity(fields.len());

    for field in &fields {
        let ft = field_type(field);
        builder_defaults.push(builder_default(field));
        builder_field_definitions.push(builder_field_definition(field, &ft));
        builder_setters.push(builder_setter(field, &vis, &ft));
        build_attrs.push(build_attr(field, &ft));
    }

    let expanded = quote! {
        impl #generics #ident #generics {
            #vis fn builder() -> #builder_ident #generics {
                #builder_ident {
                    #(#builder_defaults)*
                }
            }
        }

        #vis struct #builder_ident #generics {
            #(#builder_field_definitions)*
        }

        impl #generics #builder_ident #generics {
            #(#builder_setters)*
            #vis fn build(&mut self) -> Result<#ident #generics, Box<dyn std::error::Error>> {
                Ok(#ident {
                    #(#build_attrs)*
                })
            }
        }
    };

    TokenStream::from(expanded)
}

fn builder_default(Field { ident, .. }: &Field) -> TokenStream2 {
    quote! {
        #ident: None,
    }
}

fn builder_field_definition(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
    let ty = match field_type {
        &FieldType::Optional(ty) => ty,
        &FieldType::Required(ty) => ty,
    };
    quote! {
        #ident: Option<#ty>,
    }
}

fn builder_setter(
    Field { ident, .. }: &Field,
    vis: &Visibility,
    field_type: &FieldType,
) -> TokenStream2 {
    let ty = match field_type {
        &FieldType::Optional(ty) => ty,
        &FieldType::Required(ty) => ty,
    };
    quote! {
        #vis fn #ident(&mut self, #ident: #ty) -> &mut Self {
            self.#ident = Some(#ident);
            self
        }
    }
}

fn build_attr(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
    match field_type {
        FieldType::Optional(_) => quote! {
            #ident: self.#ident.clone(),
        },
        FieldType::Required(_) => {
            let ident = ident.clone().unwrap();
            let message = format!("field {} isn't set", ident);
            quote! {
                #ident: self.#ident
                    .clone()
                    .ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,
            }
        }
    }
}

#[derive(Debug)]
enum FieldType<'a> {
    Optional(&'a Type),
    Required(&'a Type),
}

fn field_type(Field { ty, .. }: &Field) -> FieldType {
    if let Type::Path(TypePath {
        path: Path { segments, .. },
        ..
    }) = ty
    {
        if let Some(PathSegment {
            ident,
            arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
        }) = segments.first()
        {
            if ident == "Option" {
                if let Some(GenericArgument::Type(ty)) = args.first() {
                    return FieldType::Optional(ty);
                }
            }
        }
    }
    return FieldType::Required(ty);
}

07-repeated-field, 08-unrecognized-attribute

フィールド定義の上に#[builder(each = "...")]というattributeがある場合、フィールドの型がVec<T>であると仮定してattributeで指定した名前のメソッドで要素を一つずつ追加できるようにします。
まず、コンパイラがbuilderattributeを認識できるよう、以下のようにします。

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
    ..
}

#[builder(each = "...")]というattributeがあるかどうかはfield.attrsを見ればわかりそうです。そもそもbuilderattributeがない場合Noneを、attributeがあるが構文が間違っている場合はSome(Err(err))を、正しく記述されている場合はSome(Ok(ident))を返すような関数を定義します。なお、次のコードのResult<T>syn::Result<T>です。

fn each_attr(attrs: &Vec<Attribute>) -> Option<Result<Ident>> {
    todo!()
}

argsフィールドは以下のような構造になっています。ここから"arg"を拾い上げられるように実装します。

Attribute {
    pound_token: Pound,
    style: AttrStyle::Outer,
    bracket_token: Bracket,
    meta: Meta::List {
        path: Path {
            leading_colon: None,
            segments: [
                PathSegment {
                    ident: Ident {
                        ident: "builder",
                        span: #0 bytes(1417..1424),
                    },
                    arguments: PathArguments::None,
                },
            ],
        },
        delimiter: MacroDelimiter::Paren(
            Paren,
        ),
        tokens: TokenStream [
            Ident {
                ident: "each",
                span: #0 bytes(1425..1429),
            },
            Punct {
                ch: '=',
                spacing: Alone,
                span: #0 bytes(1430..1431),
            },
            Literal {
                kind: Str,
                symbol: "arg",
                suffix: None,
                span: #0 bytes(1432..1437),
            },
        ],
    },
}

ひたすらパターンマッチで拾うよりも、Attribute型のparse_nested_metaメソッドを使うとより簡潔に実装できます。docs.rsに今回実装したいattributeと全く同じ形式の例があります。

エラーメッセージの内容も指定されているので、それに従います。エラーのspanを正しく設定する方法がわからない場合、docs.rsを見るとよいでしょう。

以下に実装例を示します。

fn each_attr(attrs: &Vec<Attribute>) -> Option<Result<Ident>> {
    let mut each: Option<Result<Ident>> = None;

    for attr in attrs {
        if !attr.path().is_ident("builder") {
            continue;
        }
        if let Meta::List(meta_list) = &attr.meta {
            let _ = attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("each") {
                    let value = meta.value()?;
                    let s: LitStr = value.parse()?;
                    each = Some(Ok(format_ident!("{}", s.value())));
                } else {
                    each = Some(Err(Error::new_spanned(meta_list, "expected `builder(each = \"...\")`")));
                }
                Ok(())
            });
        }
    }

    each
}

あわせて、先ほど作成したenum FieldTypeも変更します。

 #[derive(Debug)]
 enum FieldType<'a> {
     Optional(&'a Type),
     Required(&'a Type),
+    Repeated(&'a Type, Ident),
 }

field_type関数の戻り値もResult<T>で囲い、each_attrからのエラーをバケツリレーします。

fn field_type<'a>(Field { ty, attrs, .. }: &'a Field) -> Result<FieldType<'a>> {
    if let Type::Path(TypePath {
        path: Path { segments, .. },
        ..
    }) = ty
    {
        if let Some(PathSegment {
            ident,
            arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
        }) = segments.first()
        {
            if ident == "Option" {
                if let Some(GenericArgument::Type(ty)) = args.first() {
                    return Ok(FieldType::Optional(ty));
                }
            } else if ident == "Vec" {
                if let Some(each) = each_attr(&attrs) {
                    if let Some(GenericArgument::Type(ty)) = args.first() {
                        return Ok(FieldType::Repeated(ty, each?));
                    }
                }
            }
        }
    }
    return Ok(FieldType::Required(ty));
}

derive関数内では、field_type()関数がErrを返したら即座にコンパイルエラーになるようにします。

for field in &fields {
    match field_type(field) {
        Ok(field_type) => {
            builder_defaults.push(builder_default(field));
            builder_field_definitions.push(builder_field_definition(field, &field_type));
            builder_setters.push(builder_setter(field, &vis, &field_type));
            build_attrs.push(build_attr(field, &field_type));
        }
        Err(err) => return err.to_compile_error().into(),
    }
}

最後に、builder_setterなど他のメソッドでFieldType::Repeatedの分岐を実装すればOKです。

解答例
builder/src/lib.rs
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::*;

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let DeriveInput {
        ident,
        vis,
        generics,
        data,
        ..
    } = input;

    let builder_ident = format_ident!("{}Builder", ident);

    let fields = match data {
        Data::Struct(DataStruct {
            fields: Fields::Named(FieldsNamed { named, .. }),
            ..
        }) => named,
        _ => {
            return quote! {
                compile_error!("Builder derive only works on structs with named fields");
            }
            .into();
        }
    };

    let mut builder_defaults = Vec::with_capacity(fields.len());
    let mut builder_field_definitions = Vec::with_capacity(fields.len());
    let mut builder_setters = Vec::with_capacity(fields.len());
    let mut build_attrs = Vec::with_capacity(fields.len());

    for field in &fields {
        match field_type(field) {
            Ok(field_type) => {
                builder_defaults.push(builder_default(field));
                builder_field_definitions.push(builder_field_definition(field, &field_type));
                builder_setters.push(builder_setter(field, &vis, &field_type));
                build_attrs.push(build_attr(field, &field_type));
            }
            Err(err) => return err.to_compile_error().into(),
        }
    }

    let expanded = quote! {
        impl #generics #ident #generics {
            #vis fn builder() -> #builder_ident #generics {
                #builder_ident {
                    #(#builder_defaults)*
                }
            }
        }

        #vis struct #builder_ident #generics {
            #(#builder_field_definitions)*
        }

        impl #generics #builder_ident #generics {
            #(#builder_setters)*
            #vis fn build(&mut self) -> Result<#ident #generics, Box<dyn std::error::Error>> {
                Ok(#ident {
                    #(#build_attrs)*
                })
            }
        }
    };

    TokenStream::from(expanded)
}

fn builder_default(Field { ident, .. }: &Field) -> TokenStream2 {
    quote! {
        #ident: None,
    }
}

fn builder_field_definition(
    Field { ident, ty, .. }: &Field,
    field_type: &FieldType,
) -> TokenStream2 {
    let ty = match field_type {
        &FieldType::Optional(ty) => ty,
        &FieldType::Required(ty) => ty,
        &FieldType::Repeated(_, _) => ty,
    };
    quote! {
        #ident: Option<#ty>,
    }
}

fn builder_setter(
    Field { ident, .. }: &Field,
    vis: &Visibility,
    field_type: &FieldType,
) -> TokenStream2 {
    match field_type {
        FieldType::Optional(ty) | FieldType::Required(ty) => quote! {
            #vis fn #ident(&mut self, #ident: #ty) -> &mut Self {
                self.#ident = Some(#ident);
                self
            }
        },
        FieldType::Repeated(ty, each) => quote! {
            #vis fn #each(&mut self, #each: #ty) -> &mut Self {
                self.#ident.get_or_insert_with(Vec::new).push(#each);
                self
            }
        },
    }
}

fn build_attr(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
    match field_type {
        FieldType::Optional(_) => quote! {
            #ident: self.#ident.clone(),
        },
        FieldType::Required(_) => {
            let ident = ident.clone().unwrap();
            let message = format!("field {} isn't set", ident);
            quote! {
                #ident: self.#ident
                    .clone()
                    .ok_or_else(|| -> Box<dyn std::error::Error> { #message.into() })?,
            }
        }
        FieldType::Repeated(_, _) => quote! {
            #ident: self.#ident.clone().unwrap_or_default(),
        },
    }
}

#[derive(Debug)]
enum FieldType<'a> {
    Optional(&'a Type),
    Required(&'a Type),
    Repeated(&'a Type, Ident),
}

fn field_type<'a>(Field { ty, attrs, .. }: &'a Field) -> Result<FieldType<'a>> {
    if let Type::Path(TypePath {
        path: Path { segments, .. },
        ..
    }) = ty
    {
        if let Some(PathSegment {
            ident,
            arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
        }) = segments.first()
        {
            if ident == "Option" {
                if let Some(GenericArgument::Type(ty)) = args.first() {
                    return Ok(FieldType::Optional(ty));
                }
            } else if ident == "Vec" {
                if let Some(each) = each_attr(&attrs) {
                    if let Some(GenericArgument::Type(ty)) = args.first() {
                        return Ok(FieldType::Repeated(ty, each?));
                    }
                }
            }
        }
    }
    return Ok(FieldType::Required(ty));
}

fn each_attr(attrs: &Vec<Attribute>) -> Option<Result<Ident>> {
    let mut each: Option<Result<Ident>> = None;

    for attr in attrs {
        if !attr.path().is_ident("builder") {
            continue;
        }
        if let Meta::List(meta_list) = &attr.meta {
            let _ = attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("each") {
                    let value = meta.value()?;
                    let s: LitStr = value.parse()?;
                    each = Some(Ok(format_ident!("{}", s.value())));
                } else {
                    each = Some(Err(Error::new_spanned(
                        meta_list,
                        "expected `builder(each = \"...\")`",
                    )));
                }
                Ok(())
            });
        }
    }

    each
}

09-redefined-prelude-types

いよいよ最終問題です。Optionなどの型が上書きされていてもマクロが壊れないようにします。
とはいってもやるべきことは簡単で、問題文にもほとんど答えのようなものが書かれています。

https://github.com/dtolnay/proc-macro-workshop/blob/390e9d02ebbb9e3f12177b09f86da12bffd862da/builder/tests/09-redefined-prelude-types.rs#L13-L15

意訳:
一般に、proceduralかdeclarativeかを問わず他人に使われることを想定した全てのマクロは、展開されるコードに含まれる一切を絶対パスで記述すべきである。例えば、std::result::Resultのように。

解答例
builder/src/lib.rs
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::*;

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let DeriveInput {
        ident,
        vis,
        generics,
        data,
        ..
    } = input;

    let builder_ident = format_ident!("{}Builder", ident);

    let fields = match data {
        Data::Struct(DataStruct {
            fields: Fields::Named(FieldsNamed { named, .. }),
            ..
        }) => named,
        _ => {
            return quote! {
                compile_error!("Builder derive only works on structs with named fields");
            }
            .into();
        }
    };

    let mut builder_defaults = Vec::with_capacity(fields.len());
    let mut builder_field_definitions = Vec::with_capacity(fields.len());
    let mut builder_setters = Vec::with_capacity(fields.len());
    let mut build_attrs = Vec::with_capacity(fields.len());

    for field in &fields {
        match field_type(field) {
            Ok(field_type) => {
                builder_defaults.push(builder_default(field));
                builder_field_definitions.push(builder_field_definition(field, &field_type));
                builder_setters.push(builder_setter(field, &vis, &field_type));
                build_attrs.push(build_attr(field, &field_type));
            }
            Err(err) => return err.to_compile_error().into(),
        }
    }

    let expanded = quote! {
        impl #generics #ident #generics {
            #vis fn builder() -> #builder_ident #generics {
                #builder_ident {
                    #(#builder_defaults)*
                }
            }
        }

        #vis struct #builder_ident #generics {
            #(#builder_field_definitions)*
        }

        impl #generics #builder_ident #generics {
            #(#builder_setters)*
            #vis fn build(&mut self) -> std::result::Result<#ident #generics, std::boxed::Box<dyn std::error::Error>> {
                std::result::Result::Ok(#ident {
                    #(#build_attrs)*
                })
            }
        }
    };

    TokenStream::from(expanded)
}

fn builder_default(Field { ident, .. }: &Field) -> TokenStream2 {
    quote! {
        #ident: std::option::Option::None,
    }
}

fn builder_field_definition(
    Field { ident, ty, .. }: &Field,
    field_type: &FieldType,
) -> TokenStream2 {
    let ty = match field_type {
        &FieldType::Optional(ty) => ty,
        &FieldType::Required(ty) => ty,
        &FieldType::Repeated(_, _) => ty,
    };
    quote! {
        #ident: std::option::Option<#ty>,
    }
}

fn builder_setter(
    Field { ident, .. }: &Field,
    vis: &Visibility,
    field_type: &FieldType,
) -> TokenStream2 {
    match field_type {
        FieldType::Optional(ty) | FieldType::Required(ty) => quote! {
            #vis fn #ident(&mut self, #ident: #ty) -> &mut Self {
                self.#ident = std::option::Option::Some(#ident);
                self
            }
        },
        FieldType::Repeated(ty, each) => quote! {
            #vis fn #each(&mut self, #each: #ty) -> &mut Self {
                self.#ident.get_or_insert_with(std::vec::Vec::new).push(#each);
                self
            }
        },
    }
}

fn build_attr(Field { ident, .. }: &Field, field_type: &FieldType) -> TokenStream2 {
    match field_type {
        FieldType::Optional(_) => quote! {
            #ident: self.#ident.clone(),
        },
        FieldType::Required(_) => {
            let ident = ident.clone().unwrap();
            let message = format!("field {} isn't set", ident);
            quote! {
                #ident: self.#ident
                    .clone()
                    .ok_or_else(|| -> std::boxed::Box<dyn std::error::Error> { #message.into() })?,
            }
        }
        FieldType::Repeated(_, _) => quote! {
            #ident: self.#ident.clone().unwrap_or_default(),
        },
    }
}

#[derive(Debug)]
enum FieldType<'a> {
    Optional(&'a Type),
    Required(&'a Type),
    Repeated(&'a Type, Ident),
}

fn field_type<'a>(Field { ty, attrs, .. }: &'a Field) -> Result<FieldType<'a>> {
    if let Type::Path(TypePath {
        path: Path { segments, .. },
        ..
    }) = ty
    {
        if let Some(PathSegment {
            ident,
            arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
        }) = segments.first()
        {
            if ident == "Option" {
                if let Some(GenericArgument::Type(ty)) = args.first() {
                    return Ok(FieldType::Optional(ty));
                }
            } else if ident == "Vec" {
                if let Some(each) = each_attr(&attrs) {
                    if let Some(GenericArgument::Type(ty)) = args.first() {
                        return Ok(FieldType::Repeated(ty, each?));
                    }
                }
            }
        }
    }
    return Ok(FieldType::Required(ty));
}

fn each_attr(attrs: &Vec<Attribute>) -> Option<Result<Ident>> {
    let mut each: Option<Result<Ident>> = None;

    for attr in attrs {
        if !attr.path().is_ident("builder") {
            continue;
        }
        if let Meta::List(meta_list) = &attr.meta {
            let _ = attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("each") {
                    let value = meta.value()?;
                    let s: LitStr = value.parse()?;
                    each = Some(Ok(format_ident!("{}", s.value())));
                } else {
                    each = Some(Err(Error::new_spanned(
                        meta_list,
                        "expected `builder(each = \"...\")`",
                    )));
                }
                Ok(())
            });
        }
    }

    each
}

最後に

お疲れ様でした。これであなたもマクロが怖くなくなったはずです。poc-macro-workshopにはこれ以外にもたくさんの練習問題が用意されているので、興味がある方はぜひ挑戦してみてください。

Discussion