12k
All articles

DifftasticでAI生成コードをレビューする

DifftasticでAI生成コードを構造差分でレビューし、gitに組み込み、整形ノイズから振る舞いの変更だけを見分けます。

OpenReplay Team
OpenReplay Team
DifftasticでAI生成コードをレビューする

AIコーディングツールは、1つのコミットの中に実際のロジック変更とコード全体の再フォーマットをまとめて含めることが日常的にあります。そのような場合、行ベースのgit diffでは、動作を変える1行が大量の赤と緑の表示に埋もれてしまいます。Difftasticは、コードを構文木にパースしてテキストではなくツリーを比較する構造的なdiffツールです。これにより、純粋な再フォーマット、行の折り返し、空白の変更は変更なしとして扱われ、動作に影響する編集のみが表示されます。本記事では、difftasticの概要、gitへの組み込み方法、そしてエージェントが生成したコミットをレビューするための具体的なワークフローを解説します。

重要なポイント

  • Difftasticはtree-sitterを通じて抽象構文木(AST)を差分比較するため、再フォーマットや行の折り返しは変更なしとして扱われる一方、新たな引数が1つ追加された場合は変更として検出されます。
  • Difftasticは30以上の言語をサポートしており、未認識の拡張子やパースエラーが発生したファイルに対しては、行指向のワードハイライト付きdiffにフォールバックします。
  • ~/.gitconfig[diff] external = difftを追加することで組み込みが完了し、git show <commit> --ext-diffでエージェントのコミットを確認できます。推奨されるgitの設定はdifftastic 0.61(2024年10月)で変更されたため、マニュアルから最新の設定をコピーしてください。
  • Difftasticはgitのページャーではなく外部diffツールであるため、日常的な表示にはdeltaと組み合わせ、再フォーマットが多いAIのdiffにはdifftasticを使用することを推奨します。
  • difft --check-only --exit-codeを使用すると、「このコミットは動作を変更したのか、それとも単なる再フォーマットか?」という問いに基づいてコミットをゲートできます。

問題点:AIのdiffは重要な行を埋もれさせる

Cursor、GitHub Copilot、Claude Codeなどのエージェントやアシスタントは、1つの条件を変更するのと同じコミットで、関数全体を再フォーマットしたり、長い呼び出しを複数行に折り返したりすることがよくあります。行指向のdiffにはその違いを判別する手段がなく、変更されたすべての行を変更済みとしてマークします。その結果、大量の赤と緑の表示が生成され、ランタイムの動作を変える1つの編集がその中に埋もれてしまいます。AI生成コードのレビューは、シグナルとノイズの比率が最も重要な場面です。再フォーマットはマシンが生成したノイズであり、動作の変更こそがマージに対して責任を持つべき部分だからです。

Difftasticとは何か?

Difftasticは、テキストではなく構文に基づいてファイルを比較する構造的なdiffツールです。各バージョンをtree-sitterでパースして抽象構文木を構築し、そのツリーを差分比較します。実際的な効果として、コードを複数行にまたがるように再フォーマットした場合、difftasticは実際に変更された部分のみを表示します。また、ブロックが移動またはリネームされた場合は、削除と追加の組み合わせではなく、移動として報告されます。

DifftasticはRustで書かれており、Wilfred Hughesによって作成されたMITライセンスのオープンソースツールです。tree-sitterのパーサーを通じて30以上のプログラミング言語をサポートしています(一部のレビューで主張されている「50以上」ではありません)。認識されない拡張子のファイルに対しては、ワードハイライト付きの行指向diffに適切にフォールバックするため、パース対象外の言語でも問題なく動作します。

Difftasticのインストールとgitへの組み込み

パッケージマネージャーまたはcargodifftバイナリをインストールし、gitの外部diffとして登録します。

# macOS
brew install difftastic

# Rustツールチェーン(0.69時点でRust 1.85以上が必要)
cargo install difftastic

一時的に使用する場合は、設定をインラインで渡すことで恒久的な変更を避けられます。

git -c diff.external=difft diff
git -c diff.external=difft show --ext-diff

Difftasticをデフォルトに設定するには、マニュアルに従って~/.gitconfigに以下を追加します。

[diff]
    external = difft

これによりgit diffがdifftasticを使用するようになります。他のサブコマンドでは--ext-diffフラグを追加するだけで利用できます(例:git show --ext-diffgit log -p --ext-diff)。推奨される設定はdifftastic 0.61(2024年10月)で変更されたため、古いブログの設定をコピーしている場合は、現在のマニュアルのバージョンに置き換えてください。デフォルトの2カラム表示よりも--display inlineによるインライン表示を推奨します。

difft old.js new.js --display inline

AIコードレビューにdifftasticが優れている理由

構造的なdiffは変更された構文ノードに焦点を当て、本質的でない編集によるノイズを排除します。最も分かりやすい例が、再フォーマットと引数1つの追加が同時に行われるケースです。エージェントが1行の関数シグネチャを受け取り、行長制限を超えたためフォーマッターが複数行に折り返し、さらに引数を1つ追加したとします。

// before
function createUser(name, email) {}

// after
function createUser(
  name,
  email,
  role,
) {}

行diffではすべての行がテキスト的に変更されているため、ブロック全体がハイライトされます。一方、difftasticは動作を変える唯一の編集である新しいrole引数のみをハイライトします。これはまさに、ノイズの多いdiffをざっと確認しているレビュアーをエージェントが欺ける典型的な例です。同様のロジックは関数の抽出にも適用されます。エージェントがブロックを新しいヘルパー関数に切り出した場合、difftasticはラッパーと本体を1つの複雑な削除と追加として表示するのではなく、追加された新しい関数名としてフラグを立てます。

エージェント出力のコミットレビューワークフロー

エージェントが生成したセマンティックな変更のみを確認するには、対象のコミットを指定します。

git show <agent-commit> --ext-diff
# またはブランチ全体をレビューする場合
git log -p --ext-diff main..agent-branch

確認すべき点は3つあります。difftasticが実際に変更としてフラグを立てた追加と削除、削除と追加の組み合わせではなく移動またはリネームとして報告されたブロック、そして通常の行diffにフォールバックしたハンク(これはdifftasticが未認識の拡張子またはパースエラーに遭遇したシグナルです)。デフォルトでは、difftasticはパースエラーが発生した場合に行指向のdiffにフォールバックします。これは、構文的に異なる2つのファイルを同一と判断しないための保守的な選択です。リファクタリング途中のAI出力はクリーンにパースできない場合があるため、これは重要な動作です。ほぼ有効なファイルが不必要にフォールバックしてしまう場合は、export DFT_PARSE_ERROR_LIMIT=20で許容量を上げてください。

自動化のために、difftasticはdiffをレンダリングせずに2つのファイルが同じASTを共有しているかどうかを確認できます。

difft --check-only --exit-code before.js after.js

これは構文的な変更がない場合に終了コード0を、変更がある場合に1を設定します。「エージェントは動作を変更したのか、それとも単に再フォーマットしただけか?」という問いに答えるプレマージフックやCIステップのための高速なゲートとして機能します。

制限事項とdeltaとの組み合わせ

トレードオフについて正直に述べておきます。Difftasticは変更が多いファイルに対してスケーリングが比較的悪く、大量のメモリを使用する可能性があります。そのため、git diffの全面的な代替としてではなく、特定のコミットのレビューツールとして扱うべきです。サイドバイサイド表示は通常は良好ですが、まれに混乱を招くことがあります。また、外部diffツールであってgitのページャーではないため、GitHub PRのFilesタブや多くのIDEのdiffペイン内ではレンダリングされません。ローカルのCLIツールです。

実用的な解決策は両方を使用することです。日常的な表示には高速なテキストページャーを維持し、再フォーマットが多い複雑なdiffにはdifftasticに切り替えます。

ツールdiffの種類ページャーとして使用可能?最適な用途
difftastic構造的(AST)不可(外部diffのみ)再フォーマットが多いAIのdiff
deltaテキスト、シンタックスハイライト付き日常的なdiff
diff-so-fancyテキスト、整形済み軽量な表示改善

Deltaとdiff-so-fancyはテキストベースであり、どちらもASTのdiffは行いません。Deltaをデフォルトのページャーとして使用し、フォーマットの変更が多いdiffにはdifftasticを使用してください。

クリーンな構造的diffはプレマージのフィルターです。承認前に動作を変える行を検出します。それでも見落としてしまう変更——ノイズの多いAIのdiffに隠れた微妙なロジックの編集——は、リリース後に再現することになり、セッションリプレイで壊れたインタラクションを確認することが、このクラスのリグレッションを発見する最も直接的な方法の1つです。今すぐ[diff] external = difftを設定し、次のエージェントのコミットをgit show <commit> --ext-diffでレビューして、実際に変更された部分だけを確認してください。

FAQ

DifftasticとDeltaの違いは何ですか?

Difftasticはtree-sitterでコードをパースして抽象構文木を比較する構造的なdiffツールであり、再フォーマットや行の折り返しは変更なしとして扱われます。Deltaはテキストベースのシンタックスハイライト付きページャーであり、行単位のdiffを行います。Difftasticはgitのページャーとして機能できず、外部diffツールとしてのみ動作しますが、Deltaは日常的な表示のためのページャーとして機能します。デフォルトではDeltaを使用し、再フォーマットが多いAIのdiffにはDifftasticを使用してください。

Difftasticがパースできないファイルに遭遇した場合はどうなりますか?

Difftasticは2つのケースでワードハイライト付きの行指向diffにフォールバックします。ファイルの拡張子が認識されない場合と、パースエラーが発生した場合です。パースエラー時のフォールバックは意図的な保守的選択であり、構文的に異なる2つのファイルを同一と判断しないためのものです。これはリファクタリング途中のAI出力がクリーンにパースできない場合に重要です。ほぼ有効なファイルが不必要にフォールバックしてしまう場合は、DFT_PARSE_ERROR_LIMIT環境変数で許容量を上げてください。

DifftasticはGitHub PRのFilesタブ内でdiffを表示できますか?

いいえ。Difftasticはgitの外部diffツールとして動作するローカルのコマンドラインツールであり、GitHub PRのFilesタブや多くのIDEのdiffペイン内のレンダリングレイヤーとしては機能しません。`--ext-diff`フラグを付けた`git show`や`git log -p --ext-diff`などのコマンドを通じて実行します。magitなど一部のツール向けのサードパーティ統合は存在しますが、PRタブやIDEのサポートは限定的であるため、difftasticはターミナルベースのレビューステップとして扱うのが最善です。

AIのコミットが動作を変更したのか、単に再フォーマットしただけなのかを確認するにはどうすればよいですか?

ファイルの2つのバージョンに対して`--check-only`と`--exit-code`フラグを付けて`difft`を実行します。Difftasticはdiffをレンダリングせずにファイルが同じ抽象構文木を共有しているかどうかを確認し、構文的な変更がない場合は終了コード0を、変更がある場合は1を設定します。これは完全なdiffよりもはるかに高速であり、実際の動作変更と純粋な再フォーマットを区別するプレマージフックやCIステップのクリーンなゲートとして機能します。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.