Переписывание AST
Переписывание AST — это поиск по AST с шаблоном замены. Greph использует тот же механизм сопоставления, что и поиск по AST, затем подставляет измененный шаблон в исходный код в диапазонах байтов найденных совпадений. Пробелы, отступы и комментарии вокруг них сохраняются; совпавший фрагмент кода выводится заново (см. Внутри совпадения).
Краткие примеры
# Convert array() literals to []
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' --dry-run src
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' src
# isset ternary -> null coalesce
./vendor/bin/greph -p 'isset($X) ? $X : $Y' -r '$X ?? $Y' --dry-run src
# Rename a method call
./vendor/bin/greph -p '$obj->oldName($$$ARGS)' -r '$obj->newName($$$ARGS)' src
# Confirm each file before writing
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' --interactive src
Шаблоны замены
В шаблоне замены используются те же метапеременные, что и в шаблоне поиска. Захваченные узлы подставляются по имени:
| В шаблоне | В замене | Результат |
|---|---|---|
$X |
$X |
Вставка захваченного узла |
$$$ARGS |
$$$ARGS |
Вставка последовательности захваченных узлов |
$NAME (идентификатор) |
$NAME |
Вставка захваченного идентификатора |
Метапеременные, присутствующие в шаблоне, но отсутствующие в замене, удаляются из переписанного кода. Сейчас наличие в шаблоне замены метапеременной, отсутствующей в шаблоне поиска, не вызывает ошибку: она выводится буквально, поэтому -r '$o->$m($$$A, $EXTRA)' записывает в код переменную $EXTRA (#4). Сначала проверьте шаблоны с помощью --dry-run.
Режимы
| Флаг | Поведение |
|---|---|
| (нет) | Применить замену ко всем файлам с совпадениями |
--dry-run |
Вывести содержимое измененного файла в stdout, не записывая его |
--interactive |
Запрашивать Rewrite <file>? [y/N] перед записью каждого файла |
По умолчанию все изменения записываются. Сначала используйте --dry-run, пока подбираете шаблон, затем уберите флаг, когда результат сравнения вас устроит.
Сохранение форматирования
Greph не форматирует файл заново. Механизм сопоставления фиксирует диапазоны байтов каждого совпадения в исходном коде, а механизм переписывания напрямую вставляет отрисованный шаблон в эти диапазоны. Нетронутый код остается точно таким же, включая:
- Отступы и пробелы в конце строк
- Встроенные и отдельные комментарии
- Расположение пустых строк
- Стиль кавычек и объединения строк
- Стиль открывающего и закрывающего тегов PHP
Это сознательный компромисс по сравнению с полным форматированием: Greph не будет «приводить в порядок» код, который не совпал с шаблоном, зато не создаст лишних изменений в несвязанных строках.
Внутри совпадения
Сам совпавший диапазон заменяется шаблоном, обработанным средством форматирования PHP-Parser. Захваченные узлы выводятся заново, а не копируются из исходного текста. Сейчас это означает следующее (#2):
- Комментарий, не привязанный к следующему узлу внутри совпадения, удаляется. Например, это завершающий комментарий после последнего элемента массива или комментарий в пустом массиве.
- Комментарии, привязанные к захваченным узлам, сохраняются, но переносятся на отдельную строку. Однострочное выражение может превратиться в несколько строк.
- Переносы строк внутри совпадения не сохраняются: многострочный
array(...)без таких комментариев превращается в однострочный[...]. - Запись литералов нормализуется:
0x1Fпревращается в0x1f,1_000— в1000, escape-последовательности, такие как"\x41", записываются как соответствующие символы ("A"), а гибкие heredoc теряют отступы.
// before
$d = array(
"a" => 1, // trailing comment
);
// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];
Проверьте результат --dry-run на совпадениях с комментариями или вручную отформатированными литералами.
Многострочные и вложенные совпадения
Многострочные шаблоны работают так же, как однострочные. Замена выводится средством форматирования с подставленными метапеременными, поэтому формат измененного кода определяется средством форматирования, а не исходным совпадением.
Greph переписывает каждый файл за один проход и никогда не применяет два пересекающихся совпадения. Он обходит совпадения с конца файла и пропускает любое совпадение, перекрывающееся с уже обработанным. Если вложенные совпадения начинаются со смещений, не совпадающих друг с другом, внутреннее совпадение переписывается, а внешнее остается без изменений:
// before
$b = array(1, array(2));
// after one run of -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$b = array(1, [2]);
// after a second run
$b = [1, [2]];
Если два совпадения начинаются с одного смещения (например, цепочка вызовов метода, совпавшая с $obj->$method($$$ARGS)), переписывается внешнее совпадение. Если шаблон может быть вложенным, повторяйте переписывание, пока --dry-run не перестанет ничего выводить (#3). ast-grep переписывает внешнее из двух вложенных совпадений, поэтому здесь vendor/bin/sg и вышестоящий sg дают разные результаты.
Программное использование
use Greph\Greph;
use Greph\Ast\AstSearchOptions;
$rewrites = Greph::rewriteAst(
'array($$$ITEMS)',
'[$$$ITEMS]',
'src',
new AstSearchOptions(jobs: 4),
);
foreach ($rewrites as $result) {
if (!$result->changed()) {
continue;
}
echo "{$result->file}: {$result->replacementCount} replacement(s)\n";
file_put_contents($result->file, $result->rewrittenContents);
}
Greph::rewriteAst() сам по себе не записывает файлы. За фиксацию изменений отвечает CLI (или за их пропуск в случаях --dry-run и отклонённых --interactive). Благодаря этому фасад безопасно вызывать из автоматизированных агентов, которые хотят проверить различия перед применением изменений.
RewriteResult предоставляет:
file: абсолютный путьoriginalContents: содержимое файла до переписыванияrewrittenContents: содержимое файла после переписыванияreplacementCount: число структурных заменchanged(): помощник, возвращающийtrue, еслиoriginalContents !== rewrittenContents
Когда использовать rewrite
Rewrite — подходящий инструмент, когда:
- Преобразование локальное и структурное (не перестраивает весь файл).
- Вам нужен детерминированный, воспроизводимый рефакторинг, который можно запускать повторно, пока изменения не прекратятся.
- Вы хотите просмотреть изменения перед применением.
- Вы хотите встроить рефакторинг в скрипт или задание CI без вызова внешнего исполняемого файла.
Rewrite — неподходящий инструмент, когда:
- Преобразование зависит от информации о типах, которую PHP-Parser не может получить из одного файла.
- Для преобразования нужно добавлять или удалять импорты за пределами файла.
- Вам нужен форматировщик, а не рефакторинг (используйте PHP-CS-Fixer).