Przejdź do głównej zawartości

Refaktoryzuj kod strukturalnie

W tym poradniku pokazujemy, jak skłonić agenta do szerokiej, mechanicznej zmiany — codemodu, masowej migracji API, wzorca, którego chcesz się pozbyć — za pomocą wyszukiwania i przepisywania strukturalnego, przy czym zmiana trafia do poczekalni, zanim cokolwiek zostanie zapisane.

ast_grep dopasowuje kod po kształcie. ast_edit go przepisuje. resolve zatwierdza albo odrzuca.

Narzędzia strukturalne obejmują języki, które parsuje graf kodu: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB i Ada. Wymagają przygotowanych gramatyk tree-sittera, co w wydanym buildzie zawsze ma miejsce — brakująca gramatyka zatrzymuje serwer, zamiast degradować wyszukiwanie.

Nigdy nie przepisuj czegoś, czego nie obejrzałeś. Poproś agenta o wyszukanie:

Znajdź każde miejsce, w którym wywołujemy dispose() na wartości nullowalnej, którą dopiero co sprawdziliśmy pod kątem null.

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

Wynik to file:line oraz dopasowany kod źródłowy dla każdego miejsca.

Token Dopasowuje
$NAME Dowolny pojedynczy węzeł, przechwycony
$$$NAME Dowolny ciąg rodzeństwa — argumenty, instrukcje
$_ Cokolwiek, przechwycone bez nazwy

Najważniejsze jest to: powtórzony $NAME musi przechwycić ten sam tekst.

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

znajduje warunek ochronny, który zwalnia to, co sprawdził. Nie znajdzie natomiast if (a != null) b.dispose() — a to zwykle właśnie ten błąd, którego szukałeś. Żadne wyszukiwanie tekstowe nie potrafi tego wyrazić.

Dwie rzeczy do wypróbowania, po kolei:

  1. Zmniejsz go. foo($X) przed if (a) { foo($X); }. Wzorzec, który sam w sobie nie jest poprawnym kodem, często nie jest tą konstrukcją, o którą Ci chodziło.
  2. Nazwij język. language: jest wywnioskowany tylko wtedy, gdy path wskazuje pojedynczy plik.

Wzorzec, którego nie przyjmuje żaden kontekst parsowania, jest zgłaszany jako nieparsowalny — narzędzie mówi o tym wprost, zamiast raportować „brak dopasowań” dla czegoś, co nigdy nie było czytane tak, jak wygląda.

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

Wynikiem jest propozycja:

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

Na dysku nie ma jeszcze nic. Przeczytaj listę, sprawdź kilka miejsc zgłoszonych przez wyszukiwanie, a następnie:

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

albo

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

Sens robienia tego strukturalnie polega na tym, że jest to zmiana szeroka. Szerokie zmiany to dokładnie to miejsce, w którym wiarygodnie wyglądające przepisanie psuje typ. Po zatwierdzeniu poproś o diagnostykę:

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

file: "*" uruchamia własne narzędzie sprawdzające projektu (dart analyze, tsc --noEmit, cargo check, go build), a nie zapytanie o pojedynczy plik — to właściwy kształt pytania po zmianie, która dotknęła czterdziestu plików.

Załóżmy, że log(msg, level) zmienia się w log(level: level, message: msg).

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

Przyjrzyj się trafieniom. Jeśli niektóre z nich to inny dwuargumentowy log, zawęź wzorzec, ograniczając path do katalogu, do którego należy ten właściwy, zamiast próbować wyrazić różnicę w samym wzorcu.

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

Potem resolve, a następnie lsp(action: "diagnostics", file: "*").

Sytuacja Lepsze narzędzie
Zmiana nazwy symbolu lsp_rename — podąża za odwołaniami, a nie za kształtami
Znalezienie łańcucha znaków, komentarza albo nazwy pliku search (zawartość) / search_files (nazwy)
„Co się zepsuje, jeśli to zmienię?” Graf kodu
Język, którego gramatyki nie obejmują search plus edit