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

Переписывание 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 теряют отступы.
PHP
// before
$d = array(
    "a" => 1,  // trailing comment
);

// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];

Проверьте результат --dry-run на совпадениях с комментариями или вручную отформатированными литералами.

Многострочные и вложенные совпадения

Многострочные шаблоны работают так же, как однострочные. Замена выводится средством форматирования с подставленными метапеременными, поэтому формат измененного кода определяется средством форматирования, а не исходным совпадением.

Greph переписывает каждый файл за один проход и никогда не применяет два пересекающихся совпадения. Он обходит совпадения с конца файла и пропускает любое совпадение, перекрывающееся с уже обработанным. Если вложенные совпадения начинаются со смещений, не совпадающих друг с другом, внутреннее совпадение переписывается, а внешнее остается без изменений:

PHP
// 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 дают разные результаты.

Программное использование

PHP
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).