Greph
Modes

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 src

Replacement templates

The replacement template uses the same metavariable syntax as the search pattern. Captured nodes are spliced back in by name:

In the patternIn the templateEffect
$X$XInsert the captured node
$$$ARGS$$$ARGSInsert the captured node sequence
$NAME (identifier)$NAMEInsert 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

FlagBehavior
(none)Apply the rewrite to every matching file
--dry-runPrint the rewritten file contents to stdout, do not write
--interactivePrompt 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 path
  • originalContents: the file contents before the rewrite
  • rewrittenContents: the file contents after the rewrite
  • replacementCount: number of structural replacements
  • changed(): helper that returns true when originalContents !== 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).

On this page