Перейти до вмісту

Рефакторити код структурно

У цьому посібнику показано, як змусити агента виконати широку механічну зміну — codemod, масову міграцію API, патерн, якого ви хочете позбутися — за допомогою структурного пошуку й перезапису, причому зміну підготовлено для перегляду, перш ніж щось буде записано.

ast_grep зіставляє код за формою. ast_edit його переписує. resolve фіксує або відкидає.

Структурні інструменти охоплюють мови, які розбирає граф коду: Dart, JavaScript, TypeScript, TSX, PHP, Python, Rust, Zig, C, C++, Go, Java, Ruby, C#, Swift, Kotlin, R, Assembly, MATLAB і Ada. Їм потрібні підготовлені граматики tree-sitter, а в готовій збірці вони завжди такі: відсутня граматика зупиняє сервер, а не погіршує пошук.

Ніколи не переписуйте те, чого не бачили. Попросіть агента виконати пошук:

Знайди кожне місце, де ми викликаємо dispose() у nullable, який щойно перевірили на null.

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

Вивід — це file:line плюс зіставлений вихідний код для кожного місця.

Токен Зіставляє
$NAME Будь-який окремий вузол, захоплений
$$$NAME Будь-яка послідовність сусідів — аргументи, інструкції
$_ Будь-що, захоплене без імені

Найважливіше: повторений $NAME має захопити той самий текст.

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

знаходить захисну умову, яка звільняє саме те, що перевірила. Він не знаходить if (a != null) b.dispose() — а це зазвичай і є та помилка, яку ви шукали. Жоден текстовий пошук цього не виразить.

Дві речі, які варто спробувати, у такому порядку:

  1. Зробіть його меншим. Спершу foo($X), а вже потім if (a) { foo($X); }. Шаблон, який сам по собі не є коректним кодом, часто виявляється не тією конструкцією, яку ви мали на увазі.
  2. Назвіть мову. language: виводять лише тоді, коли path указує на один файл.

Шаблон, якого не приймає жоден контекст розбору, позначають як непридатний до розбору — інструмент каже про це прямо, а не повідомляє «збігів немає» для того, що ніколи не читалося так, як виглядає.

Перепишіть — і подивіться, перш ніж зміна потрапить на диск

Розділ із назвою «Перепишіть — і подивіться, перш ніж зміна потрапить на диск»
ast_edit(
pattern: "if ($X != null) $X.dispose()",
rewrite: "$X?.dispose()",
language: "dart",
)

Результат — це пропозиція:

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

На диску поки нічого немає. Прочитайте список, перевірте кілька місць, які повідомив пошук, і тоді:

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

або

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

Сенс робити це структурно в тому, що зміна широка. А широкі зміни — саме те місце, де правдоподібний на вигляд перезапис ламає тип. Попросіть діагностику після фіксації:

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

file: "*" запускає власний механізм перевірки проєкту (dart analyze, tsc --noEmit, cargo check, go build), а не запит до одного файлу — саме така форма питання доречна після зміни, що торкнулася сорока файлів.

Припустімо, log(msg, level) стає log(level: level, message: msg).

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

Погляньте на знайдені збіги. Якщо частина з них — інший log із двома аргументами, звузьте шаблон, обмеживши path каталогом, якому належить потрібний вам, а не намагайтеся виразити цю різницю в самому шаблоні.

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

Потім resolve, а потім lsp(action: "diagnostics", file: "*").

Ситуація Кращий інструмент
Перейменування символу lsp_rename — він іде за посиланнями, а не за формами
Пошук рядка, коментаря чи назви файлу search (вміст) / search_files (назви)
«Що зламається, якщо я це зміню?» Граф коду
Мова, яку не покривають граматики search разом з edit