AST検索
GrephのASTモードでは、PHPソースをテキストではなく構造で検索します。パターンは通常のPHPコードで記述し、$VARIABLEと$$$VARIADICのメタ変数を使います。パターンを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ノード1つ。VARとしてキャプチャ |
$x + $yはfoo() + barにマッチ |
$_ |
キャプチャしない任意のASTノード1つ | $_->method()は任意のレシーバーにマッチ |
$$$ARGS |
0個以上のノード(可変長)。ARGSとしてキャプチャ |
func($$$ARGS)はfunc()、func(1)、func(1, 2, 3)にマッチ |
$VAR(反復) |
同じ$VARは両方で同じ構造にマッチする必要がある |
$x == $xはa == aにはマッチし、a == bにはマッチしない |
メタ変数名は慣例上大文字ですが、必須ではありません。$_はキャプチャしないワイルドカードです。
$$$VARIADICは、PHPでノードのリストを指定できる場所ならどこでも使えます。関数の引数、配列の要素、パラメーターリスト、文の本体などが該当します。キャプチャには、0個の場合も含め、一致した並び全体が保存されます。
識別子のメタ変数
クラス、関数、インターフェース、トレイト、列挙型の名前は式ではなく識別子なので、通常、PHPはclassの後に$Nameを受け付けません。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が2回出現する場合、同じ並びにマッチする必要があります。
ファイルの絞り込み
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形式で、一致したコードを1行にまとめます。
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を含む大規模なコードベースを検索する場合に適した動作です。解析エラーを発生させるには、greph-index ast-index searchとgreph-index ast-cache searchで--strict-parseを渡すか、ファサードを直接使う場合は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でパターンを解析します。 - ソース解析: 各候補ファイルを1回解析します。
ast-cacheモードでは、この処理をキャッシュストアからのデシリアライズに置き換えます。 - 候補の絞り込み:
Greph\Ast\AstPatternPrefilterがパターンから大まかなシグネチャ(呼び出し名、クラス名、文の型)を抽出し、一致する可能性のないファイルを除外します。ast-indexモードでは、事前構築したファクトストアを照会します。 - 照合:
Greph\Ast\PatternMatcherがソースASTを走査してパターンASTと比較しながら、メタ変数を束縛し、反復変数の整合性を確認します。 - 結果の出力: 一致したノードを、行番号とバイト位置、元のソース範囲を含む
AstMatchオブジェクトに変換します。
繰り返し実行する場合は、インデックス付きAST検索とキャッシュ済みAST検索を使うと、手順2と3の大部分を省略できます。