Greph
Modes

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' src

Pattern syntax

A pattern is valid PHP source. Anywhere a real expression or statement could go, you can substitute a metavariable to capture that node.

SyntaxMatchesExample
$VARAny single AST node, captured as VAR$x + $y matches foo() + bar
$_Any single AST node, not captured$_->method() matches any receiver
$$$ARGSZero or more nodes (variadic), captured as ARGSfunc($$$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 {}' src

The 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' src

The 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 src

Output

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:

  1. Pattern parsing: Greph\Ast\PatternParser rewrites identifier metavariables, then parses the pattern with PHP-Parser.
  2. Source parsing: each candidate file is parsed once. In ast-cache mode this step is replaced by a deserialization from the cache store.
  3. Candidate filtering: Greph\Ast\AstPatternPrefilter extracts a coarse signature from the pattern (call name, class name, statement type) and uses it to skip files that obviously cannot contain a match. In ast-index mode this step queries a precomputed fact store instead.
  4. Matching: Greph\Ast\PatternMatcher walks the source AST and compares it against the pattern AST, binding metavariables along the way and enforcing repeated-variable consistency.
  5. Result emission: matched nodes are converted into AstMatch objects 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.

On this page