Hoppa till innehåll

Refaktorera kod strukturellt

Den här guiden visar hur du får en agent att göra en bred mekanisk ändring – en codemod, en massmigrering av ett API, ett mönster du vill bli av med – med strukturell sökning och omskrivning, där ändringen förbereds för granskning innan något skrivs.

ast_grep matchar kod efter form. ast_edit skriver om den. resolve verkställer eller kastar.

Strukturella verktyg täcker de språk som kodgrafen parsar: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB och Ada. De behöver tree-sitter-grammatikerna på plats, vilket de alltid är i en levererad build – en saknad grammatik stoppar servern i stället för att försämra sökningen.

Skriv aldrig om något du inte har tittat på. Be agenten att söka:

Hitta varje ställe där vi anropar dispose() på en nullable som vi just har null-kontrollerat.

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

Utdata är file:line plus den matchade källkoden för varje ställe.

Token Matchar
$NAME Vilken enskild nod som helst, fångad
$$$NAME Vilken följd av syskon som helst – argument, satser
$_ Vad som helst, fångat utan namn

Det som spelar roll: ett upprepat $NAME måste fånga samma text.

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

hittar ett skyddsvillkor som gör sig av med det det testade. Det hittar inte if (a != null) b.dispose() – vilket oftast är buggen du letade efter. Ingen textsökning kan uttrycka det.

Två saker att prova, i tur och ordning:

  1. Gör det mindre. foo($X) före if (a) { foo($X); }. Ett mönster som inte är giltig kod i sig är ofta inte den konstruktion du menade.
  2. Ange språket. language: härleds bara när path anger en enda fil.

Ett mönster som ingen parsningskontext accepterar rapporteras som oparsbart – verktyget säger det i stället för att rapportera ”inga träffar” för något som aldrig lästes så som det ser ut.

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

Resultatet är ett förslag:

(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.

Inget har skrivits till disk än. Läs listan, kontrollera ett par av ställena som sökningen rapporterade och kör sedan:

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

eller

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

Poängen med att göra det här strukturellt är att det är brett. Breda ändringar är precis där en omskrivning som ser rimlig ut bryter en typ. Be om diagnostik efter att du har verkställt:

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

file: "*" kör projektets egen kontroll (dart analyze, tsc --noEmit, cargo check, go build) i stället för en fråga per fil – rätt form av fråga efter en ändring som rörde fyrtio filer.

Säg att log(msg, level) blir log(level: level, message: msg).

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

Titta på träffarna. Om några av dem är en annan tvåarguments-log, snäva in mönstret genom att begränsa path till katalogen som äger den du menar, i stället för att försöka uttrycka skillnaden i mönstret.

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

Kör sedan resolve och därefter lsp(action: "diagnostics", file: "*").

Situation Bättre verktyg
Byta namn på en symbol lsp_rename – följer referenser, inte former
Hitta en sträng, en kommentar eller ett filnamn search (innehåll) / search_files (namn)
”Vad går sönder om jag ändrar det här?” Kodgrafen
Ett språk som grammatikerna inte täcker search plus edit