Přeskočit na obsah

Refaktorujte kód strukturálně

Tenhle návod ukáže, jak od agenta dostat širokou mechanickou změnu — codemod, hromadnou migraci API, vzor, který má zmizet — strukturálním hledáním a přepisem, přičemž se změna nejdřív připraví k revizi a teprve potom se cokoli zapíše.

ast_grep hledá kód podle tvaru. ast_edit ho přepisuje. resolve zápis potvrdí, nebo ho zahodí.

Strukturální nástroje pokrývají jazyky, které parsuje graf kódu: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB a Ada. Potřebují připravené gramatiky tree-sitter a v dodávaném sestavení je mají vždy — chybějící gramatika zastaví server, místo aby se hledání potichu zhoršilo.

Nepřepisuj nic, na co ses nepodíval. Požádej agenta, ať hledá:

Najdi každé místo, kde voláme dispose() na nullable, který jsme právě zkontrolovali na null.

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

Výstup je file:line a ke každému místu shodný zdroj.

K čemu je syntaxe vzoru

Sekce “K čemu je syntaxe vzoru”
Token Shoda
$NAME Libovolný jeden uzel, zachycený
$$$NAME Libovolná řada sourozenců — argumenty, příkazy
$_ Cokoli, zachycené bez jména

Podstatné je tohle: zopakované $NAME musí zachytit tentýž text.

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

najde guard, který zavolá dispose na to, co právě otestoval. Nenajde if (a != null) b.dispose() — a to bývá právě ta chyba, kterou hledáš. Textem se taková věc vyjádřit nedá.

Zkus dvě věci, v tomhle pořadí:

  1. Zmenši ho. Nejdřív foo($X), až potom if (a) { foo($X); }. Vzor, který sám o sobě není platný kód, často není ten konstrukt, který jsi myslel.
  2. Pojmenuj jazyk. language: se odvodí jen tehdy, když path uvádí jeden soubor.

Vzor, který nepřijme žádný kontext rozboru, se nahlásí jako nerozparsovatelný — nástroj to řekne, místo aby u něčeho, co se nikdy nečetlo tak, jak vypadá, ohlásil „žádné shody“.

Přepiš a podívej se, než to dopadne

Sekce “Přepiš a podívej se, než to dopadne”
ast_edit(
pattern: "if ($X != null) $X.dispose()",
rewrite: "$X?.dispose()",
language: "dart",
)

Výsledek je návrh:

(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 disku ještě nic není. Přečti seznam, zkontroluj pár míst, která hledání nahlásilo, a pak:

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

nebo

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

Strukturálně se to dělá proto, že je to široké. Právě u širokých změn věrohodný přepis rozbije typ. Po potvrzení si vyžádej diagnostiky:

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

file: "*" spustí vlastní kontrolu projektu (dart analyze, tsc --noEmit, cargo check, go build), ne dotaz na každý soubor zvlášť — správný tvar otázky po změně, která sahá do čtyřiceti souborů.

Řešený příklad: migrace volání

Sekce “Řešený příklad: migrace volání”

Dejme tomu, že log(msg, level) přechází na log(level: level, message: msg).

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

Podívej se na shody. Když je část z nich jiný dvouargumentový log, zúž vzor omezením path na adresář, kterému patří ten, o který jde, místo abys ten rozdíl vyjadřoval ve vzoru.

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

Pak resolve, pak lsp(action: "diagnostics", file: "*").

Kdy sáhnout po něčem jiném

Sekce “Kdy sáhnout po něčem jiném”
Situace Lepší nástroj
Přejmenování symbolu lsp_rename — sleduje reference, ne tvary
Řetězec, komentář nebo název souboru search (obsah) / search_files (názvy)
„Co se rozbije, když tohle změním?“ Graf kódu
Jazyk, který gramatiky nepokrývají search a edit