手続きマクロ内でクレート名を取得する

Rustにおいて、`foo`クレートで定義された`foo::Foo`トレイトの実装を`foo-derive`クレートのderiveマクロで生成するようなパターンが頻繁に登場します。この記事ではこのようなマクロの実装における名前解決のちょっとしたコーナーケースについて紹介します。

## ナイーブな実装

（この例はRust 1.37.0 (2019-8-13) で安定化された`underscore_const_names`フィーチャに依存しています）

```rust:foo/src/lib.rs
//! # Foo
//!
//! ## Examples
//!
//! ```
//! #[derive(foo::Foo)]
//! struct S;
//! ```

pub use foo_derive::Foo;

pub trait Foo {}
```

```rust:foo-derive/src/lib.rs
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`クレートをインポートできないので上手く動きません。

```toml:another-crate/Cargo.toml
[dependencies]
bar = { version = "*", package = "foo" }
```

## `proc-macro-crate`クレート

`Cargo.toml`でリネームされたクレートの名前を正攻法で知るのは難しいです。そこで、コンパイル時にマクロ呼び出し側の`Cargo.toml`を読んでリネーム後の名前を取得するマクロが[`proc-macro-crate`]クレートで提供されています。

[`proc-macro-crate`]: https://github.com/bkchr/proc-macro-crate

これを使うと`foo-derive`は以下のように書けます。

```rust:foo-derive/src/lib.rs
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テストを走らせると以下のようにエラーになります。

```console
$ 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`のテストを通せます。

```toml:foo/Cargo.toml
[dev-dependencies]
foo = { path = "" }
```

## 終わりに

この記事で実装したコードは以下のGitリポジトリにまとめてあります。

https://github.com/tesaguri/proc-macro-crate-name