📚

Rustのドキュメンテーションコメントの書き方

2023/07/15に公開

Rustを勉強している。ドキュメンテーションコメントとドキュメンテーションテストをひととおり眺め終わったので、まとめておく。

はじめに

Rustでは、ドキュメンテーションコメントという言語標準の手段で、プログラムやライブラリに対する説明をソースコード内に埋め込んで書くことができる。埋め込みドキュメントだと、ソースコードと対応するドキュメントを一体化して管理できるため、どちらか一方だけを変更してしまうという誤りをしにくいことが利点である。

また、ドキュメンテーションテストという仕組みを使うことで、ドキュメント内のコード例をテストできる。コード例の動作確認をすぐに行えるため、ソースコードの更新によりコード例が動作しなくなるという誤りが原則として発生しない。

ドキュメンテーションコメントの例

例えば、std::option::Optionis_some メソッドのドキュメンテーションコメントは、ソースコード内に下記のように書かれている。
(ソースコードの該当箇所から一部省略して引用)

/// Returns `true` if the option is a [`Some`] value.
///
/// # Examples
///
/// ```
/// let x: Option<u32> = Some(2);
/// assert_eq!(x.is_some(), true);
///
/// let x: Option<u32> = None;
/// assert_eq!(x.is_some(), false);
/// ```
pub const fn is_some(&self) -> bool {
    // ...
}

3連スラッシュ /// で始まる各行が、is_some メソッドに対するドキュメンテーションコメントである。

また、ドキュメンテーションコメント内の3連バッククォート ``` で囲まれたコードブロックは、このメソッドの使い方を示すためのソースコードの例である。このコード例は、文書内のコード例としてだけではなく、ドキュメンテーションテストのテストコードとしても扱われる。

生成されたドキュメントの例

上記のドキュメンテーションコメントからは、下記のドキュメントが生成される。

https://doc.rust-lang.org/std/option/enum.Option.html#method.is_some

std-option-Option-is_some-screenshot

生成されたドキュメントには、生成元のソースコード全体がドキュメントの一部として同梱される。ドキュメントの各所にソースコードの該当箇所へのリンクが自動で設置されるので、ドキュメントから生成元のソースコードやドキュメンテーションコメントをすぐに参照できる。

また、モジュールやクレートに対するドキュメントには、それに含まれる構造体や関数などの構成要素の一覧が自動生成され付加される。例えば std::fs モジュールのドキュメントは下記のとおり。

https://doc.rust-lang.org/std/fs/index.html

std-fs-screenshot

ドキュメント生成の手順

ドキュメントを生成する手順は下記のとおり。いずれかを行えばよい。

  • cargo doc を実行すると、Cargoの管理対象のすべてのソースコードが走査され、公開要素に対するすべてのドキュメンテーションコメントがドキュメント化される
    • バイナリクレートの場合は、非公開要素に対するドキュメンテーションコメントもドキュメント化される
  • cargo doc --open を実行すると、cargo doc 同様にドキュメントが生成され、生成後にその結果をWebブラウザで開いてくれる

ドキュメントは、HTML・CSS・JavaScriptで構成される一連のWebページとして、./target/doc/ 内に生成される。

ドキュメントの生成は、ドキュメンテーションコメントがまったくなくても行える。その状態でも、関数のシグネチャなどがドキュメント化されるため、それなりに有用である。

cargo doc の処理は、rustdoc によって行われる。rustdoc がサポートする出力ドキュメント形式は、前述のWebページのみであり、PDF形式などはサポートされていない。

ドキュメンテーションテストの実行手順

ドキュメンテーションテストを実行する手順は下記のとおり。いずれかを行えばよい。

  • cargo test --doc を実行すると、Cargoの管理対象のすべてのドキュメンテーションコメント内のコードブロックがテストコードとして実行される
  • cargo test を実行すると、すべての単体テスト・結合テスト・ドキュメンテーションテストがまとめて実行される
  • cargo test --doc src/file.rscargo test --doc struct::method などと対象を指定して実行すると、特定のドキュメンテーションテストのみが実行される

ドキュメンテーションコメントの書き方

ドキュメンテーションコメントの種別

ドキュメンテーションコメント(doc comment)には、下記の4種類がある。
(参照: Doc comments - The Rust Reference)

記述 inner / outer line / block
//! ... inner line (行末までがコメント。 // と同様)
/*! ... */ inner block (囲まれた範囲内がコメント。 /* ... */ と同様)
/// ... outer line
/** ... */ outer block

inner doc commentとouter doc commentでは、ドキュメンテーションコメントの適用先が下記のように異なる。

inner / outer ドキュメンテーションコメントの適用先
inner そのコメントを含んでいる親の要素
outer そのコメントの直後に置かれた要素

4種類のドキュメンテーションコメントは、下記のように使い分ける。
(参照: Use line comments - RFC1574: More API Documentation Conventions)

  • 必須: クレートに対しては、inner doc commentを使用する (文法上outer doc commentを使用できないため)
  • 推奨: モジュールに対しては:
    • そのモジュールのソースコードを独立したファイルに分離しているなら、分離したファイル内でinner doc commentを使用する
    • さもなければ、モジュールブロックに対してouter doc commentを使用する
  • 推奨: クレートとモジュール以外のものに対しては、outer doc commentを使用する
  • 推奨: lineスタイルを使用する。blockスタイルは使用しない

上記に従うと、多くの箇所で下記のような /// によるouter line doc commentを使うことになる。クレートルートとモジュールのファイル冒頭でのみ、//! によるinner line doc commentを使用する。

/// Creates a new, empty value.
pub fn new() -> Self {
    // ...
}

ドキュメンテーションコメント内の文法

ドキュメンテーションコメント内の記述は、Markdown記法として解釈される。具体的には、下記の記法を使用できる。
(参照: Markdown - The rustdoc book)

リンク

ドキュメンテーションコメント内では、他の要素のドキュメントへのリンクを [name] などとして記述できる。例えば下記のとおり。
(Linking to items by name - The rustdoc bookの例を参考に構成した)

/// This struct is not [Bar]
pub struct Foo1;

/// This struct *is* [`Bar`]!
pub struct Bar;

/// This struct is not [`Baz<T>`]
///
/// [`Baz<T>`]: Baz
pub struct Foo2;

/// This struct is not [`Baz<T>`][Baz]
pub struct Foo3;

例えば [Bar][`Bar`][`Baz<T>`][Baz][`foo::Bar`][`std::iter::Iterator`][`foo()`][`foo!`] などと書いた場合には、下記のいずれかの方法でリンク先の要素が推定される。

  • [`Bar`] などと名前がバッククォートで囲まれている場合には、バッククォートを外した名前に対して推定を行う
  • [`Baz<T>`][Baz] などと推定すべき名前が後置の [...] にて指定されている場合には、Baz<T> ではなく Baz という名前に対して推定を行う
  • ドキュメンテーションコメントのどこかに [`Baz<T>`]: Baz という記述があれば、Baz<T> という名前のリンク先を推定する際には Baz という名前を使う
  • ドキュメンテーションコメントのどこかに [`foo`]: #method.foo という記述があれば、foo という名前のリンク先を推定する際には foo というメソッド名を使う
  • Barfoo::Barstd::iter::Iterator などといったパスは、そのドキュメンテーションコメントの対象の要素の置かれた位置を起点にして推定する
    • rustdoc の管理対象のソースコード内にそのパスの要素(例えば Barfoo::Bar)が存在すれば、その要素をリンク先とする
    • 標準ライブラリ内にそのパスの要素(例えば std::iter::Iterator)が存在すれば、その要素をリンク先とする
  • [`foo()`] などと末尾に () を付けて書くと、関数 foo を探す
  • [`foo!`] などと末尾に ! を付けて書くと、マクロ foo! を探す

リンクに関する詳細は下記を参照のこと。

コードブロック

ドキュメンテーションコメント内の3連バッククォート (```) で囲まれたコードブロックは、Markdown記法のコードブロックとしてドキュメントに反映されるだけでなく、ドキュメンテーションテストのテストコードとしても扱われる。例えば下記のように記述した場合は(std::option::Optionis_some メソッドの例の再掲)、ドキュメンテーションテストを実行すると、2つの assert_eq! がテストされる。コードブロックに対するコンパイルでエラーが発生したり、コードブロックの実行時に assert_eq! で失敗したりすると、テストは失敗する。

/// Returns `true` if the option is a [`Some`] value.
///
/// # Examples
///
/// ```
/// let x: Option<u32> = Some(2);
/// assert_eq!(x.is_some(), true);
///
/// let x: Option<u32> = None;
/// assert_eq!(x.is_some(), false);
/// ```
pub const fn is_some(&self) -> bool {
    // ...
}

ドキュメンテーションコメント内のコードブロックでは、原則としてRustのすべての文法を使用できる。通常のプログラムのソースコードには main 関数を置く必要があるが、ドキュメンテーションコメント内のコードブロックでは省略できる。省略した場合には、ドキュメンテーションテストではそのコードブロックが fn main() { ... } の内側に書かれているものとして扱われる。
(参照: Pre-processing examples - The rustdoc book)

特定の行のドキュメント化の抑制

コードブロック内の行頭に # を置くと、その行はテストコードとしては使用されるがドキュメントには転記されない。例えば下記のように書くと、fn main() -> io::Result<()> {Ok(())} の3行はドキュメントに転記されない。テストコードとしては必要だがドキュメントとしては隠したい行があるときに使うとよい。
(Using ? in doc tests - The rustdoc bookの例から引用)

/// ```
/// use std::io;
/// # fn main() -> io::Result<()> {
/// let mut input = String::new();
/// io::stdin().read_line(&mut input)?;
/// # Ok(())
/// # }
/// ```

コードブロックの属性

コードブロックに属性を与えることで、ドキュメンテーションテストでのそのコードブロックの扱いを制御できる。例えばパニックすることをテストしたい場合には、コードブロックに下記のように should_panic 属性を与えればよい。

/// ```should_panic
/// assert!(false);
/// ```

コードブロックに付与できる主な属性は下記のとおり。
(参照: Attributes - The rustdoc book)

属性 ドキュメンテーションテストでの扱い
ignore 無視される
should_panic コンパイルに成功し、かつ実行してパニックすればテスト成功
no_run コンパイルに成功すればテスト成功 (実行しない)
compile_fail コンパイルに失敗すればテスト成功

なお、Markdownのコードブロックでは開始側の3連バッククォートの直後に rust などと書くことでコードブロックの言語を指定できるが、ドキュメンテーションコメント内では rust と明示しても何も書かなくても、どちらでもドキュメンテーションテストのテストコードとして扱われる(参照: Documentation tests - The rustdoc book)。コードブロックの言語に shell などとRust以外の言語を指定すると、ignore を明示した場合と同様にドキュメンテーションテストでは無視されるようである。

よいドキュメンテーションコメントの書き方

推奨される構成

個別のドキュメンテーションコメントは、下記の順序で書かれることが推奨されている。
(参照: Documenting components - The rustdoc book)

  1. 対象が何であるかを表現した、簡潔な要約
    • ドキュメンテーションコメント内の最初の空行までが要約として扱われる
  2. もう少し詳しい説明
  3. 少なくとも1つのコード例
    • このコード例は、読者がコピー&ペーストして試せるほうがよい
  4. 必要に応じて、より進んだ話題

例えば、std::fs モジュールの read 関数のドキュメンテーションコメントは下記のとおり。
(ソースコードの該当箇所から引用)

/// Read the entire contents of a file into a bytes vector.
///
/// This is a convenience function for using [`File::open`] and [`read_to_end`]
/// with fewer imports and without an intermediate variable.
///
/// [`read_to_end`]: Read::read_to_end
///
/// # Errors
///
/// This function will return an error if `path` does not already exist.
/// Other errors may also be returned according to [`OpenOptions::open`].
///
/// It will also return an error if it encounters while reading an error
/// of a kind other than [`io::ErrorKind::Interrupted`].
///
/// # Examples
///
/// ```no_run
/// use std::fs;
/// use std::net::SocketAddr;
///
/// fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
///     let foo: SocketAddr = String::from_utf8_lossy(&fs::read("address.txt")?).parse()?;
///     Ok(())
/// }
/// ```

一般に、冒頭に簡潔な要約があると、ドキュメントとして読みやすい。要約が長いと読みにくくなるので、避けたほうがよい。

Rustのドキュメンテーションコメントの要約は、その要素に対するドキュメント以外の箇所でも適宜使用される。例えば下記の std::fs モジュールのドキュメントの read 関数の説明には、上記の引用の要約である "Read the entire contents of a file into a bytes vector." が転記されている。

https://doc.rust-lang.org/std/fs/index.html

設置が推奨されるセクション

個別のドキュメンテーションコメントには、内容に応じていくつかのセクション(Markdown記法の見出し1による節)を設置することが推奨されている。主なものは下記のとおり。

  • Examples
    • コード例を示すセクション
    • 無理のない範囲で、すべての公開要素に設ける (別の箇所のコード例へのリンクでもよい)
    • 説明対象(例えば関数)を使う理由がわかるよう示すべきである。記述方法だけを示しても、ドキュメントとしてあまり有用ではない
  • Errors
    • エラーを返す可能性のある関数に対して設ける
    • エラーを返す条件やそのときに返されるエラーなどについて書く
  • Panics
    • パニックを起こす可能性のある関数に対して設ける
    • パニックを起こす条件などを書く
  • Safety
    • Unsafeな関数に対して設ける、unsafeの扱いに関するセクション
    • その関数を正しく使うために呼び出し側が守らなければならない不変条件を書く

詳細は下記を参照のこと。

モジュールに関する推奨事項

モジュールに対するドキュメンテーションコメントでは、それが含む個別の構造体や関数などの詳細ではなく、より抽象度の高い内容を扱ったほうがよい。
(参照: Module-level vs type-level docs - RFC1574: More API Documentation Conventions)

例えば std::collections モジュールのドキュメンテーションコメントは、下記のような構成になっている。

  • 要約: "Collection types."
  • 導入説明: 含まれているコレクションの一覧など
  • どんなときにどのコレクションを使うべきか
  • 性能

クレートに関する推奨事項

クレートに対するドキュメンテーションコメント(フロントページ)では、そのクレート自体に対する説明を扱うとよい。モジュールに対するものと同様に、それが含む個別の要素の詳細には立ち入らないほうがよい。例えば、下記のようなことを書くとよい。
(参照: Getting Started - The rustdoc book)

  • 冒頭に要約
  • クレートの役割
  • いつどのような目的でこのクレートを使うべきか
  • 技術詳細へのリンク
  • 使い方の例 (コード例を用いて)

なお、下記に関しては、クレートに対するドキュメンテーションコメントではなく、Readme(例えばクレートに同梱の README.md)に書いたほうがよいようである。ただ、Rust APIガイドラインなどに明記されるほど指標が明確化されているわけではないようだ。

  • 必要なもの (依存するシステムライブラリなど)
  • サポートするRustやOSのバージョン
  • 開発者向けの情報
  • Code of conduct (行動規範)
  • ライセンス

詳細は下記を参照のこと。

その他の推奨事項

リンクを張る

関連する要素の説明には、リンクが張ってあるほうがよい。
(参照: 文章に関係する項目へのリンクを含める (C-LINK) - Rust APIガイドライン)

コードブロックでは ? を使う

コード例では、unwrap などではなく ? を使うことが推奨されている。理由は、unwrap などの回復不能なエラー処理手段よりも ? による回復可能な手段のほうが実用的であり、読者がコード例をコピー&ペーストする際には実用的なもののほうがよいためである。

コード例に暗黙に補われる main 関数は戻り値を持たないため、そのままでは ? を使用できない。それを回避するために、適切な戻り値を持たせた main 関数を置き、行頭に # を置いて隠すとよい。例えば下記のとおり。
(Using ? in doc tests - The rustdoc bookの例から引用)

/// ```
/// use std::io;
/// # fn main() -> io::Result<()> {
/// let mut input = String::new();
/// io::stdin().read_line(&mut input)?;
/// # Ok(())
/// # }
/// ```

詳細は下記を参照のこと。

記述例の探し方

Rustのドキュメンテーションコメントは言語標準の文書化手段なので、標準ライブラリも、ソースコードを公開しているRustパッケージも、ソースコードに対するドキュメントは原則としてすべてドキュメンテーションコメントで書かれている。

標準ライブラリのドキュメントは、下記にて参照できる。対応するドキュメンテーションコメントは、ドキュメント内の各所に置かれたソースコードへのリンク先にて確認できる。

Rust標準のパッケージレジストリは crates.io で、そこに登録されたパッケージのドキュメントは Docs.rs に自動生成されるようになっている。パッケージ名を特定してドキュメントやドキュメンテーションコメントを参照すればよい。例えば clap であれば、下記に置かれている。

  • crates.io: clap
  • Docs.rs: clap (latest)

Tips

付与忘れの検出

ドキュメンテーションコメントの付与忘れを検出したい場合には、下記のLint項目の警告を有効にするとよい。

  • missing_docs
    • ドキュメンテーションコメントのない要素があれば警告する
    • デフォルトは allow (警告しない)
    • ソースコード内に #![warn(missing_docs)] と記述すれば、設定を allow から warn へと変更できる
      • 例えば、ライブラリクレートの src/lib.rs#![warn(missing_docs)] などと記述すると、ドキュメンテーションコメントのない公開要素がクレート内にあれば警告される
    • このLint項目は、rustc 実行時にチェックされる
  • missing_crate_level_docs
    • クレートにドキュメンテーションコメントがなければ警告する
    • デフォルトは allow
    • #![warn(rustdoc::missing_crate_level_docs)] と記述すれば、設定を allow から warn へと変更できる
    • このLint項目は、rustdoc 実行時にチェックされる

カバレッジ測定

下記を実行すると、ドキュメンテーションコメントのカバー率を確認できる。実行にはnightly toolchainが必要。
(参照: --show-coverage: calculate the percentage of items with documentation)

RUSTDOCFLAGS='-Z unstable-options --show-coverage' cargo +nightly doc --no-deps
  • nightly toolchainをインストールするには、例えば rustup toolchain install nightly を実行すればよい
  • --no-deps を付けることで、依存クレートのカバー率表示を抑制している

動作確認に使用した環境

項目 内容
OS, Distribution Debian bullseye (11.7) for amd64
cargo, rustdoc (stable) 1.71.0
cargo, rustdoc (nightly) 1.73.0-nightly (ad963232d 2023-07-14)

参考文献

Discussion