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リポジトリにまとめてあります。