Переписування 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() самостійно не записує файли. За застосування змін (або їх пропуск у випадках --dry-run і відхилення --interactive) відповідає CLI. Це дає змогу безпечно викликати фасад з автоматизованих агентів, яким потрібно переглянути відмінності перед застосуванням змін.
RewriteResult надає:
file: абсолютний шляхoriginalContents: вміст файлу до переписуванняrewrittenContents: вміст файлу після переписуванняreplacementCount: кількість структурних замінchanged(): допоміжну функцію, яка повертаєtrue, якщоoriginalContents !== rewrittenContents
Коли використовувати переписування
Переписування — доречний інструмент, коли:
- Перетворення є локальним і структурним (не перебудовує весь файл).
- Вам потрібен детермінований, повторюваний рефакторинг, який можна запускати знову, доки зміни не припиняться.
- Ви хочете переглянути зміни перед застосуванням.
- Ви хочете додати рефакторинг до скрипту або завдання CI без виклику зовнішнього двійкового файла.
Переписування — недоречний інструмент, коли:
- Для перетворення потрібна інформація про типи, яку PHP-Parser не може отримати з одного файлу.
- Перетворення має додавати або вилучати імпорти за межами файлу.
- Вам потрібен форматувальник, а не рефакторинг (використовуйте PHP-CS-Fixer).