Claude Codeでrustdocの警告とintra-docリンク切れを直す
cargo docの警告をエラー扱いにして、Claude Codeにintra-docリンク切れを直させる手順です。リンク・記法・公開範囲の3系統に分け、doctestをignoreで黙らせない権限設計まで示します。
rustdocの警告をClaude Codeに直させるなら、RUSTDOCFLAGS="-D warnings" cargo doc --no-deps と cargo test --doc の2本をStop hookに置くのが近道です。どちらかが失敗すればClaudeは止まれず、出力を読んで直し続けます。
ただし警告の中身で、直してよい場所が違います。存在しない名前へのリンク切れはコメントの修正で消えます。一方で「公開ドキュメントが非公開の項目にリンクしている」警告は、直し方の1つが可視性の変更で、公開APIに手が入ります。ここでは警告を3系統に分け、doctestを ignore で黙らせる抜け道を塞ぎながら、Claudeに触らせる範囲をコメントとdoctestに絞る設計を示します。
rustdocの警告は3系統に分かれる
rustdocは通常のlintと同じ仕組みで、ドキュメント専用の検査を持っています。allow / warn / deny の3段階で、クレートの先頭に #![deny(rustdoc::broken_intra_doc_links)] のように書けます。なお missing_docs を除き、これらのlintは rustc ではなく rustdoc を実行したときだけ有効です。つまり cargo check や cargo clippy を通っても、cargo doc で初めて警告が出ます。
Claudeに任せる観点では、次の3系統で考えると整理しやすくなります。
| 系統 | 主なlint | 既定 | 直す場所 | Claudeに任せてよいか |
|---|---|---|---|---|
| リンク | 主なlintbroken_intra_doc_links | 既定warn | 直す場所/// 内のリンク記法 | Claudeに任せてよいか任せやすい |
| 記法 | 主なlintbare_urls / redundant_explicit_links / invalid_html_tags / invalid_rust_codeblocks | 既定warn | 直す場所/// 内のMarkdown | Claudeに任せてよいか任せやすい |
| 記法(要有効化) | 主なlintunescaped_backticks / missing_docs | 既定allow | 直す場所/// 内、または新規コメント | Claudeに任せてよいか有効化は人が決める |
| 公開範囲 | 主なlintprivate_intra_doc_links | 既定warn | 直す場所可視性かリンク先 | Claudeに任せてよいか方針を人が決める |
既定が allow のlintは、警告が出ていないのではなく見ていないだけです。「警告ゼロ」を完了条件にしても、unescaped_backticks や missing_docs は何も言いません。見たい検査があるなら、クレートの先頭か RUSTDOCFLAGS で明示的に有効にします。
警告をエラーにしてClaudeに出力を読ませる
Claudeが直すには、警告が「失敗」として返る必要があります。cargo doc は警告があっても終了コード0で終わるからです。方法は2つあります。
警告をエラーにする2つの方法
コマンド側で一括
RUSTDOCFLAGS="-D warnings" を付けて実行します。警告になるものがすべてエラーになり、ソースは触りません。Claude用の検査だけを厳しくしたいときに向きます。
ソース側で個別
#![deny(rustdoc::broken_intra_doc_links)] をクレート先頭に書きます。人がローカルで cargo doc を回しても同じ基準が効きます。対象のlintを絞れます。
RUSTDOCFLAGS は、Cargoが実行するすべてのrustdocに渡されるフラグです。-D warnings のような -D 指定の挙動はrustcのlint水準と同じで、warnings グループは「そのままでは警告になるすべてのlint」をまとめて指します。
実行コマンドは次の形です。
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --document-private-items--no-deps は依存クレートのドキュメントを作りません。依存側のrustdocを走らせずに済むので、自分のクレートの警告だけが出力に残ります。--document-private-items は非公開の項目も出力に含めるオプションで、つけると、公開ドキュメントから非公開項目へのリンクが「解決はするが警告は出る」状態になります。これは後で触れる private_intra_doc_links の挙動と関係します。
ワークスペースでは、--workspace か -p <パッケージ> で対象を選びます。選択がなければ、ルートがワークスペースの場合は既定メンバー、そうでなければそのパッケージだけが対象です。
broken_intra_doc_links を直す
intra-docリンクは、[`Bar`] のようにアイテム名を角括弧で囲むだけで、rustdocがそのアイテムのページへのリンクにしてくれる機能です。解決できないと警告になります。rustdocのドキュメントに載っている例だと、次のような出力です。
warning: unresolved link to `Nonexistent`
--> test.rs:1:24
|
1 | /// I want to link to [`Nonexistent`] but it doesn't exist!
| ^^^^^^^^^^^^^ no item named `Nonexistent` in `test`直し方は原因で変わります。原因を3つに分けて、Claudeに見分けさせます。
原因1: スコープにない、またはパスが違う
リンクは「スコープにあるもの」を指せます。Self / self / super / crate を含むパスも使えます。別モジュールの型を、use もパスもなしで書くと解決できません。
/// 設定を読み込みます。失敗したときは [`ConfigError`] を返します。
pub fn load() -> Result<Config, crate::error::ConfigError> {
todo!()
}この例では ConfigError がスコープにないため、リンク切れになります。直し方は [`crate::error::ConfigError`] のようにパスを足すか、ファイルに use を足すかです。ここで use を足すと、コードに変更が入ります。コメントだけで済ませたいので、パスを足す方法を選ぶ、とCLAUDE.mdに書いておきます。
ジェネリクスは [`Vec<T>`] と書いても [`Vec<T>`](Vec) と同じに解決されます。完全修飾構文(<Vec as IntoIterator>::into_iter())は、まだ対応していません。
原因2: 同じ名前が複数ある(曖昧さ)
Rustのパスには型・値・マクロの3つの名前空間があり、名前が重なると曖昧だと警告されます。構造体 Foo と関数 Foo が両方ある場合などです。このときは警告のhelpに「列挙型へリンクするなら enum@Foo、関数なら Foo()」のように候補が出ます。
使える接頭辞は struct / enum / trait / union / mod / const / fn / field / variant / method / derive / type / value / macro / prim などです。関数は名前の後ろに ()、マクロは ! を付けても区別できます。
/// 参照: [`Foo`](struct@Foo)
struct Bar;
/// こちらは [`Foo`](fn@Foo) とは別物です
struct Foo {}
fn Foo() {}表示では接頭辞が取り除かれて、Foo とだけ出ます。曖昧さの警告は、helpの提案をそのまま採用すれば機械的に消せる種類です。Claudeに向いた作業です。
トレイトと、同名のderiveマクロの両方に解決される場合だけ例外で、自動的にトレイト側へリンクされます。Clone が代表例です。derive側を指したいときは macro@ を使います。
原因3: 警告が出ない(だから見逃す)リンク
警告が出る一方で、警告が出ないのにリンクが生成されていない場合があります。公式の説明にある範囲は次のとおりです。
/や[]を含むものは、intra-docリンクと見なされず、解決に失敗しても警告されない- 別クレートから再エクスポートされた項目のリンクが解決できなくても、警告は出ない
cfgの裏にある項目など、リンクが生成できない場合は、書き方によって表示が変わる。[a]や[b][c]は書いたとおり(角括弧付き)に出力され、[d](e)と参照形式の[f]は指定先へのリンクになる
つまり、cargo doc の警告ゼロは、リンクが全部生きている証明ではありません。公開前の最終確認では、生成されたHTMLを別の手段で調べる選択肢があります。URLのリンク切れ検査はlycheeでリンク切れを直す手順が参考になります。
記法系の警告はコメント内の書き直しで消える
リンク以外にも、/// の中身を検査するlintがあります。これらは機械的に直せるので、まとめてClaudeに任せやすい部類です。
| lint | 何を検出するか | 直し方 |
|---|---|---|
bare_urls | 何を検出するかリンクになっていないURL | 直し方<https://example.org> の形にする |
redundant_explicit_links | 何を検出するか自動リンクと同じ明示リンク | 直し方[`usize`](usize) を [`usize`] にする |
invalid_html_tags | 何を検出するか閉じていない・開いていないHTMLタグ | 直し方タグを閉じる、またはコードスパンに入れる |
invalid_rust_codeblocks | 何を検出するか空、またはRustとして解釈できないコードブロック | 直し方中身を直す。Rustでない内容は言語指定を変える |
unescaped_backticks | 何を検出するか閉じていないバッククォート(既定はallow) | 直し方閉じ忘れを直す |
redundant_explicit_links は、明示リンクが自動リンクと同じ宛先なら削れるという検査です。「リンクが冗長」という警告を、リンクごと消してしまうと情報が減るので、[`usize`](usize) を [`usize`] に縮める、とCLAUDE.mdに書いておくとぶれません。
invalid_rust_codeblocks には注意点があります。このlintは、空のブロックや解析できないブロックを拾います。Rustではない内容(設定ファイルや出力例)なら、言語指定を text などにすれば警告は消えますが、そのブロックはdoctestの対象から外れます。Rustのコード例を text に変えて黙らせると、テストが1本減るだけです。この点は次の節で扱います。
doctestを通す — ignore に逃がさせない
rustdocはドキュメント中のRustコードブロックを、テストとしてコンパイルして実行します。cargo test --doc が入口です。--doc はライブラリのドキュメントだけをテストし、他のターゲット指定とは併用できません。
doctestが落ちたときの直し方には、実質的に3通りあります。
- 例のコードを直す(本来の修正)
- 不要な準備コードを隠し行にする(
#を行頭につける) ignoreを付けてテストから外す(逃げ道)
3つ目は、警告を黙らせる最短手です。しかし ignore は、そのコード例が実在のAPIと合っているかをもう検査しません。Claudeが「テストを通す」ことだけを目標にすると、この手に流れる余地があります。
関連する属性の意味を整理すると、使い分けが見えます。
| 属性 | コンパイル | 実行 | 向く場面 |
|---|---|---|---|
| (なし) | コンパイルする | 実行する | 向く場面通常の例 |
no_run | コンパイルする | 実行しない | 向く場面ネットワークや外部資源を使う例 |
should_panic | コンパイルする | 実行する(panicを期待) | 向く場面panicを示す例 |
compile_fail | コンパイルする(失敗を期待) | 実行しない | 向く場面「これは書けない」ことを示す例 |
ignore | コンパイルしない | 実行しない | 向く場面どうしても検査できない例(理由を添える) |
ignore には理由を括弧で添える慣習があります。コードブロックの開始行を ignore (needs extra dependency) のように書く形です。外部サービスに接続する例なら ignore ではなく no_run にします。コンパイルは通るので、APIの変更を検知できます。
例を短く保つには、隠し行が使えます。# で始まる行は出力から隠れますが、コンパイルには含まれます。
/// 設定を読み込みます。
///
/// ```
/// # use mycrate::{load, Config};
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let config: Config = load()?;
/// # Ok(())
/// # }
/// ```
pub fn load() -> Result<Config, ConfigError> { todo!() }? を使う例は、末尾に Ok::<(), Error>(()) を置く書き方でも対応できます。(()) は空白を挟まず1続きで書く必要があります。
doctestの警告を確認したいときは、cargo test --doc -- --show-output を使います。既定では「未使用」の警告は隠されます。例が非公開関数を使うことが多いためです。見たい場合は、例の先頭に #![warn(unused)] を書くか、クレートルートで #![doc(test(attr(warn(unused))))] を使います。
2024 editionでは、互換性のあるdoctestが1つにまとめてコンパイルされます。時間がかかるのはコンパイルの段階なので、まとめてコンパイルすると速くなります。ただし実行は各doctestごとに別プロセスです。
Stop hookで完了条件にする
Claude CodeのStop hookは、終了コード2を返すと停止を防ぎ、stderrの内容をClaudeへのフィードバックにします。これで、2つのコマンドの出力をそのまま読ませます。
.claude/hooks/rustdoc-check.sh を作ります。
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 0
OUT=$(RUSTDOCFLAGS="-D warnings" cargo doc --no-deps 2>&1) || {
echo "$OUT" | tail -60 >&2
exit 2
}
OUT=$(cargo test --doc 2>&1) || {
echo "$OUT" | tail -60 >&2
exit 2
}
exit 0.claude/settings.json には、Stopイベントとして登録します。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/rustdoc-check.sh"
}
]
}
]
}
}Stop hookの入力には stop_hook_active があり、すでにhookによる続行中なら true です。これを見て早期終了する書き方もありますが、上のスクリプトでは入れていません。入れると、一度続行した後は警告が残っていても止まれるようになり、「警告ゼロ」が完了条件でなくなるからです。
無限ループの安全弁は、Claude Code側にあります。Stop hookが連続8回ブロックすると、次のブロックは無視されてターンが終わります。ツールを呼ぶたびにこのカウントはリセットされます。警告が多く、8回を超えて収束するクレートでは、環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で上限を上げられます。同じ型の設計は、TypeScript側ではTypeDoc警告を直す手順にあります。
tail -60 で切るのは、警告が数百件あるクレートで会話が埋まるのを避けるためです。60件を直せば次が見えるので、反復で減っていきます。
書き換えをコメントとdoctestに限る
CLAUDE.mdの指示は、守られない場合があります。権限と検査で補強します。まず権限です。
{
"permissions": {
"allow": [
"Edit(src/**)",
"Bash(cargo doc *)",
"Bash(cargo test --doc*)"
],
"deny": [
"Edit(Cargo.toml)",
"Edit(Cargo.lock)",
"Edit(.cargo/**)"
]
}
}.cargo/ を拒否に入れる理由は、build.rustdocflags が設定ファイル側にも置けるからです。Claudeに設定ファイルを書き換えさせると、警告を出さない設定に変えて「ゼロ」にできてしまいます。
次に、差分の検査を2つ目のhookにします。src の変更のうち、コメント以外の行と、新しく足された ignore を見つけたら止めます。
#!/bin/bash
cd "$CLAUDE_PROJECT_DIR" || exit 0
DIFF=$(git diff -U0 -- src | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)')
CODE=$(echo "$DIFF" | grep -vE '^[+-][[:space:]]*//' | grep -vE '^[+-][[:space:]]*$')
if [ -n "$CODE" ]; then
echo "src にコメント以外の変更があります。rustdoc の警告対応は //! と /// だけにしてください:" >&2
echo "$CODE" | head -20 >&2
exit 2
fi
if echo "$DIFF" | grep -qE '^\+.*```[a-z_,0-9-]*ignore'; then
echo "doctestにignoreが追加されています。no_runか、例の修正で対応してください。" >&2
exit 2
fi
exit 0これは行頭の記号でコメントを近似した簡易版です。空行の追加・削除はコメントの書き直しでよく起きるので、フィルタで除外しています。// で始まる通常のコメントも通ってしまいますし、/// の中のコード例を壊す変更は検知できません。そこは cargo test --doc が受け持ちます。厳密にしたい場合は、構文解析でコメント以外のトークン列が変わっていないことを比較する仕組みに置き換えます。
2つのhookは、Stop の hooks 配列に並べて登録します。
CLAUDE.mdに渡す方針
警告の種類ごとの扱いを、書いておきます。
## rustdocの警告の扱い
- broken_intra_doc_links: パスを足す。useは足さない。曖昧さは警告のhelpに従う
- bare_urls / redundant_explicit_links / invalid_html_tags: コメント内だけを直す
- private_intra_doc_links: 直さない。項目名とリンク元を一覧にして報告する
- doctestが落ちた場合: 例を直す。外部資源を使う例はno_run。ignoreは足さない
- 触らない: Cargo.toml / .cargo/ / lib.rs先頭の #![deny]や #![allow]private_intra_doc_links は、公開ドキュメントが非公開の項目にリンクしているときの警告です。直し方は、リンク先を公開するか、リンクをやめて文章にするかの2つで、公開APIの設計判断を含みます。--document-private-items を付けるとリンクは生成されますが、警告は残ります。そのため一覧で報告させ、人が決めます。
警告ゼロまでの反復
作業の流れは次の順番です。
警告ゼロまでの反復
- 1
現状を一覧にする
RUSTDOCFLAGS="-D warnings" cargo doc --no-depsを一度手で実行し、lint名ごとの件数を見ます。 - 2
リンクと記法から直させる
broken_intra_doc_linksと記法系は、コメントだけで直ります。CLAUDE.mdの方針を渡して依頼します。 - 3
公開範囲は一覧で受け取る
private_intra_doc_linksはClaudeに直させず、項目名とリンク元の一覧を出させます。 - 4
doctestを通す
cargo test --docを回し、落ちた例を直させます。ignoreは足させません。
Rustのコードそのものの修正(所有権まわり)はborrow checkerエラーを直す方針、検査コマンドの回し方はAxumのRust APIを開発する手順にあります。
まとめ
rustdocの警告は、リンク・記法・公開範囲の3系統に分けると、Claudeに任せる範囲が決まります。リンクと記法はコメントの修正で消え、公開範囲は設計判断です。この区別をCLAUDE.mdと権限に落とすのが、手戻りの少ない運用です。
もう1つの分かれ目はdoctestです。cargo doc が警告ゼロでも、ignore が増えていれば、例が実在のAPIと合っているかは誰も見ていません。ignore の追加と allow の追加をhookで止めておけば、「警告ゼロ」が見かけの数字でなく、検査された状態になります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- doc.rust-lang.org/rustdoc/lints.html
- doc.rust-lang.org/rustdoc/write-documentation/linking-to-items-by-name.html
- doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html
- doc.rust-lang.org/cargo/commands/cargo-doc.html
- doc.rust-lang.org/cargo/reference/environment-variables.html
- doc.rust-lang.org/rustc/lints/levels.html
- code.claude.com/docs/en/hooks