Zum Inhalt springen

AST-Umschreibung

Eine AST-Umschreibung ergänzt die AST-Suche um eine Ersetzungsvorlage. Greph führt denselben Abgleicher wie die AST-Suche aus und fügt die umgeschriebene Vorlage anschließend an den Bytebereichen der Treffer in den Quellcode ein. Der umgebende Leerraum, die Einrückung und Kommentare bleiben erhalten; der gefundene Code selbst wird neu ausgegeben (siehe Innerhalb eines Treffers).

Schnellbeispiele

Shell
# 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

Ersetzungsvorlagen

Die Ersetzungsvorlage verwendet dieselbe Metavariablensyntax wie das Suchmuster. Erfasste Knoten werden anhand ihres Namens eingefügt:

Im Muster In der Vorlage Wirkung
$X $X Erfassten Knoten einfügen
$$$ARGS $$$ARGS Erfasste Knotensequenz einfügen
$NAME (Bezeichner) $NAME Erfassten Bezeichner einfügen

Metavariablen, die im Muster, aber nicht in der Vorlage vorkommen, werden aus dem umgeschriebenen Code entfernt. Eine Metavariable, die in der Vorlage, aber nicht im Muster vorkommt, wird derzeit nicht abgelehnt: Sie wird wörtlich ausgegeben, sodass -r '$o->$m($$$A, $EXTRA)' eine Variable $EXTRA in den Code schreibt (#4). Überprüfe Vorlagen zuerst mit --dry-run.

Modi

Flag Verhalten
(keines) Umschreibung auf alle passenden Dateien anwenden
--dry-run Umgeschriebenen Dateiinhalt an stdout ausgeben, nicht schreiben
--interactive Vor dem Schreiben jeder Datei Rewrite <file>? [y/N] fragen

Standardmäßig werden alle Änderungen geschrieben. Verwende beim Ausarbeiten eines Musters zuerst --dry-run und lass das Flag weg, sobald du mit dem Diff zufrieden bist.

Format beibehalten

Greph formatiert die Datei nicht neu. Der Abgleicher erfasst für jeden Treffer den Bytebereich im ursprünglichen Quellcode, und der Umschreiber fügt die gerenderte Vorlage direkt in diesen Bereich ein. Nicht bearbeiteter Code bleibt exakt wie zuvor, einschließlich:

  • Einrückung und abschließender Leerraum
  • Inline- und eigenständige Kommentare
  • Anordnung leerer Zeilen
  • Anführungszeichen und Zeichenfolgenverkettung
  • Stil der PHP-Öffnungs- und -Schließ-Tags

Das ist ein bewusster Kompromiss gegenüber einer vollständigen Neuformatierung: Greph räumt nicht den Code auf, auf den das Muster nicht zutrifft, verursacht dafür aber keine unnötigen Diffs in anderen Zeilen.

Innerhalb eines Treffers

Der gefundene Bereich wird durch die Vorlage ersetzt, die mit dem Pretty Printer von PHP-Parser gerendert wird. Dabei werden erfasste Knoten neu ausgegeben, statt als Quelltext kopiert zu werden. Das bedeutet derzeit Folgendes (#2):

  • Ein Kommentar, der innerhalb des Treffers nicht einem nachfolgenden Knoten zugeordnet ist, wird entfernt, zum Beispiel ein abschließender Kommentar nach dem letzten Array-Element oder ein Kommentar in einem ansonsten leeren Array.
  • Kommentare an erfassten Knoten bleiben erhalten, werden aber in eine eigene Zeile verschoben. Dadurch kann aus einem einzeiligen Ausdruck ein mehrzeiliger werden.
  • Zeilenumbrüche innerhalb des Treffers bleiben nicht erhalten: Ein mehrzeiliges array(...) ohne solche Kommentare wird zu einem einzeiligen [...].
  • Literale werden vereinheitlicht: 0x1F wird zu 0x1f, 1_000 wird zu 1000, Escape-Sequenzen wie "\x41" werden als Zeichen ausgegeben ("A"), und flexible Heredocs verlieren ihre Einrückung.
PHP
// before
$d = array(
    "a" => 1,  // trailing comment
);

// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];

Überprüfe die Ausgabe von --dry-run bei Treffern mit Kommentaren oder manuell formatierten Literalen.

Mehrzeilige und verschachtelte Treffer

Mehrzeilige Muster funktionieren genauso wie einzeilige. Die Vorlage wird mit dem Pretty Printer gerendert und die Metavariablen werden eingesetzt. Das Layout des ersetzten Codes stammt daher vom Pretty Printer und nicht vom ursprünglichen Treffer.

Greph schreibt jede Datei in einem Durchlauf um und wendet niemals zwei überlappende Treffer an. Es durchläuft die Treffer vom Dateiende rückwärts und überspringt jeden Treffer, der sich mit einem bereits umgeschriebenen überschneidet. Bei verschachtelten Treffern mit unterschiedlichen Startpositionen wird der innere Treffer umgeschrieben, während der äußere unverändert bleibt:

PHP
// before
$b = array(1, array(2));

// after one run of -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$b = array(1, [2]);

// after a second run
$b = [1, [2]];

Wenn zwei Treffer an derselben Position beginnen (zum Beispiel eine Methodenkette, auf die $obj->$method($$$ARGS) zutrifft), wird der äußere Treffer umgeschrieben. Wenn ein Muster verschachtelt auftreten kann, führe die Umschreibung erneut aus, bis --dry-run nichts mehr ausgibt (#3). ast-grep schreibt von zwei verschachtelten Treffern den äußeren um. Daher liefern vendor/bin/sg und das vorgelagerte sg hier unterschiedliche Ergebnisse.

Programmatische Verwendung

PHP
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() schreibt selbst keine Dateien. CLI ist dafür zuständig, die Änderungen zu übernehmen (oder sie bei --dry-run- und abgelehnten --interactive-Fällen zu überspringen). Dadurch kann die Fassade sicher von automatisierten Agenten aufgerufen werden, die den Diff vor dem Anwenden prüfen möchten.

RewriteResult stellt Folgendes bereit:

  • file: absoluter Pfad
  • originalContents: Dateiinhalt vor der Neuschreibung
  • rewrittenContents: Dateiinhalt nach der Neuschreibung
  • replacementCount: Anzahl struktureller Ersetzungen
  • changed(): Hilfsfunktion, die true zurückgibt, wenn originalContents !== rewrittenContents

Wann du rewrite verwenden solltest

Rewrite ist das richtige Werkzeug, wenn:

  • Die Transformation lokal und strukturell ist (keine Umstrukturierung der gesamten Datei).
  • Du ein deterministisches, wiederholbares Refactoring möchtest, das du erneut ausführen kannst, bis sich nichts mehr ändert.
  • Du die Änderung vor der Anwendung in der Vorschau ansehen möchtest.
  • Du das Refactoring in ein Skript oder einen CI-Job integrieren möchtest, ohne eine externe Binärdatei aufzurufen.

Rewrite ist nicht das richtige Werkzeug, wenn:

  • Die Transformation von Typinformationen abhängt, die PHP-Parser nicht aus einer einzelnen Datei ermitteln kann.
  • Für die Transformation Imports über Dateigrenzen hinweg hinzugefügt oder entfernt werden müssen.
  • Du einen Formatierer statt eines Refactorings möchtest (verwende PHP-CS-Fixer).