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 |
将重写后的文件内容打印到标准输出,不写入文件 |
--interactive |
写入每个文件前提示 Rewrite <file>? [y/N] |
默认行为是“写入所有发生变化的内容”。迭代调整模式时,先使用 --dry-run,对差异满意后再移除该标志。
保留格式
Greph不会重新格式化文件。匹配器会记录原始源代码中每个匹配项的字节范围,重写器再将渲染后的模板直接拼接到这些范围内。未触及的代码保持原样,包括:
- 缩进和尾随空白
- 行内注释和独立注释
- 空行布局
- 字符串引号样式和拼接方式
- PHP 开始/结束标签样式
这是相对于全面重新格式化的一项有意取舍:Greph不会“整理”未匹配的代码,但也不会在无关行中引入嘈杂的差异。
匹配内部
匹配范围本身会替换为经 PHP-Parser 的美化打印器渲染的模板;捕获的节点会重新打印,而不是作为源代码文本复制。目前这意味着(#2):
- 匹配内部未附加到后续节点的注释会被删除,例如数组最后一项后的尾随注释,或其他内容为空的数组中的注释。
- 附加到捕获节点的注释会保留,但会移到单独一行,这可能导致单行表达式变为多行。
- 匹配内部的换行不会保留:没有这类注释的多行
array(...)会变成单行[...]。 - 字面写法会规范化:
0x1F会变成0x1f,1_000会变成1000,"\x41"等转义序列会被写成对应字符("A"),灵活 heredoc 会失去缩进。
// before
$d = array(
"a" => 1, // trailing comment
);
// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];
请检查 --dry-run 输出,留意其中包含注释或手动格式化字面量的匹配项。
多行和嵌套匹配
多行模式的工作方式与单行模式相同。替换内容会由美化打印器渲染,并插入元变量,因此被替换代码的布局由打印器决定,而非原始匹配内容。
Greph会在一次遍历中重写每个文件,绝不会应用两个相互重叠的匹配项。它会从文件末尾向前遍历匹配项,并跳过与已经重写的匹配项重叠的内容。对于起始偏移不同的嵌套匹配,会重写内部匹配项,并保持外部匹配项不变:
// 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和上游的 sg 会产生不同结果。
编程方式使用
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)。