
# AST-Umschreibung

Eine AST-Umschreibung ergänzt die AST-Suche um eine Ersetzungsvorlage. Greph führt denselben Abgleicher wie die [AST-Suche](https://greph.inline0.dev/de/docs/modes/ast) 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](#inside-a-match)).

## Schnellbeispiele

```bash
# 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](https://github.com/inline0/greph/issues/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](https://github.com/inline0/greph/issues/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](https://github.com/inline0/greph/issues/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).
