AST Search
Greph's AST mode searches PHP source by structure, not by text. Patterns are written as ordinary PHP code with $VARIABLE and $$$VARIADIC metavariables. The pattern is parsed by nikic/php-parser into the same AST shape as the source it is searching, then matched node-by-node.
This is the same model as ast-grep, and the sg compatibility wrapper exposes ast-grep's CLI surface on top of the same engine.
Quick examples
# 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' srcPattern syntax
A pattern is valid PHP source. Anywhere a real expression or statement could go, you can substitute a metavariable to capture that node.
| Syntax | Matches | Example |
|---|---|---|
$VAR | Any single AST node, captured as VAR | $x + $y matches foo() + bar |
$_ | Any single AST node, not captured | $_->method() matches any receiver |
$$$ARGS | Zero or more nodes (variadic), captured as ARGS | func($$$ARGS) matches func(), func(1), func(1, 2, 3) |
$VAR (repeated) | Same $VAR must match the same structure both times | $x == $x matches a == a, not a == b |
The metavariable name is uppercase by convention but does not have to be. $_ is a non-capturing wildcard.
$$$VARIADIC works wherever PHP allows a list of nodes: function arguments, array items, parameter lists, statement bodies. The capture stores the entire matched sequence (including zero items).
Identifier metavariables
Class, function, interface, trait, and enum names are identifiers, not expressions, so PHP would not normally accept $Name after class. Greph rewrites these patterns before parsing so the metavariable still works:
./vendor/bin/greph -p 'class $NAME extends BaseController {}' src
./vendor/bin/greph -p 'function $NAME(): void {}' src
./vendor/bin/greph -p 'interface $NAME {}' srcThe same rewrite happens for class, interface, trait, enum, and function.
Repeated metavariables
Re-using a metavariable inside the same pattern asserts that both occurrences must match the same node structurally. This is useful for spotting redundant code:
# Tautological comparisons
./vendor/bin/greph -p '$X == $X' src
# Redundant ternaries
./vendor/bin/greph -p '$X ? $X : null' srcThe same rule applies to $$$VARIADIC: two occurrences of $$$ARGS must match the same sequence.
Filtering files
AST mode walks the file tree with the same walker as text mode and defaults to the php file type filter. Override with --type, --type-not, --glob, --no-ignore, and --hidden. The walker still respects .gitignore and .grephignore and skips binary files.
./vendor/bin/greph -p 'new $CLASS()' --glob 'src/**/*.php' .
./vendor/bin/greph -p 'new $CLASS()' --type-not phpt srcOutput
The default output uses the same file:line:content format as text mode, with the matched code collapsed onto a single line:
src/Greph.php:38:return (new FileWalker())->walk($paths, $options);
src/Greph.php:49:$searcher = new TextSearcher();Pass --json to emit structured matches:
[
{
"file": "src/Greph.php",
"start_line": 38,
"end_line": 38,
"start_file_pos": 1234,
"end_file_pos": 1252,
"code": "new FileWalker()"
}
]Parse errors
By default Greph silently skips files that fail to parse. This is the right behavior when running across a large codebase that contains experimental or generated PHP. Pass --strict-parse (on greph-index ast-index search and greph-index ast-cache search) to raise the parse error instead, or set skipParseErrors: false on AstSearchOptions when using the facade directly.
Programmatic use
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 is the captured PhpParser\Node (so you can walk it with PHP-Parser's NodeVisitor), and $match->captures maps each metavariable name to its captured node or list of nodes.
How it works
The pipeline is:
- Pattern parsing:
Greph\Ast\PatternParserrewrites identifier metavariables, then parses the pattern with PHP-Parser. - Source parsing: each candidate file is parsed once. In
ast-cachemode this step is replaced by a deserialization from the cache store. - Candidate filtering:
Greph\Ast\AstPatternPrefilterextracts a coarse signature from the pattern (call name, class name, statement type) and uses it to skip files that obviously cannot contain a match. Inast-indexmode this step queries a precomputed fact store instead. - Matching:
Greph\Ast\PatternMatcherwalks the source AST and compares it against the pattern AST, binding metavariables along the way and enforcing repeated-variable consistency. - Result emission: matched nodes are converted into
AstMatchobjects with line/byte positions and the original source slice.
For repeated workloads, indexed AST search and cached AST search skip most of steps 2 and 3.