Rustのドキュメンテーションコメントの書き方
Rustを勉強している。ドキュメンテーションコメントとドキュメンテーションテストをひととおり眺め終わったので、まとめておく。
はじめに
Rustでは、ドキュメンテーションコメントという言語標準の手段で、プログラムやライブラリに対する説明をソースコード内に埋め込んで書くことができる。埋め込みドキュメントだと、ソースコードと対応するドキュメントを一体化して管理できるため、どちらか一方だけを変更してしまうという誤りをしにくいことが利点である。
また、ドキュメンテーションテストという仕組みを使うことで、ドキュメント内のコード例をテストできる。コード例の動作確認をすぐに行えるため、ソースコードの更新によりコード例が動作しなくなるという誤りが原則として発生しない。
ドキュメンテーションコメントの例
例えば、std::option::Option
の is_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連バッククォート ```
で囲まれたコードブロックは、このメソッドの使い方を示すためのソースコードの例である。このコード例は、文書内のコード例としてだけではなく、ドキュメンテーションテストのテストコードとしても扱われる。
生成されたドキュメントの例
上記のドキュメンテーションコメントからは、下記のドキュメントが生成される。
生成されたドキュメントには、生成元のソースコード全体がドキュメントの一部として同梱される。ドキュメントの各所にソースコードの該当箇所へのリンクが自動で設置されるので、ドキュメントから生成元のソースコードやドキュメンテーションコメントをすぐに参照できる。
また、モジュールやクレートに対するドキュメントには、それに含まれる構造体や関数などの構成要素の一覧が自動生成され付加される。例えば std::fs
モジュールのドキュメントは下記のとおり。
ドキュメント生成の手順
ドキュメントを生成する手順は下記のとおり。いずれかを行えばよい。
-
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.rs
やcargo 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)
-
CommonMark
- 早見表は CommonMark Reference を、現在の仕様は CommonMark Spec を参照のこと
-
rustdoc
によるいくつかの拡張: 取り消し線や脚注、表など
リンク
ドキュメンテーションコメント内では、他の要素のドキュメントへのリンクを [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
というメソッド名を使う -
Bar
やfoo::Bar
、std::iter::Iterator
などといったパスは、そのドキュメンテーションコメントの対象の要素の置かれた位置を起点にして推定する-
rustdoc
の管理対象のソースコード内にそのパスの要素(例えばBar
やfoo::Bar
)が存在すれば、その要素をリンク先とする - 標準ライブラリ内にそのパスの要素(例えば
std::iter::Iterator
)が存在すれば、その要素をリンク先とする
-
-
[`foo()`]
などと末尾に()
を付けて書くと、関数foo
を探す -
[`foo!`]
などと末尾に!
を付けて書くと、マクロfoo!
を探す
リンクに関する詳細は下記を参照のこと。
- Linking to items by name - The rustdoc book
- RFC1946: Intra Rustdoc Links - The Rust RFC Book
- 文章に関係する項目へのリンクを含める (C-LINK) - Rust APIガイドライン (原文)
コードブロック
ドキュメンテーションコメント内の3連バッククォート (```
) で囲まれたコードブロックは、Markdown記法のコードブロックとしてドキュメントに反映されるだけでなく、ドキュメンテーションテストのテストコードとしても扱われる。例えば下記のように記述した場合は(std::option::Option
の is_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つのコード例
- このコード例は、読者がコピー&ペーストして試せるほうがよい
- 必要に応じて、より進んだ話題
例えば、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." が転記されている。
設置が推奨されるセクション
個別のドキュメンテーションコメントには、内容に応じていくつかのセクション(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-CRATE-DOC) - Rust APIガイドライン
- 「RFC 1687参照」とのみ記載されている。リンク先はRFCの完成稿ではなくpull request
- Unresolved questions - API doc frontpage styleguide
その他の推奨事項
リンクを張る
関連する要素の説明には、リンクが張ってあるほうがよい。
(参照: 文章に関係する項目へのリンクを含める (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(())
/// # }
/// ```
詳細は下記を参照のこと。
- (コード例がtry!やunwrapではなく?を使っている (C-QUESTION-MARK) - Rust APIガイドライン)
- Using
?
in doc tests - The rustdoc book
記述例の探し方
Rustのドキュメンテーションコメントは言語標準の文書化手段なので、標準ライブラリも、ソースコードを公開しているRustパッケージも、ソースコードに対するドキュメントは原則としてすべてドキュメンテーションコメントで書かれている。
標準ライブラリのドキュメントは、下記にて参照できる。対応するドキュメンテーションコメントは、ドキュメント内の各所に置かれたソースコードへのリンク先にて確認できる。
Rust標準のパッケージレジストリは crates.io で、そこに登録されたパッケージのドキュメントは Docs.rs に自動生成されるようになっている。パッケージ名を特定してドキュメントやドキュメンテーションコメントを参照すればよい。例えば clap であれば、下記に置かれている。
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