AST Rewrite
AST rewrite is AST search with a replacement template. Greph runs the same matcher as AST search, then splices the rewritten template back into the source at the matched byte ranges. Surrounding whitespace, indentation, and comments are preserved.
Quick examples
# Convert array() literals to []
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' --dry-run src
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' src
# isset ternary -> null coalesce
./vendor/bin/greph -p 'isset($X) ? $X : $Y' -r '$X ?? $Y' --dry-run src
# Rename a method call
./vendor/bin/greph -p '$obj->oldName($$$ARGS)' -r '$obj->newName($$$ARGS)' src
# Confirm each file before writing
./vendor/bin/greph -p 'array($$$ITEMS)' -r '[$$$ITEMS]' --interactive srcReplacement templates
The replacement template uses the same metavariable syntax as the search pattern. Captured nodes are spliced back in by name:
| In the pattern | In the template | Effect |
|---|---|---|
$X | $X | Insert the captured node |
$$$ARGS | $$$ARGS | Insert the captured node sequence |
$NAME (identifier) | $NAME | Insert the captured identifier |
Metavariables that appear in the template but not in the pattern are an error. Metavariables that appear in the pattern but not in the template are dropped from the rewritten code.
Modes
| Flag | Behavior |
|---|---|
| (none) | Apply the rewrite to every matching file |
--dry-run | Print the rewritten file contents to stdout, do not write |
--interactive | Prompt Rewrite <file>? [y/N] before writing each file |
The default is "write everything that changes". Use --dry-run first when iterating on a pattern, then drop the flag once you are happy with the diff.
Format preservation
Greph does not reformat the file. The matcher records the byte range of every match in the original source, and the rewriter splices the rendered template directly into those ranges. Untouched code stays exactly as it was, including:
- Indentation and trailing whitespace
- Inline and standalone comments
- Blank line layout
- String quote style and concatenation
- PHP open/close tag style
This is a deliberate trade-off against full reformatting: Greph will not "tidy up" code it did not match, but it also will not introduce noisy diffs in unrelated lines.
Multi-line and nested matches
Multi-line patterns work the same as single-line ones. The replacement is rendered with the metavariables substituted in, so the surrounding indentation is taken from the new template, not the original match. If you need the rewritten block to keep the original indentation, write the template at the same indentation level as the pattern.
Nested matches are handled left-to-right by byte position. Greph applies the rewrite to the outermost match, then re-runs against the resulting source. This means a chain of rewrites converges in a single pass.
Programmatic use
use Greph\Greph;
use Greph\Ast\AstSearchOptions;
$rewrites = Greph::rewriteAst(
'array($$$ITEMS)',
'[$$$ITEMS]',
'src',
new AstSearchOptions(jobs: 4),
);
foreach ($rewrites as $result) {
if (!$result->changed()) {
continue;
}
echo "{$result->file}: {$result->replacementCount} replacement(s)\n";
file_put_contents($result->file, $result->rewrittenContents);
}Greph::rewriteAst() does not write files itself. The CLI is responsible for committing the changes (or skipping them in --dry-run and declined --interactive cases). This makes the facade safe to call from automated agents that want to inspect the diff before applying it.
RewriteResult exposes:
file: absolute pathoriginalContents: the file contents before the rewriterewrittenContents: the file contents after the rewritereplacementCount: number of structural replacementschanged(): helper that returnstruewhenoriginalContents !== rewrittenContents
When to use rewrite
Rewrite is the right tool when:
- The transformation is local and structural (not whole-file restructuring).
- You want a deterministic, repeatable refactor that survives running it twice.
- You want to preview the change before applying it.
- You want to integrate the refactor into a script or CI job without invoking an external binary.
Rewrite is not the right tool when:
- The transformation depends on type information that PHP-Parser cannot resolve from a single file.
- The transformation needs to add or remove imports across the file boundary.
- You want a formatter rather than a refactor (use PHP-CS-Fixer).