Zum Inhalt springen

Code strukturell refaktorieren

Diese Anleitung zeigt, wie du einen Agenten mit struktureller Suche und Umschreibung eine breite mechanische Änderung machen lässt — einen Codemod, eine flächige API-Migration, ein Muster, das weg soll — und die Änderung zur Prüfung bereithältst, bevor irgendetwas geschrieben wird.

ast_grep trifft Code nach der Form. ast_edit schreibt ihn um. resolve übernimmt oder verwirft.

Die strukturellen Werkzeuge decken die Sprachen ab, die der Code-Graph parst: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB und Ada. Sie brauchen die mitgelieferten tree-sitter-Grammatiken, und die sind in einem ausgelieferten Build immer da — eine fehlende Grammatik stoppt den Server, statt die Suche zu verschlechtern.

Schreib nichts um, das du dir nicht angesehen hast. Bitte den Agenten, zu suchen:

Finde jede Stelle, an der wir dispose() auf einem nullbaren Wert aufrufen, den wir gerade auf null geprüft haben.

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

Die Ausgabe ist file:line plus der getroffene Quelltext für jede Stelle.

Token Trifft
$NAME Ein beliebiger einzelner Knoten, erfasst
$$$NAME Eine beliebige Folge von Geschwisterknoten — Argumente, Anweisungen
$_ Beliebiges, unter keinem Namen erfasst

Worauf es ankommt: ein wiederholtes $NAME muss denselben Text erfassen.

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

findet eine Absicherung, die das freigibt, was sie geprüft hat. if (a != null) b.dispose() findet sie nicht — und genau das ist meist der Fehler, den du suchst. Keine Textsuche kann das ausdrücken.

Zwei Versuche, in dieser Reihenfolge:

  1. Mach es kleiner. foo($X) vor if (a) { foo($X); }. Ein Muster, das für sich kein gültiger Code ist, ist oft nicht das Konstrukt, das du meintest.
  2. Nenn die Sprache. language: wird nur abgeleitet, wenn path eine einzelne Datei nennt.

Ein Muster, das kein Parse-Kontext annimmt, wird als nicht parsebar gemeldet — das Tool sagt das, statt „keine Treffer“ für etwas zu melden, das nie so gelesen wurde, wie es aussieht.

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

Das Ergebnis ist ein Vorschlag:

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

Auf der Platte liegt noch nichts. Lies die Liste, prüfe ein paar der Fundstellen, die die Suche gemeldet hat, dann:

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

oder

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

Der Grund, das strukturell zu machen, ist die Breite. Breite Änderungen sind genau die Stelle, an der eine plausibel aussehende Umschreibung an einem Typ scheitert. Bitte nach dem resolve um Diagnosen:

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

file: "*" führt den eigenen Checker des Projekts aus (dart analyze, tsc --noEmit, cargo check, go build), nicht eine Abfrage pro Datei — die richtige Form der Frage nach einer Änderung, die vierzig Dateien berührt hat.

Angenommen, log(msg, level) wird zu log(level: level, message: msg).

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

Sieh dir die Treffer an. Sind einige davon ein anderes log mit zwei Argumenten, grenze das Muster ein, indem du path auf das Verzeichnis setzt, zu dem das gemeinte gehört, statt den Unterschied im Muster auszudrücken.

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

Dann resolve, dann lsp(action: "diagnostics", file: "*").

Situation Besseres Werkzeug
Ein Symbol umbenennen lsp_rename — es folgt Referenzen, nicht Formen
Eine Zeichenkette, einen Kommentar oder einen Dateinamen finden search (Inhalt) / search_files (Namen)
„Was bricht, wenn ich das ändere?“ Der Code-Graph
Eine Sprache, die die Grammatiken nicht abdecken search plus edit