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

Переписывание AST — это поиск по AST с шаблоном замены. Greph использует тот же механизм сопоставления, что и [поиск по AST](https://greph.inline0.dev/ru/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()` сам по себе не записывает файлы. За фиксацию изменений отвечает CLI (или за их пропуск в случаях `--dry-run` и отклонённых `--interactive`). Благодаря этому фасад безопасно вызывать из автоматизированных агентов, которые хотят проверить различия перед применением изменений.

`RewriteResult` предоставляет:

- `file`: абсолютный путь
- `originalContents`: содержимое файла до переписывания
- `rewrittenContents`: содержимое файла после переписывания
- `replacementCount`: число структурных замен
- `changed()`: помощник, возвращающий `true`, если `originalContents !== rewrittenContents`

## Когда использовать rewrite

Rewrite — подходящий инструмент, когда:

- Преобразование локальное и структурное (не перестраивает весь файл).
- Вам нужен детерминированный, воспроизводимый рефакторинг, который можно запускать повторно, пока изменения не прекратятся.
- Вы хотите просмотреть изменения перед применением.
- Вы хотите встроить рефакторинг в скрипт или задание CI без вызова внешнего исполняемого файла.

Rewrite — неподходящий инструмент, когда:

- Преобразование зависит от информации о типах, которую PHP-Parser не может получить из одного файла.
- Для преобразования нужно добавлять или удалять импорты за пределами файла.
- Вам нужен форматировщик, а не рефакторинг (используйте PHP-CS-Fixer).
