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

Переписування 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() самостійно не записує файли. За застосування змін (або їх пропуск у випадках --dry-run і відхилення --interactive) відповідає CLI. Це дає змогу безпечно викликати фасад з автоматизованих агентів, яким потрібно переглянути відмінності перед застосуванням змін.

RewriteResult надає:

  • file: абсолютний шлях
  • originalContents: вміст файлу до переписування
  • rewrittenContents: вміст файлу після переписування
  • replacementCount: кількість структурних замін
  • changed(): допоміжну функцію, яка повертає true, якщо originalContents !== rewrittenContents

Коли використовувати переписування

Переписування — доречний інструмент, коли:

  • Перетворення є локальним і структурним (не перебудовує весь файл).
  • Вам потрібен детермінований, повторюваний рефакторинг, який можна запускати знову, доки зміни не припиняться.
  • Ви хочете переглянути зміни перед застосуванням.
  • Ви хочете додати рефакторинг до скрипту або завдання CI без виклику зовнішнього двійкового файла.

Переписування — недоречний інструмент, коли:

  • Для перетворення потрібна інформація про типи, яку PHP-Parser не може отримати з одного файлу.
  • Перетворення має додавати або вилучати імпорти за межами файлу.
  • Вам потрібен форматувальник, а не рефакторинг (використовуйте PHP-CS-Fixer).