Réécriture AST
La réécriture AST consiste à rechercher dans un AST en utilisant un modèle de remplacement. Greph applique le même moteur de comparaison que la recherche AST, puis insère le modèle réécrit dans le code source aux plages d'octets correspondantes. Les espaces, l'indentation et les commentaires environnants sont conservés ; le code correspondant est réimprimé (voir À l'intérieur d'une correspondance).
Exemples rapides
# 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
Modèles de remplacement
Le modèle de remplacement utilise la même syntaxe de métavariables que le motif de recherche. Les nœuds capturés sont insérés par nom :
| Dans le motif | Dans le modèle | Effet |
|---|---|---|
$X |
$X |
Insère le nœud capturé |
$$$ARGS |
$$$ARGS |
Insère la séquence de nœuds capturée |
$NAME (identifiant) |
$NAME |
Insère l'identifiant capturé |
Les métavariables présentes dans le motif, mais absentes du modèle, sont supprimées du code réécrit. Une métavariable présente dans le modèle, mais absente du motif, n'est pas rejetée pour l'instant : elle est imprimée littéralement. Ainsi, -r '$o->$m($$$A, $EXTRA)' écrit une variable $EXTRA dans le code (#4). Vérifiez d'abord les modèles avec --dry-run.
Modes
| Option | Comportement |
|---|---|
| (aucune) | Applique la réécriture à chaque fichier correspondant |
--dry-run |
Affiche le contenu du fichier réécrit sur stdout, sans l'écrire |
--interactive |
Demande confirmation à Rewrite <file>? [y/N] avant d'écrire chaque fichier |
Par défaut, « écrire tout ce qui change ». Utilisez d'abord --dry-run pour mettre au point un motif, puis retirez l'option lorsque le diff vous convient.
Préservation du format
Greph ne reformate pas le fichier. Le moteur de comparaison enregistre la plage d'octets de chaque correspondance dans le code source d'origine, et le réécrivain insère directement le modèle rendu dans ces plages. Le code non modifié reste exactement tel quel, notamment :
- L'indentation et les espaces de fin
- Les commentaires en ligne ou isolés
- La disposition des lignes vides
- Le style des guillemets et la concaténation des chaînes
- Le style des balises ouvrantes et fermantes PHP
Il s'agit d'un compromis délibéré par rapport à un reformatage complet : Greph ne « remettra pas en ordre » le code qui ne correspond pas au motif, mais évitera aussi de produire des différences bruyantes dans les lignes sans rapport.
À l'intérieur d'une correspondance
La plage correspondant au motif est remplacée par le modèle rendu avec le formateur de PHP-Parser ; les nœuds capturés sont réimprimés au lieu d'être copiés comme texte source. Actuellement, cela signifie (#2) :
- Un commentaire qui n'est pas rattaché à un nœud suivant dans la correspondance est supprimé, par exemple un commentaire de fin après le dernier élément d'un tableau ou un commentaire dans un tableau autrement vide.
- Les commentaires rattachés aux nœuds capturés sont conservés, mais déplacés sur leur propre ligne ; une expression écrite sur une seule ligne peut donc s'étaler sur plusieurs lignes.
- Les sauts de ligne dans la correspondance ne sont pas conservés : un
array(...)multilignes sans ces commentaires devient un[...]sur une seule ligne. - L'écriture littérale est normalisée :
0x1Fdevient0x1f,1_000devient1000, les séquences d'échappement comme"\x41"sont écrites sous forme de caractère ("A"), et les heredocs flexibles perdent leur indentation.
// before
$d = array(
"a" => 1, // trailing comment
);
// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];
Vérifiez le résultat --dry-run pour les correspondances qui contiennent des commentaires ou des littéraux mis en forme manuellement.
Correspondances multilignes et imbriquées
Les motifs multilignes fonctionnent comme ceux sur une seule ligne. Le remplacement est rendu par le formateur, après substitution des métavariables ; la disposition du code remplacé provient donc du formateur, et non de la correspondance d'origine.
Greph réécrit chaque fichier en un seul passage et n'applique jamais deux correspondances qui se chevauchent. Il parcourt les correspondances en remontant depuis la fin du fichier et ignore toute correspondance qui chevauche une correspondance déjà réécrite. Pour les correspondances imbriquées qui commencent à des positions différentes, la correspondance interne est réécrite et l'externe reste telle quelle :
// 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]];
Lorsque deux correspondances commencent à la même position (une chaîne de méthodes reconnue par $obj->$method($$$ARGS), par exemple), la correspondance externe est réécrite. Lorsqu'un motif peut être imbriqué, relancez la réécriture jusqu'à ce que --dry-run n'affiche plus rien (#3). ast-grep réécrit la correspondance externe de deux correspondances imbriquées ; vendor/bin/sg et sg en amont produisent donc des résultats différents ici.
Utilisation par programmation
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() n'écrit pas lui-même les fichiers. CLI se charge de valider les modifications (ou de les ignorer en cas de --dry-run et de refus --interactive). La façade peut ainsi être appelée en toute sécurité par des agents automatisés qui souhaitent examiner le diff avant de l'appliquer.
RewriteResult expose :
file: chemin absoluoriginalContents: contenu du fichier avant la réécriturerewrittenContents: contenu du fichier après la réécriturereplacementCount: nombre de remplacements structurelschanged(): fonction utilitaire qui renvoietruelorsqueoriginalContents !== rewrittenContents
Quand utiliser rewrite
Rewrite est l'outil adapté lorsque :
- La transformation est locale et structurelle (et ne restructure pas le fichier entier).
- Vous voulez un remaniement déterministe et reproductible, que vous pouvez relancer jusqu'à ce que plus rien ne change.
- Vous voulez prévisualiser la modification avant de l'appliquer.
- Vous voulez intégrer le remaniement à un script ou à une tâche CI sans invoquer de binaire externe.
Rewrite n'est pas l'outil adapté lorsque :
- La transformation dépend d'informations de type que PHP-Parser ne peut pas résoudre à partir d'un seul fichier.
- La transformation doit ajouter ou supprimer des imports au-delà des limites du fichier.
- Vous voulez un formateur plutôt qu'un outil de remaniement (utilisez PHP-CS-Fixer).