Перейти к содержимому

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

Это руководство показывает, как заставить агента выполнить широкое механическое изменение — кодмод, массовую миграцию 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