Ga naar inhoud

Code structureel refactoren

Deze gids laat zien hoe je een agent een brede, mechanische wijziging laat doorvoeren — een codemod, een bulk-API-migratie, een patroon dat je weg wilt — met structureel zoeken en herschrijven, waarbij de wijziging klaarstaat voor review voordat er iets wordt geschreven.

ast_grep matcht code op vorm. ast_edit herschrijft die. resolve voert de wijziging door of gooit haar weg.

Structurele tools dekken de talen die de codegraph parseert: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB en Ada. Ze hebben de tree-sitter-grammatica’s klaargezet nodig, wat in een uitgebrachte build altijd zo is — een ontbrekende grammatica stopt de server in plaats van het zoeken te verslechteren.

Herschrijf nooit iets waar je niet naar hebt gekeken. Vraag de agent om te zoeken:

Vind elke plek waar we dispose() aanroepen op een nullable die we net op null hebben gecontroleerd.

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

De uitvoer is file:line plus de gematchte broncode voor elke plek.

Token Matcht
$NAME Elk afzonderlijk knooppunt, vastgelegd
$$$NAME Elke reeks broers en zussen — argumenten, statements
$_ Alles, zonder naam vastgelegd

Degene die ertoe doet: een herhaalde $NAME moet dezelfde tekst vastleggen.

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

vindt een guard die opruimt wat hij heeft getest. Hij vindt niet if (a != null) b.dispose() — en dat is meestal precies de bug die je zocht. Geen enkele tekstuele zoekopdracht kan dat uitdrukken.

Twee dingen om te proberen, in deze volgorde:

  1. Maak het kleiner. foo($X) vóór if (a) { foo($X); }. Een patroon dat op zichzelf geen geldige code is, is vaak niet het construct dat je bedoelde.
  2. Noem de taal. language: wordt alleen afgeleid als path naar één enkel bestand wijst.

Een patroon dat geen enkele parsecontext accepteert, wordt als onparseerbaar gemeld — de tool zegt dat, in plaats van “geen overeenkomsten” te melden voor iets dat nooit werd gelezen zoals het eruitziet.

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

Het resultaat is een voorstel:

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

Er staat nog niets op schijf. Lees de lijst, controleer een paar van de plekken die de zoekopdracht meldde, en dan:

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

of

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

Het punt van dit structureel aanpakken is dat het breed is. Brede wijzigingen zijn precies waar een ogenschijnlijk plausibele herschrijving een type breekt. Vraag na het doorvoeren om diagnostiek:

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

file: "*" draait de eigen checker van het project (dart analyze, tsc --noEmit, cargo check, go build) in plaats van een query per bestand — de juiste vorm van vraag na een wijziging die veertig bestanden raakte.

Stel dat log(msg, level) verandert in log(level: level, message: msg).

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

Bekijk de treffers. Als sommige ervan een andere log met twee argumenten zijn, beperk het patroon dan door path te richten op de map die de bedoelde bevat, in plaats van het verschil in het patroon te willen uitdrukken.

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

Dan resolve, dan lsp(action: "diagnostics", file: "*").

Situatie Betere tool
Een symbool hernoemen lsp_rename — die volgt referenties, geen vormen
Een string, een commentaar of een bestandsnaam vinden search (inhoud) / search_files (namen)
“Wat breekt er als ik dit wijzig?” De codegraph
Een taal die de grammatica’s niet dekken search plus edit