AST 搜索
Greph的 AST 模式按结构而非文本搜索 PHP 源代码。模式使用带有 $VARIABLE 和 $$$VARIADIC 元变量的普通 PHP 代码编写。模式由 nikic/php-parser解析为与待搜索源代码相同的 AST 结构,然后逐节点匹配。
这与 ast-grep采用相同的模型,而 sg兼容性封装则基于同一引擎提供 ast-grep的 CLI接口。
快速示例
# Find every constructor call
./vendor/bin/greph -p 'new $CLASS()' src
# Find every method call with any number of arguments
./vendor/bin/greph -p '$obj->$method($$$ARGS)' src
# Find old-style array() calls
./vendor/bin/greph -p 'array($$$ITEMS)' src
# Find void-returning functions
./vendor/bin/greph -p 'function $name($$$PARAMS): void {}' src
# Find isset ternaries that should be ??
./vendor/bin/greph -p 'isset($x) ? $x : $default' src
模式语法
模式是有效的 PHP 源代码。凡是可以放置实际表达式或语句的位置,都可以用元变量替代,以捕获该节点。
| 语法 | 匹配内容 | 示例 |
|---|---|---|
$VAR |
任意单个 AST 节点,捕获为 VAR |
$x + $y 匹配 foo() + bar |
$_ |
任意单个 AST 节点,不捕获 | $_->method() 匹配任意接收者 |
$$$ARGS |
零个或多个节点(可变参数),捕获为 ARGS |
func($$$ARGS) 匹配 func()、func(1)、func(1, 2, 3) |
$VAR(重复) |
相同的 $VAR 两次都必须匹配相同结构 |
$x == $x 匹配 a == a,但不匹配 a == b |
按照惯例,元变量名称使用大写,但并非必须如此。$_ 是一个不捕获内容的通配符。
$$$VARIADIC可用于 PHP允许节点列表的任何位置:函数参数、数组项、参数列表、语句体。捕获内容会存储整个匹配序列(包括零个项)。
标识符元变量
类、函数、接口、特征和枚举名称都是标识符,而非表达式,因此通常情况下,PHP不会接受 $Name跟在 class 后面。Greph会在解析前重写这些模式,使元变量仍能正常工作:
./vendor/bin/greph -p 'class $NAME extends BaseController {}' src
./vendor/bin/greph -p 'function $NAME(): void {}' src
./vendor/bin/greph -p 'interface $NAME {}' src
对 class、interface、trait、enum 和 function也会进行相同的重写。
重复元变量
在同一模式中重复使用元变量,表示这两处都必须在结构上匹配同一个节点。这有助于发现冗余代码:
# Tautological comparisons
./vendor/bin/greph -p '$X == $X' src
# Redundant ternaries
./vendor/bin/greph -p '$X ? $X : null' src
相同规则也适用于 $$$VARIADIC:$$$ARGS 的两处都必须匹配相同的序列。
文件筛选
AST 模式使用与文本模式相同的遍历器遍历文件树,默认采用 php 文件类型筛选器。可通过 --type、--type-not、--glob、--no-ignore 和 --hidden 覆盖默认设置。遍历器仍会遵循 .gitignore 和 .grephignore,并跳过二进制文件。
./vendor/bin/greph -p 'new $CLASS()' --glob 'src/**/*.php' .
./vendor/bin/greph -p 'new $CLASS()' --type-not phpt src
输出
默认输出采用与文本模式相同的 file:line:content 格式,并将匹配的代码折叠到单行:
src/Greph.php:38:return (new FileWalker())->walk($paths, $options);
src/Greph.php:49:$searcher = new TextSearcher();
传入 --json 以输出结构化匹配结果:
[
{
"file": "src/Greph.php",
"start_line": 38,
"end_line": 38,
"start_file_pos": 1234,
"end_file_pos": 1252,
"code": "new FileWalker()"
}
]
解析错误
默认情况下,Greph会静默跳过解析失败的文件。对于包含实验性或生成的 PHP 的大型代码库,这种行为很合适。传入 --strict-parse(用于 greph-index ast-index search 和 greph-index ast-cache search)以改为抛出解析错误;直接使用门面时,也可在 AstSearchOptions 上设置 skipParseErrors: false。
编程方式使用
use Greph\Greph;
use Greph\Ast\AstSearchOptions;
$matches = Greph::searchAst(
'$obj->$method($$$ARGS)',
'src',
new AstSearchOptions(jobs: 4),
);
foreach ($matches as $match) {
echo "{$match->file}:{$match->startLine}\n";
echo $match->code . "\n";
foreach ($match->captures as $name => $node) {
echo " {$name} = " . get_debug_type($node) . "\n";
}
}
$match->node 是捕获到的 PhpParser\Node(因此你可以使用 PHP-Parser 的 NodeVisitor 遍历它),而 $match->captures 会将每个元变量名称映射到捕获的节点或节点列表。
工作原理
处理流程如下:
- 模式解析:
Greph\Ast\PatternParser会重写标识符元变量,然后使用 PHP-Parser解析模式。 - 源代码解析:每个候选文件只解析一次。在
ast-cache模式下,此步骤由从缓存存储反序列化来代替。 - 候选项筛选:
Greph\Ast\AstPatternPrefilter会从模式中提取粗略特征(调用名称、类名、语句类型),据此跳过明显不可能包含匹配项的文件。在ast-index模式下,此步骤改为查询预计算的事实存储。 - 匹配:
Greph\Ast\PatternMatcher会遍历源 AST 并与模式 AST 比较,在此过程中绑定元变量并确保重复变量保持一致。 - 输出结果:匹配节点会转换为带有行号/字节位置和原始源代码片段的
AstMatch对象。