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 и upstream 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).