
# AST 重写

AST 重写就是带有替换模板的 AST 搜索。Greph会运行与[AST 搜索](https://greph.inline0.dev/zh/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` | 将重写后的文件内容打印到标准输出，不写入文件 |
| `--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`，`"\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()`：在 `originalContents !== rewrittenContents` 时返回 `true` 的辅助函数

## 何时使用重写

以下情况适合使用重写：

- 转换范围局部且基于结构（并非重构整个文件）。
- 你希望进行确定、可重复的重构，并且可以反复运行，直到内容不再变化。
- 你想在应用更改前先预览。
- 你想将重构集成到脚本或 CI 作业中，而无需调用外部二进制文件。

以下情况不适合使用重写：

- 转换依赖于 PHP-Parser 无法从单个文件解析的类型信息。
- 转换需要跨越文件边界添加或移除导入。
- 你需要的是格式化工具，而不是重构工具（请使用 PHP-CS-Fixer）。
