clone()とunwrap()を量産させない──Claude Code × Rust開発のCLAUDE.md完全設計

はじめに

Claude CodeにRustのコードを生成させると、コンパイルは通っているのにclone()unwrap()が大量に出てくることがある。エラーを沈黙させる最短路として使われるためだ。RustらしいコードをClaude Codeに書かせるには、CLAUDE.mdに「何をしてはいけないか」を書く。


Claude CodeがRustで間違えやすいパターン

clone()の乱用──借用で解決できるのに複製する

// Claude Codeが出しやすい(Vecごとclone)
fn process_names(names: Vec) -> Vec {
    names.clone().iter().map(|n| n.clone().to_uppercase()).collect()
}

// 正しいパターン(スライスとイテレータ)
fn process_names(names: &[String]) -> Vec {
    names.iter().map(|n| n.to_uppercase()).collect()
}

borrow checkerのエラーをclone()で黙らせる癖が出やすい。&[T]&strで渡せる場合、clone()は不要だ。

unwrap()の多用──エラー処理の強みを捨てる

// Claude Codeが出しやすい
fn read_config(path: &str) -> String {
    let content = fs::read_to_string(path).unwrap();
    serde_json::from_str::(&content).unwrap()["key"]
        .as_str().unwrap().to_string()
}

// 正しいパターン(anyhow + ?演算子)
use anyhow::{Context, Result};

fn read_config(path: &str) -> Result {
    let content = fs::read_to_string(path)
        .with_context(|| format!("設定ファイルの読み込みに失敗: {path}"))?;
    serde_json::from_str::(&content)
        .context("JSONパースに失敗")?["key"]
        .as_str().context("keyが文字列ではない")
        .map(|s| s.to_string())
}

tokio非同期の落とし穴

async fn内でstd::fsなどの同期I/Oを呼ぶと、tokioのスレッドをストールさせる。tokio::fsの非同期版を使うか、tokio::task::spawn_blocking()でブロッキング処理を包む。またtokio::sync::Mutexが必要なのはawaitをまたぐ場合のみで、それ以外はstd::sync::Mutexで十分だ。


Rust向けCLAUDE.md完全テンプレート

# Rustプロジェクト

技術スタック

Rustエディション: 2021 / 非同期ランタイム: tokio / エラー: lib→thiserror / app→anyhow

ビルド・テストコマンド

cargo check / cargo test / cargo fmt / cargo clippy -- -D warnings

所有権・借用

  • clone()は原則禁止。&T/&mut Tで解決できないか先に検討する
  • 関数引数は &[T] / &str を優先する(Stringを渡さない)
  • 構造体が参照するだけのフィールドにはライフタイム付き借用を使う
  • 'staticをライフタイムエラーの回避に使わない
  • エラー処理

  • unwrap()はテストコードのみ許可
  • expect()を使う場合はパニック理由を文字列で明記する
  • ライブラリ(lib.rs): thiserrorで型付きエラーを定義する
  • アプリ(main.rs, bin/): anyhowと?演算子で伝播する
  • エラーにはwith_context(|| format!(...))でコンテキストを付与する
  • 非同期(tokio)

  • async fn内でstd::fs等のブロッキングI/Oを使わない(tokio::fs or spawn_blocking)
  • Mutexはawaitをまたぐときのみtokio::sync::Mutexを使う
  • block_on()をtokioランタイム内から呼ばない(デッドロック原因)
  • 禁止パターン

  • 本番コードでのunwrap()
  • 理由のないclone()
  • 'staticをライフタイムエラーの回避手段として使うこと
  • async fn内でのブロッキングI/O

  • PostToolUseフック──cargo fmt + clippy自動実行

    .rsファイルを書き込むたびに自動でフォーマット+Lintが走る設定だ。

    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Write|Edit|MultiEdit",
            "hooks": [{ "type": "command", "command": "bash -c 'if [ -f Cargo.toml ]; then cargo fmt && cargo clippy -- -D warnings 2>&1; fi'" }]
          }
        ]
      }
    }

    clippyが警告を検出すると、Claude Codeがエラー出力を読んでコードを修正する。このフィードバックループにより、人間が毎回チェックしなくても品質が保たれる。

    CIにも同じゲートを設定する。

    # .github/workflows/quality.yml(主要ステップのみ)
    
  • run: cargo fmt --check
  • run: cargo clippy -- -D warnings
  • run: cargo test

  • EM視点:RustチームへのClaude Code導入

    導入初期は最低限の禁止事項2行から始める。「unwrap()はテストのみ」「clone()は理由をコメントで明記」だけで、Claude Codeの出力が変わる。1〜2ヶ月後、チームレビューで気になったパターンを追記する。thiserror/anyhowの使い分けや、プロジェクト固有の非同期パターンなどだ。安定期に入ったらclippy.tomlでプロジェクト固有のlintを追加し、-D clippy::unwrap_usedのように特定パターンをCIで強制する。

    Rustのborrow checkerとclippyは即座にエラーを返す。他の言語と違い、Claude Codeが変なコードを書いてもコンパイラが自動で検出する。人間のレビューは設計判断とドメインロジックに集中できる。


    まとめ

    clone()unwrap()が出てくる根本原因は、borrow checkerとエラー処理のルールをCLAUDE.mdで伝えていないことだ。まず「unwrap()はテストのみ」「clone()は理由をコメントで明記」の2行を追記して、PostToolUseフックでclippyを自動実行する設定を入れてほしい。Rustのコンパイラがそれ以降の品質を担保してくれる。

    コメント

    タイトルとURLをコピーしました