
# Переписування AST

Переписування AST — це пошук AST із шаблоном заміни. Greph використовує той самий механізм зіставлення, що й [пошук AST](https://greph.inline0.dev/uk/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` і попереднього `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).
