コンテンツにスキップ

構造的にコードをリファクタ

このガイドでは、構造検索と構造書き換えを使って、エージェントに広範囲の機械的な変更 — codemod、一括の API 移行、消し去りたいパターン — を加えさせる方法を説明します。変更は、何かが書き込まれる前にレビュー用にステージングされます。

ast_grep はコードを形でマッチします。ast_edit はそれを書き換えます。resolve がコミットするか破棄します。

構造ツールが扱うのは、コードグラフがパースする言語です: Dart、JavaScript、TypeScript、TSX、PHP、Python、Rust、Zig、C、C++、Go、Java、Ruby、C#、Swift、Kotlin、R、Assembly、MATLAB、Ada。これらは tree-sitter の文法がステージされている必要がありますが、出荷ビルドでは常にそうなっています — 文法が欠けていると、検索が劣化するのではなくサーバーが停止します。

見ていないものを書き換えてはいけません。エージェントに検索を頼みます:

null チェックしたばかりの nullable に対して dispose() を呼び出している箇所をすべて見つけて。

ast_grep(pattern: "if ($X != null) $X.dispose()", language: "dart")

出力は、各箇所の file:line と一致したソースです。

トークン マッチするもの
$NAME 任意の単一ノード(捕捉される)
$$$NAME 任意の連続する兄弟 — 引数、文
$_ 何にでもマッチ(名前を付けずに捕捉される)

重要なのはこれです: 繰り返された $NAME は同じテキストを捕捉しなければなりません。

if ($X != null) $X.dispose()

これは、検査したものを破棄するガードを見つけます。if (a != null) b.dispose() は見つけません — そしてそれは通常、探していたバグそのものです。テキスト検索ではこれを表現できません。

順に試すべきことが 2 つあります:

  1. 小さくする。 if (a) { foo($X); } より先に foo($X)。それ自体では有効なコードにならないパターンは、多くの場合、意図した構文ではありません。
  2. 言語を明示する。 language: が推論されるのは、path が単一のファイルを指すときだけです。

どのパースコンテキストも受け入れないパターンは、パース不能として報告されます — 見た目どおりには一度も読まれていないものに対して「マッチなし」と報告するのではなく、ツールがそう告げます。

ast_edit(
pattern: "if ($X != null) $X.dispose()",
rewrite: "$X?.dispose()",
language: "dart",
)

結果は提案です:

(proposed) ast_edit: if ($X != null) $X.dispose() → $X?.dispose()
12 replacements in 4 files:
lib/features/a/x.dart (3)
lib/features/b/y.dart (5)
…
Nothing has been written. Call resolve with edit_id "edit_1" and action
"accept" to apply it, or "discard" to drop it.

まだディスクには何も書き込まれていません。リストを読み、検索が報告した箇所をいくつか確認してから、次を実行します:

resolve(edit_id: "edit_1", action: "accept")

または

resolve(edit_id: "edit_1", action: "discard")

これを構造的に行う要点は、変更が広範囲に及ぶことです。広範囲の変更こそ、もっともらしく見える書き換えが型を壊す場所です。resolve の後に診断を要求します:

lsp(action: "diagnostics", file: "*")

file: "*" はファイル単位のクエリではなく、プロジェクト自身のチェッカー(dart analyze、tsc --noEmit、cargo check、go build)を実行します — 40 ファイルに触れた変更の後にふさわしい問いの形です。

log(msg, level) が log(level: level, message: msg) になるとします。

ast_grep(pattern: "log($MSG, $LEVEL)", language: "dart")

ヒットを確認します。その一部が別の 2 引数の log であるなら、違いをパターンで表現しようとするのではなく、path を目的の log を所有するディレクトリに絞ってパターンを狭めます。

ast_edit(
pattern: "log($MSG, $LEVEL)",
rewrite: "log(level: $LEVEL, message: $MSG)",
path: "lib/features/telemetry",
language: "dart",
)

次に resolve、そして lsp(action: "diagnostics", file: "*") です。

状況 適したツール
シンボルの名前変更 lsp_rename — 形ではなく参照をたどります
文字列、コメント、ファイル名の検索 search(内容)/ search_files(名前)
「これを変えると何が壊れるか?」 コードグラフ
文法が対応していない言語 search と edit