Riscrittura AST
La riscrittura AST è una ricerca AST con un modello di sostituzione. Greph usa lo stesso motore di corrispondenza della ricerca AST, poi inserisce il modello riscritto nel sorgente agli intervalli di byte corrispondenti. Gli spazi circostanti, l’indentazione e i commenti vengono preservati; il codice corrispondente viene ristampato (vedere All’interno di una corrispondenza).
Esempi rapidi
# 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
Modelli di sostituzione
Il modello di sostituzione usa la stessa sintassi delle metavariabili del pattern di ricerca. I nodi acquisiti vengono reinseriti in base al nome:
| Nel pattern | Nel modello | Effetto |
|---|---|---|
$X |
$X |
Inserisce il nodo acquisito |
$$$ARGS |
$$$ARGS |
Inserisce la sequenza di nodi acquisita |
$NAME (identificatore) |
$NAME |
Inserisce l’identificatore acquisito |
Le metavariabili presenti nel pattern ma assenti nel modello vengono eliminate dal codice riscritto. Al momento, una metavariabile presente nel modello ma assente nel pattern non viene rifiutata: viene stampata letteralmente, quindi -r '$o->$m($$$A, $EXTRA)' inserisce nel codice una variabile $EXTRA (#4). Verificate prima i modelli con --dry-run.
Modalità
| Flag | Comportamento |
|---|---|
| (nessuno) | Applica la riscrittura a tutti i file corrispondenti |
--dry-run |
Stampa su stdout il contenuto dei file riscritti, senza scriverlo su disco |
--interactive |
Chiede Rewrite <file>? [y/N] prima di scrivere ogni file |
L’impostazione predefinita è «scrivi tutte le modifiche». Usate prima --dry-run mentre lavorate su un pattern, poi rimuovete il flag quando siete soddisfatti delle differenze.
Conservazione della formattazione
Greph non riformatta il file. Il motore di corrispondenza registra l’intervallo di byte di ogni corrispondenza nel sorgente originale e il riscrittore inserisce direttamente in quegli intervalli il modello generato. Il codice non modificato resta esattamente com’era, inclusi:
- Indentazione e spazi finali
- Commenti inline e autonomi
- Disposizione delle righe vuote
- Stile delle virgolette nelle stringhe e concatenazione
- Stile dei tag di apertura/chiusura PHP
È un compromesso intenzionale rispetto alla riformattazione completa: Greph non riordina il codice che non ha trovato, ma evita anche di introdurre differenze superflue in righe non pertinenti.
All’interno di una corrispondenza
L’intervallo corrispondente viene sostituito dal modello generato con il pretty printer di PHP-Parser; i nodi acquisiti vengono ristampati invece di essere copiati come testo sorgente. Al momento, questo significa (#2):
- Un commento non associato a un nodo successivo all’interno della corrispondenza viene eliminato, per esempio un commento finale dopo l’ultimo elemento di un array o un commento in un array altrimenti vuoto.
- I commenti associati ai nodi acquisiti vengono conservati, ma spostati su una riga propria; un’espressione su una sola riga può quindi occupare più righe.
- Le interruzioni di riga all’interno della corrispondenza non vengono conservate: un
array(...)su più righe senza questi commenti diventa un[...]su una sola riga. - La forma letterale viene normalizzata:
0x1Fdiventa0x1f,1_000diventa1000, le sequenze di escape come"\x41"vengono scritte come carattere ("A") e gli heredoc flessibili perdono l’indentazione.
// before
$d = array(
"a" => 1, // trailing comment
);
// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];
Controllate l’output di --dry-run per le corrispondenze che contengono commenti o valori letterali formattati a mano.
Corrispondenze su più righe e annidate
I pattern su più righe funzionano come quelli su una sola riga. La sostituzione viene generata dal pretty printer con le metavariabili inserite, quindi la disposizione del codice sostituito è determinata dal printer, non dalla corrispondenza originale.
Greph riscrive ogni file in un solo passaggio e non applica mai due corrispondenze sovrapposte. Esamina le corrispondenze a ritroso, dalla fine del file, e salta quelle che si sovrappongono a una già riscritta. Per le corrispondenze annidate che iniziano a offset diversi, viene riscritta quella interna e quella esterna resta com’era:
// 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]];
Quando due corrispondenze iniziano allo stesso offset (per esempio, una catena di metodi individuata da $obj->$method($$$ARGS)), viene riscritta la corrispondenza più esterna. Se un pattern può annidarsi, eseguite di nuovo la riscrittura finché --dry-run non produce più output (#3). ast-grep riscrive la corrispondenza più esterna tra due corrispondenze annidate, quindi vendor/bin/sg e sg upstream producono qui risultati diversi.
Uso programmatico
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() non scrive direttamente i file. CLI è responsabile del commit delle modifiche (oppure di ignorarle nei casi --dry-run e di rifiuto --interactive). In questo modo, la facade può essere chiamata in sicurezza da agenti automatizzati che vogliono esaminare il diff prima di applicarlo.
RewriteResult espone:
file: percorso assolutooriginalContents: contenuto del file prima della riscritturarewrittenContents: contenuto del file dopo la riscritturareplacementCount: numero di sostituzioni strutturalichanged(): helper che restituiscetruequandooriginalContents !== rewrittenContents
Quando usare rewrite
Rewrite è lo strumento giusto quando:
- La trasformazione è locale e strutturale (non ristruttura l'intero file).
- Volete un refactoring deterministico e ripetibile, da rieseguire finché non cambia più nulla.
- Volete visualizzare l'anteprima delle modifiche prima di applicarle.
- Volete integrare il refactoring in uno script o in un job CI senza richiamare un binario esterno.
Rewrite non è lo strumento giusto quando:
- La trasformazione dipende da informazioni sui tipi che PHP-Parser non può ricavare da un singolo file.
- La trasformazione deve aggiungere o rimuovere import oltre i confini del file.
- Volete un formatter anziché un refactoring (usate PHP-CS-Fixer).