Към съдържанието

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