本記事ではdtolnay先生のproc-macro-workshopのbuilderをやっていきます。
workshopはprocedural macrosの学習を目的に作られており、workshop内の各projectをstep by stepで実装していくことで自然とprocedural macrosに慣れることができます。 本workshopの存在自体は知っていたのですが途中までしかできていない状態でした。
そんな中で今働いているfraimという会社でRust勉強会が始まり、そこで週一回少しづつ進めていく機会を得ました。(副業の募集もあるので仕事でRustに触れたい方等おられましたら是非募集を覗いてみてください)
workshopの進め方
workshopの進め方について。
まず、proc-macro-workshopをfork(optional), cloneしてきます。
projectのrootに各workshopのprojectが用意されています。本記事ではbuilderをおこなっていきますので、builderにcdします。
すると、tests/progress.rsというファイルが用意されており、このファイルを編集しながら実装を進めていきます。
#[test]
fn tests() {
let t = trybuild::TestCases::new();
//t.pass("tests/01-parse.rs");
//t.pass("tests/02-create-builder.rs");
//t.pass("tests/03-call-setters.rs");
//t.pass("tests/04-call-build.rs");
//t.pass("tests/05-method-chaining.rs");
//t.pass("tests/06-optional-field.rs");
//t.pass("tests/07-repeated-field.rs");
//t.compile_fail("tests/08-unrecognized-attribute.rs");
//t.pass("tests/09-redefined-prelude-types.rs");
}最初はt.pass()がコメントアウトされており、一つずつコメントアウトを外しながら進めていきます。
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/01-parse.rs");
// ...
}01-parse
まずは01-parse.rsを有効にします。
01-parse.rsは以下の内容になっています。(commentは割愛)
use derive_builder::Builder;
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: String,
}
fn main() {}定義されているCommandstructにderive(Builder)が宣言されています。この様に任意のstructにderiveしてbuilderを生成するのが本projectの目標です。
01-parse.rsではbuilderの処理がよびだされていないのでとりあえずcompileを通せばよいだけです。 肝心のbuilderの実装はsrc/lib.rsに書いていきます。
lib.rsは初期状態では以下のようになっています。
use proc_macro::TokenStream;
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _ = input;
unimplemented!()
}この状態でcargo test --quietを実行するとnot implementedとなり失敗します。
❯ cargo test --quiet
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.12s
test tests/01-parse.rs ... error
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: proc-macro derive panicked
--> tests/01-parse.rs:26:10
|
26 | #[derive(Builder)]
| ^^^^^^^
|
= help: message: not implemented
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈今回はcompileさえ通せばよいので空のTokenStreamを返してみます。
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let _ = input;
TokenStream::new()
}❯ cargo test --quiet
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
Compiling derive_builder v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/builder)
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
test tests/01-parse.rs ... okこれで01-parse.rsをパスできました🎉
基本的な流れは後は同じでtestを通しながらBuilderを作っていきます。
02-create-builder
fn tests() {
let t = trybuild::TestCases::new();
// ...
t.pass("tests/02-create-builder.rs");
// ...
}02-create-builder.rsは以下のようになっています。
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: String,
}
fn main() {
let builder = Command::builder();
let _ = builder;
}Commandにbuildermethodを生成し、CommandBuilder型を返せばよさそうです。
生成するコードのイメージとしては
struct CommandBuilder {
// field definitions...
}
impl Command {
fn builder(&self) -> CommandBuilder {
CommandBuilder {
// initial fields...
}
}
}という感じになりそうです。具体的には
CommandBuilderのstruct定義CommandにbuildermethodをimplCommandBuilderの初期化処理
を実装します。実装に着手する前に必要な依存を追加します。
cargo add syn quote proc-macro2を実行します。
[dependencies]
proc-macro2 = "1.0.46"
quote = "1.0.21"
syn = { version = "1.0.102", features = ["full", "extra-traits", "parsing"] }testではbuilderに特に処理が必要ないので空のbuilderを生成するコードを目指します。
最初は以下のようになりました。
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{Data, DataStruct, DeriveInput, Fields, FieldsNamed, Result, parse_macro_input, Error};
use syn::spanned::Spanned;
use proc_macro2::TokenStream as TokenStream2;
#[proc_macro_derive(Builder)]
pub fn derive(input: TokenStream) -> TokenStream {
let input: DeriveInput = parse_macro_input!(input as DeriveInput);
match derive_builder(input) {
Ok(token_stream) => TokenStream::from(token_stream),
Err(err) => TokenStream::from(err.to_compile_error())
}
}
fn derive_builder(input: DeriveInput) -> Result<TokenStream2> {
if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }), ..
}) = input.data {
let _ = named;
let ident = input.ident;
let builder = format_ident!("{}Builder",ident);
Ok(quote! {
struct #builder {}
impl #ident {
fn builder() -> #builder {
#builder {}
}
}
})
} else {
Err(Error::new(input.span(), "Only struct supported"))
}
}procedural macroの最初の課題はsyn(とquote)のapiに慣れることにあると思います。
基本的にTokenStreamを直接扱うことはせず、synでRustの型情報を表す型にparseします。今回はderiveに書かれる前提なのでDeriveInput型にparseします。
そこから必要な情報を抽出して、quote!でTokenStreamを組み立てます。
❯ cargo test --quiet
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
Compiling derive_builder v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/builder)
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.34s
test tests/02-create-builder.rs ... ok無事testをパスしました。 生成されたコードを確かめる方法ですが、test caseのコードをproject直下のmain.rsに貼り付けてcargo expandを実行することで確かめられます。試しに実行してみると以下のようになりました。
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use derive_builder::Builder;
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: String,
}
struct CommandBuilder {}
impl Command {
fn builder() -> CommandBuilder {
CommandBuilder {}
}
}
fn main() {
let builder = Command::builder();
let _ = builder;
}03-call-setters
fn tests() {
let t = trybuild::TestCases::new();
// ...
t.pass("tests/03-call-setters.rs");
// ...
}03-call-setters.rsでは
use derive_builder::Builder;
#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: String,
}
fn main() {
let mut builder = Command::builder();
builder.executable("cargo".to_owned());
builder.args(vec!["build".to_owned(), "--release".to_owned()]);
builder.env(vec![]);
builder.current_dir("..".to_owned());
}builderに定義元のstructのfield名のmethodが定義されています。型は定義元のCommandのfieldと同じようです。これに対応するには以下の変更が必要そうです。
- builderのfieldを
Option<T>とする - builderにderive元のfield名に対応したmethodを定義
追加した実装が以下です。
fn derive_builder(input: DeriveInput) -> Result<TokenStream2> {
if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }), ..
}) = input.data {
let ident = input.ident;
let builder = format_ident!("{}Builder",ident);
// 👇
let fields = named.iter().map(|f| (f.ident.as_ref().expect("field have ident"),&f.ty));
let builder_fields = fields.clone().map(|(ident, ty)| quote! {#ident: ::core::option::Option<#ty>});
let builder_init_fields = fields.clone().map(|(ident,_)| quote!{ #ident: ::core::option::Option::None});
let builder_methods = fields.clone().map(|(ident, ty)| impl_builder_method(ident, ty));
Ok(quote! {
struct #builder {
#(#builder_fields),*
}
impl #builder {
#(#builder_methods)*
}
impl #ident {
fn builder() -> #builder {
#builder {
#(#builder_init_fields),*
}
}
}
})
} else {
Err(Error::new(input.span(), "Only struct supported"))
}
}
fn impl_builder_method(
ident: &Ident,
ty: &Type,
) -> TokenStream2 {
quote! {
fn #ident (&mut self, #ident: #ty) -> &mut Self {
self.#ident = ::core::option::Option::Some(#ident);
self
}
}
}fieldの情報(識別子と型)をiterateしてそれぞれOptionでwrapしたり同名のmethodを生成させています。
struct #builder {
#(#builder_fields),*
}declarative macroと同様に#(...)*で繰り返しが表現できるので、iteratorを参照するとそれが展開されるイメージです。
expandしてみると
struct CommandBuilder {
executable: ::core::option::Option<String>,
args: ::core::option::Option<Vec<String>>,
env: ::core::option::Option<Vec<String>>,
current_dir: ::core::option::Option<String>,
}
impl CommandBuilder {
fn executable(&mut self, executable: String) -> &mut Self {
self.executable = ::core::option::Option::Some(executable);
self
}
fn args(&mut self, args: Vec<String>) -> &mut Self {
self.args = ::core::option::Option::Some(args);
self
}
fn env(&mut self, env: Vec<String>) -> &mut Self {
self.env = ::core::option::Option::Some(env);
self
}
fn current_dir(&mut self, current_dir: String) -> &mut Self {
self.current_dir = ::core::option::Option::Some(current_dir);
self
}
}
impl Command {
fn builder() -> CommandBuilder {
CommandBuilder {
executable: ::core::option::Option::None,
args: ::core::option::Option::None,
env: ::core::option::Option::None,
current_dir: ::core::option::Option::None,
}
}
}となり意図通りのコードが生成されています。
❯ cargo test --quiet
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.13s
test tests/03-call-setters.rs ... ok無事テストもパスしました。 source code
04-call-build
04-call-buildでは実際にbuilderのbuild methodが呼ばれます。
fn main() {
let mut builder = Command::builder();
builder.executable("cargo".to_owned());
builder.args(vec!["build".to_owned(), "--release".to_owned()]);
builder.env(vec![]);
builder.current_dir("..".to_owned());
let command = builder.build().unwrap();
assert_eq!(command.executable, "cargo");
}これに対応するためにはbuilderにbuild methodを追加し、自身のfieldの値からderive元のCommandをconstructするような処理が必要です。これに対応したのが以下です。
fn derive_builder(input: DeriveInput) -> Result<TokenStream2> {
if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) = input.data
{
let ident = input.ident;
let builder = format_ident!("{}Builder", ident);
let fields = named
.iter()
.map(|f| (f.ident.as_ref().expect("field have ident"), &f.ty));
// 👇
let idents = fields.clone().map(|(ident,_)| ident);
let builder_fields = fields
.clone()
.map(|(ident, ty)| quote! {#ident: ::core::option::Option<#ty>});
let builder_init_fields = fields
.clone()
.map(|(ident, _)| quote! { #ident: ::core::option::Option::None});
let builder_methods = fields
.clone()
.map(|(ident, ty)| impl_builder_method(ident, ty));
Ok(quote! {
struct #builder {
#(#builder_fields),*
}
impl #builder {
#(#builder_methods)*
// 👇
fn build(&mut self) -> ::core::result::Result<
#ident,
::std::boxed::Box<dyn ::std::error::Error>
>
{
Ok(#ident {
#(
#idents: self.#idents.take().ok_or_else(||
format!("{} is not provided", stringify!(#idents))
)?
),*
})
}
}
impl #ident {
fn builder() -> #builder {
#builder {
#(#builder_init_fields),*
}
}
}
})
} else {
Err(Error::new(input.span(), "Only struct supported"))
}
}filedの識別だけを保持するiteratorを用意しておき、#idents: self.#idents.take()のように使います。あくまでtokenなので、実際にはOptionでなくても(syn::Ident)takeが呼べてしまうんですね。
❯ cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.02s
Running unittests src/lib.rs (/Users/ymgyt/rs/proc-macro-workshop-blog/target/debug/deps/derive_builder-5d658299e3122af7)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/progress.rs (/Users/ymgyt/rs/proc-macro-workshop-blog/target/debug/deps/tests-0fe6acae350aa640)
running 1 test
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.13s
test tests/04-call-build.rs ... okテストもパスしました。
05-method-chaining
fn tests() {
let t = trybuild::TestCases::new();
// ...
t.pass("tests/05-method-chaining.rs");
// ...
}builderのmethodをchainできるかのtestです。
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
.args(vec!["build".to_owned(), "--release".to_owned()])
.env(vec![])
.current_dir("..".to_owned())
.build()
.unwrap();
assert_eq!(command.executable, "cargo");
}今回は新たに実装を変更しなくてもテストをパスできます。
今回はbuilderのsignatureを&mut selfとしていますが、selfとしたい場面もあります。今回のケースでは初期値をすべてOption型としているので、takeで所有権を奪えるのですがそうでない場合には&mut selfから値をmoveできずに不要なcloneが生じてしまう場合があります。
このあたりのPro/Conについてはderive_builderに説明がありました。
❯ cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.02s
Running unittests src/lib.rs (/Users/ymgyt/rs/proc-macro-workshop-blog/target/debug/deps/derive_builder-5d658299e3122af7)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/progress.rs (/Users/ymgyt/rs/proc-macro-workshop-blog/target/debug/deps/tests-0fe6acae350aa640)
running 1 test
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.13s
test tests/05-method-chaining.rs ... ok06-optional-fields
fn tests() {
let t = trybuild::TestCases::new();
// ...
t.pass("tests/06-optional-field.rs");
// ...
}#[derive(Builder)]
pub struct Command {
executable: String,
args: Vec<String>,
env: Vec<String>,
current_dir: Option<String>,
}
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
.args(vec!["build".to_owned(), "--release".to_owned()])
.env(vec![])
.build()
.unwrap();
assert!(command.current_dir.is_none());
let command = Command::builder()
.executable("cargo".to_owned())
.args(vec!["build".to_owned(), "--release".to_owned()])
.env(vec![])
.current_dir("..".to_owned())
.build()
.unwrap();
assert!(command.current_dir.is_some());
}derive元では 今回の変更点は以下です。 変更後のコードは以下になります。 まず型の判別とinner typeを取得するutil処理として、 これでテストをパスできました。 fieldに 今回必要な変更点は 変更後のコードは以下のようになりました。 無事テストをパスしました。 testを実行してみると以下のように意図したエラーメッセージとちょっとずれています。 具体的には、 Creates an error with the specified message spanning the given syntax tree node. とあるので、syn側でいい感じにtoken treeからspanの開始と終了を組み立ててくれている様です。 無事テストも通りました。 最後のケースはstdのよく使われる型にaliasが設定されていても機能するかのテストです。 元々、 今まではtest caseを一つずつ有効にしていましたが通しで実行してみましょう。 無事全てケースをパスできました🎉 最終的には以下のようになりました。 簡単にですがbuilder projectをやってみました。current_dirがOptionになっています。それに対応してbuilderでもcurrent_dirの呼び出しがoptionalになっています。
ようやくbuilderらしくなってきたのですが、このあたりからつらくなります。具体的には扱っている対象があくまでtoken列なので、型として情報がまだなく、今扱っているfiledの型がOptionなのかの判別やOptionSome(None)にする fn derive_builder(input: DeriveInput) -> Result<TokenStream2> {
if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) = input.data
{
let ident = input.ident;
let builder = format_ident!("{}Builder", ident);
let fields = named
.iter()
.map(|f| (f.ident.as_ref().expect("field have ident"), &f.ty));
let idents = fields.clone().map(|(ident, _)| ident);
let builder_fields = fields
.clone()
.map(|(ident, ty)| quote! {#ident: ::core::option::Option<#ty>});
let builder_init_fields = fields.clone().map(builder_init_field);
let builder_methods = fields
.clone()
.map(|(ident, ty)| impl_builder_method(ident, ty));
Ok(quote! {
struct #builder {
#(#builder_fields),*
}
impl #builder {
#(#builder_methods)*
fn build(&mut self) -> ::core::result::Result<
#ident,
::std::boxed::Box<dyn ::std::error::Error>
>
{
Ok(#ident {
#(
#idents: self.#idents.take().ok_or_else(||
format!("{} is not provided", stringify!(#idents))
)?
),*
})
}
}
impl #ident {
fn builder() -> #builder {
#builder {
#(#builder_init_fields),*
}
}
}
})
} else {
Err(Error::new(input.span(), "Only struct supported"))
}
}
// 👇
fn impl_builder_method(ident: &Ident, ty: &Type) -> TokenStream2 {
match field_type(ty) {
FieldType::Option(inner_ty) => {
quote! {
fn #ident (&mut self, #ident: #inner_ty) -> &mut Self {
self.#ident = ::core::option::Option::Some(
::core::option::Option::Some(#ident)
);
self
}
}
}
_ => {
quote! {
fn #ident (&mut self, #ident: #ty) -> &mut Self {
self.#ident = ::core::option::Option::Some(#ident);
self
}
}
}
}
}
// 👇
fn builder_init_field((ident, ty): (&Ident, &Type)) -> TokenStream2 {
match field_type(ty) {
FieldType::Option(_inner_ty) => {
quote! { #ident: ::core::option::Option::Some(::core::option::Option::None)}
}
FieldType::Vec(_inner_ty) => {
quote! { #ident: ::core::option::Option::Some(::std::vec::Vec::new())}
}
FieldType::Raw => {
quote! { #ident: ::core::option::Option::None}
}
}
}
// 👇
fn field_type(ty: &Type) -> FieldType {
use syn::{
AngleBracketedGenericArguments, GenericArgument, Path, PathArguments, PathSegment, TypePath,
};
if let Type::Path(TypePath {
qself: None,
path: Path {
leading_colon,
segments,
},
}) = ty
{
if leading_colon.is_none() && segments.len() == 1 {
if let Some(PathSegment {
ident,
arguments:
PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) = segments.first()
{
if let (1, Some(GenericArgument::Type(t))) = (args.len(), args.first()) {
if ident == "Option" {
return FieldType::Option(t.clone());
} else if ident == "Vec" {
return FieldType::Vec(t.clone());
}
}
}
}
}
FieldType::Raw
}
enum FieldType {
Raw,
Option(Type),
Vec(Type),
}field_type()を定義します。
このようにsynから型を判別してinner typeを取得する処理は結構大変です。この処理でも簡易的でもっと考慮しないといけなケースがあります。
filedの型がOption<T>と判別できれば、初期値はSome(None)として、methodの引数ではTをうけとれるようにできます。❯ cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.02s
Running unittests src/lib.rs (/Users/ymgyt/rs/proc-macro-workshop-blog/target/debug/deps/derive_builder-5d658299e3122af7)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/progress.rs (/Users/ymgyt/rs/proc-macro-workshop-blog/target/debug/deps/tests-0fe6acae350aa640)
running 1 test
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.13s
test tests/06-optional-field.rs ... ok07-repeated-fieldfn tests() {
let t = trybuild::TestCases::new();
// ...
t.pass("tests/07-repeated-field.rs");
// ...
}07-repeted-fieldは以下のようになっています。#[derive(Builder)]
pub struct Command {
executable: String,
#[builder(each = "arg")]
args: Vec<String>,
#[builder(each = "env")]
env: Vec<String>,
current_dir: Option<String>,
}
fn main() {
let command = Command::builder()
.executable("cargo".to_owned())
.arg("build".to_owned())
.arg("--release".to_owned())
.build()
.unwrap();
assert_eq!(command.executable, "cargo");
assert_eq!(command.args, vec!["build", "--release"]);
}builder(each = ...)というannotationが追加されており、Vec<T>型のfieldに対して、eachで指定したmethodを複数回呼ぶことで内部的なvecにpushできるようなapiになっています。
ここで注意していただきたいのが、argsではeachがargになっているのに対して、envではeachもenvとなっており、eachと元々のfield名が一致しています。eachのannotationに対応したbuilder method#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
let input: DeriveInput = parse_macro_input!(input as DeriveInput);
// ...
}
fn derive_builder(input: DeriveInput) -> Result<TokenStream2> {
if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) = input.data
{
let ident = input.ident;
let builder = format_ident!("{}Builder", ident);
let fields = named
.iter()
.map(|f| (f.ident.as_ref().expect("field have ident"), &f.ty));
let idents = fields.clone().map(|(ident, _)| ident);
let builder_fields = fields
.clone()
.map(|(ident, ty)| quote! {#ident: ::core::option::Option<#ty>});
let builder_init_fields = fields.clone().map(builder_init_field);
// 👇
let each_attributes = named.iter().map(|f| match f.attrs.first() {
Some(attr) => inspect_each(attr),
None => Ok(None),
}).collect::<Result<Vec<_>>>()?;
let builder_methods = fields
.clone()
.zip(each_attributes)
.map(|((ident, ty), maybe_each)| impl_builder_method(ident, ty, maybe_each));
Ok(quote! {
struct #builder {
#(#builder_fields),*
}
impl #builder {
#(#builder_methods)*
fn build(&mut self) -> ::core::result::Result<
#ident,
::std::boxed::Box<dyn ::std::error::Error>
>
{
Ok(#ident {
#(
#idents: self.#idents.take().ok_or_else(||
format!("{} is not provided", stringify!(#idents))
)?
),*
})
}
}
impl #ident {
fn builder() -> #builder {
#builder {
#(#builder_init_fields),*
}
}
}
})
} else {
Err(Error::new(input.span(), "Only struct supported"))
}
}
// ...
// 👇
fn inspect_each(attr: &syn::Attribute) -> Result<Option<Ident>> {
use syn::{Lit, Meta, MetaList, MetaNameValue, NestedMeta};
let meta = attr.parse_meta()?;
match &meta {
Meta::List(MetaList { path, nested, .. }) if path.is_ident("builder") => {
if let Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { lit, path, .. }))) =
nested.first()
{
match lit {
Lit::Str(s) if path.is_ident("each") => {
Ok(Some(format_ident!("{}", s.value())))
}
_ => Err(Error::new(
meta.span(),
"expected `builder(each = \"...\")`",
)),
}
} else {
Err(Error::new(
meta.span(),
"expected `builder(each = \"...\")`",
))
}
}
_ => Ok(None),
}
}builder attributeをうけとるために、top levelの関数のannotationを変更しています。
具体的には、#[proc_macro_derive(Builder, attributes(builder))]のようにしてbuilder attributeを使うことを指示しています。
attributeを値を抽出するhelperを定義。eachが指定されていたらbuilderのmethod生成時にVec型のsignatureを変更します。❯ cargo test --quiet
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
Compiling derive_builder v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/builder)
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.41s
test tests/07-repeated-field.rs ... ok08-unrecognized-attributefn tests() {
let t = trybuild::TestCases::new();
// ...
t.compile_fail("tests/08-unrecognized-attribute.rs");
// ...
}08-unrecognized-attributeは意図しないattributeが渡された場合に適切なcompile errorを返せているかをチェックするテストです。具体的には以下のようにeachがtypoされています。#[derive(Builder)]
pub struct Command {
executable: String,
// 👇
#[builder(eac = "arg")]
args: Vec<String>,
env: Vec<String>,
current_dir: Option<String>,
}test tests/08-unrecognized-attribute.rs ... mismatch
EXPECTED:
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: expected `builder(each = "...")`
--> tests/08-unrecognized-attribute.rs:22:7
|
22 | #[builder(eac = "arg")]
| ^^^^^^^^^^^^^^^^^^^^
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
ACTUAL OUTPUT:
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error: expected `builder(each = "...")`
--> tests/08-unrecognized-attribute.rs:22:7
|
22 | #[builder(eac = "arg")]
| ^^^^^^^
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈^を`builderから引数の終わりまでつけたいのですが、現状ではbuilderにしか付与されていません。これに対応するために以下のように変更します。fn inspect_each(attr: &syn::Attribute) -> Result<Option<Ident>> {
use syn::{Lit, Meta, MetaList, MetaNameValue, NestedMeta};
let meta = attr.parse_meta()?;
match &meta {
Meta::List(MetaList { path, nested, .. }) if path.is_ident("builder") => {
if let Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { lit, path, .. }))) =
nested.first()
{
match lit {
Lit::Str(s) if path.is_ident("each") => {
Ok(Some(format_ident!("{}", s.value())))
}
_ => {
// 👇
Err(Error::new_spanned(
meta,
"expected `builder(each = \"...\")`",
))
},
}
} else {
// 👇
Err(Error::new_spanned(
meta,
"expected `builder(each = \"...\")`",
))
}
}
_ => Ok(None),
}
}syn::Error::new()をsyn::Error::new_spanned()に変更して、引数にspanではなく、直接syn::Metaを渡しました。syn::Error::new_spanned()は以下のように定義されています。/// Creates an error with the specified message spanning the given syntax
/// tree node.
///
/// Unlike the `Error::new` constructor, this constructor takes an argument
/// `tokens` which is a syntax tree node. This allows the resulting `Error`
/// to attempt to span all tokens inside of `tokens`. While you would
/// typically be able to use the `Spanned` trait with the above `Error::new`
/// constructor, implementation limitations today mean that
/// `Error::new_spanned` may provide a higher-quality error message on
/// stable Rust.
///
/// When in doubt it's recommended to stick to `Error::new` (or
/// `ParseStream::error`)!
#[cfg(feature = "printing")]
pub fn new_spanned<T: ToTokens, U: Display>(tokens: T, message: U) -> Self {
let mut iter = tokens.into_token_stream().into_iter();
let start = iter.next().map_or_else(Span::call_site, |t| t.span());
let end = iter.last().map_or(start, |t| t.span());
Error {
messages: vec![ErrorMessage {
start_span: ThreadBound::new(start),
end_span: ThreadBound::new(end),
message: message.to_string(),
}],
}
}
実装もstartとendを取得しているので、error messageを返す際はこのmethodを使っておけば直感的に問題のあるコードが指定されそうです。
(stderrもテストできるtrybuildも便利で他にも使い道がありそうでもっと調べてみたくなります)❯ cargo test --quiet
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
Compiling derive_builder v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/builder)
Checking derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
test tests/08-unrecognized-attribute.rs ... ok09-redefined-prelude-typefn tests() {
let t = trybuild::TestCases::new();
// ...
t.pass("tests/09-redefined-prelude-types.rs");
// ...
}
具体的には以下のようにOptionやResultのaliasが設定されています。
procedural macroに限らずdeclarative macroでも共通しますが、ユーザのimport/aliasに影響されないように型を参照する際はすべてフルパスにしておく必要があります。type Option = ();
type Some = ();
type None = ();
type Result = ();
type Box = ();
#[derive(Builder)]
pub struct Command {
executable: String,
}
fn main() {}::core::option::Optionのように型をフルパスで指定していたので特に変更せずにテストをパスできます。❯ cargo test --quiet
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.13s
test tests/09-redefined-prelude-types.rs ... okまとめ
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/01-parse.rs");
t.pass("tests/02-create-builder.rs");
t.pass("tests/03-call-setters.rs");
t.pass("tests/04-call-build.rs");
t.pass("tests/05-method-chaining.rs");
t.pass("tests/06-optional-field.rs");
t.pass("tests/07-repeated-field.rs");
t.compile_fail("tests/08-unrecognized-attribute.rs");
t.pass("tests/09-redefined-prelude-types.rs");
}❯ cargo test --quiet
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
Compiling derive_builder-tests v0.0.0 (/Users/ymgyt/rs/proc-macro-workshop-blog/target/tests/derive_builder)
Finished dev [unoptimized + debuginfo] target(s) in 0.14s
test tests/01-parse.rs [should pass] ... ok
test tests/02-create-builder.rs [should pass] ... ok
test tests/03-call-setters.rs [should pass] ... ok
test tests/04-call-build.rs [should pass] ... ok
test tests/05-method-chaining.rs [should pass] ... ok
test tests/06-optional-field.rs [should pass] ... ok
test tests/07-repeated-field.rs [should pass] ... ok
test tests/08-unrecognized-attribute.rs [should fail to compile] ... ok
test tests/09-redefined-prelude-types.rs [should pass] ... ok
source codeuse proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use syn::{
parse_macro_input, Data, DataStruct, DeriveInput, Error, Fields, FieldsNamed, Ident, Result,
Type,
};
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive(input: TokenStream) -> TokenStream {
let input: DeriveInput = parse_macro_input!(input as DeriveInput);
match derive_builder(input) {
Ok(token_stream) => TokenStream::from(token_stream),
Err(err) => TokenStream::from(err.to_compile_error()),
}
}
fn derive_builder(input: DeriveInput) -> Result<TokenStream2> {
if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { named, .. }),
..
}) = input.data
{
let ident = input.ident;
let builder = format_ident!("{}Builder", ident);
let fields = named
.iter()
.map(|f| (f.ident.as_ref().expect("field have ident"), &f.ty));
let idents = fields.clone().map(|(ident, _)| ident);
let builder_fields = fields
.clone()
.map(|(ident, ty)| quote! {#ident: ::core::option::Option<#ty>});
let builder_init_fields = fields.clone().map(builder_init_field);
let each_attributes = named.iter().map(|f| match f.attrs.first() {
Some(attr) => inspect_each(attr),
None => Ok(None),
}).collect::<Result<Vec<_>>>()?;
let builder_methods = fields
.clone()
.zip(each_attributes)
.map(|((ident, ty), maybe_each)| impl_builder_method(ident, ty, maybe_each));
Ok(quote! {
struct #builder {
#(#builder_fields),*
}
impl #builder {
#(#builder_methods)*
fn build(&mut self) -> ::core::result::Result<
#ident,
::std::boxed::Box<dyn ::std::error::Error>
>
{
Ok(#ident {
#(
#idents: self.#idents.take().ok_or_else(||
format!("{} is not provided", stringify!(#idents))
)?
),*
})
}
}
impl #ident {
fn builder() -> #builder {
#builder {
#(#builder_init_fields),*
}
}
}
})
} else {
Err(Error::new(input.span(), "Only struct supported"))
}
}
fn impl_builder_method(ident: &Ident, ty: &Type, each: Option<Ident>) -> TokenStream2 {
let has_each = each.is_some();
match field_type(ty) {
FieldType::Option(inner_ty) => {
quote! {
fn #ident (&mut self, #ident: #inner_ty) -> &mut Self {
self.#ident = ::core::option::Option::Some(
::core::option::Option::Some(#ident)
);
self
}
}
}
FieldType::Vec(inner_ty) if has_each => {
let each = each.unwrap();
quote! {
fn #each (&mut self, #each: #inner_ty) -> &mut Self {
self.#ident.as_mut().map(|v| v.push(#each));
self
}
}
}
_ => {
quote! {
fn #ident (&mut self, #ident: #ty) -> &mut Self {
self.#ident = ::core::option::Option::Some(#ident);
self
}
}
}
}
}
fn builder_init_field((ident, ty): (&Ident, &Type)) -> TokenStream2 {
match field_type(ty) {
FieldType::Option(_inner_ty) => {
quote! { #ident: ::core::option::Option::Some(::core::option::Option::None)}
}
FieldType::Vec(_inner_ty) => {
quote! { #ident: ::core::option::Option::Some(::std::vec::Vec::new())}
}
FieldType::Raw => {
quote! { #ident: ::core::option::Option::None}
}
}
}
fn field_type(ty: &Type) -> FieldType {
use syn::{
AngleBracketedGenericArguments, GenericArgument, Path, PathArguments, PathSegment, TypePath,
};
if let Type::Path(TypePath {
qself: None,
path: Path {
leading_colon,
segments,
},
}) = ty
{
if leading_colon.is_none() && segments.len() == 1 {
if let Some(PathSegment {
ident,
arguments:
PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
}) = segments.first()
{
if let (1, Some(GenericArgument::Type(t))) = (args.len(), args.first()) {
if ident == "Option" {
return FieldType::Option(t.clone());
} else if ident == "Vec" {
return FieldType::Vec(t.clone());
}
}
}
}
}
FieldType::Raw
}
enum FieldType {
Raw,
Option(Type),
Vec(Type),
}
fn inspect_each(attr: &syn::Attribute) -> Result<Option<Ident>> {
use syn::{Lit, Meta, MetaList, MetaNameValue, NestedMeta};
let meta = attr.parse_meta()?;
match &meta {
Meta::List(MetaList { path, nested, .. }) if path.is_ident("builder") => {
if let Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { lit, path, .. }))) =
nested.first()
{
match lit {
Lit::Str(s) if path.is_ident("each") => {
Ok(Some(format_ident!("{}", s.value())))
}
_ => {
Err(Error::new_spanned(
meta,
"expected `builder(each = \"...\")`",
))
},
}
} else {
Err(Error::new_spanned(
meta,
"expected `builder(each = \"...\")`",
))
}
}
_ => Ok(None),
}
}
synのAPIを理解して、実装したい型を適切に分割して部分的に組み立てられることがわかりました。
token streamのparseはすでに実装されているので、rustのiterator等を普段と同じ様に使えるので非常に表現力が高いと感じました。また、quote!してしまうとTokenStreamになって型としては情報を失うので、できるだけ途中結果を表現する型を用意して、最後にquoteするようにしないとどんどん読みにくくなるなと感じました。今回の短い例でも、builderの初期値はこうなってるから...と考えながらやったのでより複雑なユースケースに対応するにはfiled毎のparse結果を保持する型を定義してユニットテスト書きながら行いたいと思いました。