跳转到正文

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 格式,并将匹配的代码折叠到单行:

Text
src/Greph.php:38:return (new FileWalker())->walk($paths, $options);
src/Greph.php:49:$searcher = new TextSearcher();

传入 --json 以输出结构化匹配结果:

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。

编程方式使用

PHP
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 会将每个元变量名称映射到捕获的节点或节点列表。

工作原理

处理流程如下:

  1. 模式解析:Greph\Ast\PatternParser会重写标识符元变量,然后使用 PHP-Parser解析模式。
  2. 源代码解析:每个候选文件只解析一次。在 ast-cache 模式下,此步骤由从缓存存储反序列化来代替。
  3. 候选项筛选:Greph\Ast\AstPatternPrefilter会从模式中提取粗略特征(调用名称、类名、语句类型),据此跳过明显不可能包含匹配项的文件。在 ast-index 模式下,此步骤改为查询预计算的事实存储。
  4. 匹配:Greph\Ast\PatternMatcher会遍历源 AST 并与模式 AST 比较,在此过程中绑定元变量并确保重复变量保持一致。
  5. 输出结果:匹配节点会转换为带有行号/字节位置和原始源代码片段的 AstMatch 对象。

对于重复工作负载,索引 AST 搜索和缓存 AST 搜索会跳过步骤 2 和 3 中的大部分工作。