Rustにおいて、foo
クレートで定義されたfoo::Foo
トレイトの実装をfoo-derive
クレートのderiveマクロで生成するようなパターンが頻繁に登場します。この記事ではこのようなマクロの実装における名前解決のちょっとしたコーナーケースについて紹介します。
ナイーブな実装
(この例はRust 1.37.0 (2019-8-13) で安定化されたunderscore_const_names
フィーチャに依存しています)
//! # Foo
//!
//! ## Examples
//!
//! ```
//! #[derive(foo::Foo)]
//! struct S;
//! ```
pub use foo_derive::Foo;
pub trait Foo {}
extern crate proc_macro;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Foo)]
pub fn derive_foo(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let output = quote! {
const _: () = {
// 呼び出し側の型名前空間で `foo` が上書きされている可能性があるので別名でインポートする
extern crate foo as _foo;
impl _foo::Foo for #name {}
};
};
output.into()
}
多くのケースではこれで問題なく動きますが、もしマクロ呼び出し側のCargo.toml
で以下のようにfoo
がリネームされていたら、extern crate foo
でfoo
クレートをインポートできないので上手く動きません。
[dependencies]
bar = { version = "*", package = "foo" }
proc-macro-crate
クレート
Cargo.toml
でリネームされたクレートの名前を正攻法で知るのは難しいです。そこで、コンパイル時にマクロ呼び出し側のCargo.toml
を読んでリネーム後の名前を取得するマクロがproc-macro-crate
クレートで提供されています。
これを使うとfoo-derive
は以下のように書けます。
extern crate proc_macro;
use proc_macro2::{Span, Ident};
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Foo)]
pub fn derive_foo(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let foo = proc_macro_crate::crate_name("foo").unwrap();
let foo = Ident::new(&foo, Span::call_site());
let output = quote! {
const _: () = {
// 呼び出し側の型名前空間で `#foo` が上書きされている可能性があるので別名でインポートする
extern crate #foo as _foo;
impl _foo::Foo for #name {}
};
};
output.into()
}
これでマクロ呼び出し側のコンパイルが通るようになり、一件落着……と思いきや、ここでfoo
のdocテストを走らせると以下のようにエラーになります。
$ cargo test --doc
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Doc-tests foo
running 1 test
test src/lib.rs - (line 5) ... FAILED
failures:
---- src/lib.rs - (line 5) stdout ----
error: proc-macro derive panicked
--> src/lib.rs:6:10
|
4 | #[derive(foo::Foo)]
| ^^^^^^^^
|
= help: message: called `Result::unwrap()` on an `Err` value: "Could not find `foo` in `dependencies` or `dev-dependencies` in `/proc-macro-crate-name/foo/Cargo.toml`!"
error: aborting due to previous error
Couldn't compile the test.
failures:
src/lib.rs - (line 5)
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--doc'
foo/Cargo.toml
の依存にfoo
パッケージが見当たらないため、proc_macro_crate::crate_name
がエラーになっているようです。これはfoo
自身のdocテストなので、Cargo.toml
の依存にfoo
が書かれていないのは当然といえば当然ですが、それでテストが通らないのは困ります。
foo-derive/src/lib.rs
のunwrap()
をunwrap_or("foo")
としても良いのですが、テスト環境だけで起こるエラーのためにエラーを握りつぶすのが常に望ましい方法とはいえないと思います(それで困ることも稀でしょうが)。
少しトリッキーになりますが、以下のようにfoo
のCargo.toml
のdev-dependencies
にfoo
自身を加えれば、foo-derive
を書き換えずともfoo
のテストを通せます。
[dev-dependencies]
foo = { path = "" }
終わりに
この記事で実装したコードは以下のGitリポジトリにまとめてあります。