
# AST пренаписване

AST пренаписването е AST търсене с шаблон за замяна. Greph използва същия механизъм за съпоставяне като [AST търсенето](https://greph.inline0.dev/bg/docs/modes/ast), след което вмъква пренаписания шаблон обратно в изходния код на позициите в байтове, където е имало съвпадения. Околните интервали, отстъпите и коментарите се запазват; съвпадналият код се отпечатва отново (вижте [Вътре в съвпадение](#inside-a-match)).

## Кратки примери

```bash
# 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](https://github.com/inline0/greph/issues/4)). Първо проверявайте шаблоните с `--dry-run`.

## Режими

| Флаг | Поведение |
|---|---|
| (няма) | Прилага пренаписването към всеки файл със съвпадение |
| `--dry-run` | Извежда съдържанието на пренаписания файл на stdout, без да го записва |
| `--interactive` | Пита `Rewrite <file>? [y/N]` преди записването на всеки файл |

По подразбиране се „записва всичко, което се променя“. Първо използвайте `--dry-run`, докато настройвате шаблон, а след това премахнете флага, когато сте доволни от разликата.

## Запазване на форматирането

Greph не преформатира файла. Механизмът за съпоставяне записва байтовия диапазон на всяко съвпадение в оригиналния изходен код, а механизмът за пренаписване вмъква изобразения шаблон директно в тези диапазони. Непромененият код остава точно както е бил, включително:

- Отстъпите и крайните интервали
- Вградените и самостоятелните коментари
- Оформлението на празните редове
- Стилът на кавичките за низове и конкатенацията
- Стилът на отварящите и затварящите тагове на PHP

Това е съзнателен компромис спрямо пълното преформатиране: Greph няма да „подрежда“ код, който не е съпоставил, но и няма да внася шумни разлики в несвързани редове.

### Вътре в съвпадение

Самият съвпаднал диапазон се заменя с шаблона, изобразен чрез форматиращия модул на PHP-Parser, като уловените възли се отпечатват отново, вместо да се копират като изходен текст. Засега това означава ([#2](https://github.com/inline0/greph/issues/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](https://github.com/inline0/greph/issues/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).
