
# AST 搜索

Greph的 AST 模式按结构而非文本搜索 PHP 源代码。模式使用带有 `$VARIABLE` 和 `$$$VARIADIC` 元变量的普通 PHP 代码编写。模式由 [nikic/php-parser](https://github.com/nikic/PHP-Parser)解析为与待搜索源代码相同的 AST 结构，然后逐节点匹配。

这与 [ast-grep](https://ast-grep.github.io/)采用相同的模型，而 `sg`兼容性封装则基于同一引擎提供 ast-grep的 CLI接口。

## 快速示例

```bash
# 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会在解析前重写这些模式，使元变量仍能正常工作：

```bash
./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`也会进行相同的重写。

## 重复元变量

在同一模式中重复使用元变量，表示这两处都必须在结构上匹配同一个节点。这有助于发现冗余代码：

```bash
# 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`，并跳过二进制文件。

```bash
./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 搜索](https://greph.inline0.dev/zh/docs/indexed/ast)和[缓存 AST 搜索](https://greph.inline0.dev/zh/docs/indexed/cache)会跳过步骤 2 和 3 中的大部分工作。
