Rust のマクロで生成した構造体などにドキュメントをつける

TL;DR

以下のように、マクロで Attr も受け取り、出力しなおすとよい。

macro_rules! create_struct {
    (
        $(#[$meta: meta])*
        $name: ident
    ) => {
        $(#[$meta])*
        pub struct $name();
    };
}

create_struct!(
    /// hello
    /// world
    Example
);

マクロ記述の近くに doc comments を使ってもドキュメントは書けない

何らかの都合で、マクロを使って構造体を作る時を考えます。

直感的には、マクロ展開後にドキュメンテーションされている状態にすることで、ドキュメントが書けそうです。

例えば、以下のように。

macro_rules! create_struct {
    ($name: ident) => {
        pub struct $name();
    };
}

/// Example documentation
create_struct!(Example);

ですが、このコードをコンパイルすると、

7 | /// Example documentation
  | ^^^^^^^^^^^^^^^^^^^^^^^^^ rustdoc does not generate documentation for macro invocations
  |
= note: `#[warn(unused_doc_comments)]` on by default
= help: to document an item produced by a macro, the macro must produce the documentation as part of its expansion

といった warning が出ます。

the macro must produce the documentation as part of its expansion

とのことなので、マクロでドキュメントの面倒も見る必要がありそうです。

マクロ内で doc comments を書いてみる

直感的には、マクロ内で doc comments を書けばよさそうです。

macro_rules! create_struct {
    (
        $docstring: expr,
        $name: ident
    ) => {
        /// $docstring
        pub struct $name();
    };
}

create_struct!("Example documentation", Example);

ですが、

ドキュメントとして $docstring が表示されている

のように、 $docstring が直接出力されてしまいます。

これは、 doc comments が #[doc] attribute のシンタックスシュガーであることが原因です。

詳しくは Comments - The Rust Reference の doc comments の節 を読んでほしいのですが、以下の2つは全く同じです。

/// $docstring
#[doc = " $docstring"]

doc comments では裸だった値が、 attribute では " に囲まれています。

" で囲まれた値は Literal Expression なので、変数展開されなくなります。

マクロ内で doc attribute を付与する

変数が展開されるようにするには、直接 #[doc] attribute を使います。

具体的には、以下のように書き換えます。

macro_rules! create_struct {
    (
        $docstring: literal,
        $name: ident
    ) => {
        #[doc = $docstring]
        pub struct $name();
    };
}

create_struct!("Example documentation", Example);

そして crate doc すると……

ドキュメントとして Example documentation が表示されている

きちんと設定されました!

引数ではなく doc comments っぽく書きたい

マクロの引数としてドキュメントを渡せることは分かりました。

ですが、マクロの引数としてリテラルを受け取ると、シンタックスハイライトの色がコメントっぽくなくなります。

doc comments が doc attribute に変換されることを利用すれば、ドキュメントをコメントっぽい色でハイライトさせることも可能です。

これは、マクロが Attr にマッチするようにし、マクロ呼び出しの引数で doc comments を直接書くことで実現できます。

macro_rules! create_struct {
    (
        $(#[$meta: meta])*
        $name: ident
    ) => {
        $(#[$meta])*
        pub struct $name();
    };
}

create_struct!(
    /// Example documentation
    Example
);

この方法だと、 #[cfg(test)] など、 doc comments 以外の Attribute にもマッチしてしまいます。

Meta variables の fragment-specifier に指定できるものはかなり限られているので、マッチャー内で解決するのは諦めたほうが幸せかもしれません。

最後に

The first rule of macro club is: Don’t write macros

When do you need macros in Clojure? | Creative Clojure

Clojure界隈で有名な「マクロ・クラブのルール」の一節ですが、Rustに対しても同じことが言えるでしょう。

しかし、いつか、ドキュメント付きのマクロを作らなくてはならない時が訪れるかもしれません。

その際に、この記事がお役に立てば幸いです。