Reescritura AST
La reescritura AST es una búsqueda AST con una plantilla de reemplazo. Greph ejecuta el mismo comparador que la búsqueda AST y luego inserta la plantilla reescrita en el código fuente en los rangos de bytes coincidentes. Se conservan los espacios en blanco, la sangría y los comentarios circundantes; el código coincidente se vuelve a imprimir (consulta Dentro de una coincidencia).
Ejemplos rápidos
# 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
Plantillas de reemplazo
La plantilla de reemplazo usa la misma sintaxis de metavariables que el patrón de búsqueda. Los nodos capturados se insertan por nombre:
| En el patrón | En la plantilla | Efecto |
|---|---|---|
$X |
$X |
Insertar el nodo capturado |
$$$ARGS |
$$$ARGS |
Insertar la secuencia de nodos capturada |
$NAME (identificador) |
$NAME |
Insertar el identificador capturado |
Las metavariables que aparecen en el patrón, pero no en la plantilla, se descartan del código reescrito. Actualmente, no se rechaza una metavariable que aparece en la plantilla, pero no en el patrón: se imprime literalmente, por lo que -r '$o->$m($$$A, $EXTRA)' escribe una variable $EXTRA en el código (#4). Comprueba primero las plantillas con --dry-run.
Modos
| Opción | Comportamiento |
|---|---|
| (ninguna) | Aplicar la reescritura a todos los archivos coincidentes |
--dry-run |
Imprimir el contenido del archivo reescrito en stdout; no escribirlo |
--interactive |
Preguntar Rewrite <file>? [y/N] antes de escribir cada archivo |
De forma predeterminada, se escriben todos los cambios. Usa primero --dry-run mientras ajustas un patrón; cuando te convenza el diff, quita la opción.
Conservación del formato
Greph no reformatea el archivo. El comparador registra el rango de bytes de cada coincidencia en el código fuente original, y el reescritor inserta la plantilla renderizada directamente en esos rangos. El código intacto se conserva exactamente como estaba, incluidos:
- La sangría y los espacios en blanco finales
- Los comentarios en línea e independientes
- La disposición de las líneas en blanco
- El estilo de las comillas de las cadenas y su concatenación
- El estilo de etiquetas de apertura y cierre de PHP
Esta es una concesión deliberada frente al reformateo completo: Greph no ordenará el código que no haya coincidido, pero tampoco introducirá diferencias ruidosas en líneas sin relación.
Dentro de una coincidencia
El rango coincidente se sustituye por la plantilla procesada con el impresor de código de PHP-Parser, que vuelve a imprimir los nodos capturados en lugar de copiarlos como texto fuente. Actualmente, esto significa (#2):
- Se descarta todo comentario que no esté asociado a un nodo siguiente dentro de la coincidencia; por ejemplo, un comentario al final, tras el último elemento de una matriz, o un comentario en una matriz vacía.
- Los comentarios asociados a nodos capturados se conservan, pero se colocan en su propia línea, lo que puede convertir una expresión de una sola línea en varias.
- Se eliminan los saltos de línea dentro de la coincidencia: un
array(...)multilínea sin esos comentarios se convierte en un[...]de una sola línea. - La escritura de los literales se normaliza:
0x1Fse convierte en0x1f,1_000se convierte en1000, las secuencias de escape como"\x41"se escriben como el carácter ("A") y los heredoc flexibles pierden la sangría.
// before
$d = array(
"a" => 1, // trailing comment
);
// after -p 'array($$$ITEMS)' -r '[$$$ITEMS]'
$d = ["a" => 1];
Revisa el resultado de --dry-run si hay coincidencias con comentarios o literales formateados manualmente.
Coincidencias multilínea y anidadas
Los patrones multilínea funcionan igual que los de una sola línea. El impresor de código genera el reemplazo con las metavariables sustituidas, así que la disposición del código reemplazado proviene del impresor y no de la coincidencia original.
Greph reescribe cada archivo en una sola pasada y nunca aplica dos coincidencias superpuestas. Recorre las coincidencias desde el final del archivo hacia el principio y omite cualquiera que se superponga a otra ya reescrita. En coincidencias anidadas que comienzan en distintos desplazamientos, se reescribe la coincidencia interna y la externa queda intacta:
// 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]];
Cuando dos coincidencias comienzan en el mismo desplazamiento (por ejemplo, una cadena de métodos que coincide con $obj->$method($$$ARGS)), se reescribe la coincidencia externa. Si un patrón puede anidarse, vuelve a ejecutar la reescritura hasta que --dry-run no imprima nada (#3). ast-grep reescribe la coincidencia externa de dos coincidencias anidadas, así que vendor/bin/sg y el sg original generan resultados distintos en este caso.
Uso programático
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() no escribe los archivos por sí mismo. CLI se encarga de aplicar los cambios (o descartarlos en casos --dry-run y rechazados --interactive). Así, la fachada es segura para que la usen agentes automatizados que quieran inspeccionar el diff antes de aplicarlo.
RewriteResult expone:
file: ruta absolutaoriginalContents: contenido del archivo antes de la reescriturarewrittenContents: contenido del archivo después de la reescriturareplacementCount: número de reemplazos estructuraleschanged(): helper que devuelvetruecuandooriginalContents !== rewrittenContents
Cuándo usar rewrite
Rewrite es la herramienta adecuada cuando:
- La transformación es local y estructural (no reorganiza todo el archivo).
- Quieres una refactorización determinista y repetible que puedas volver a ejecutar hasta que no cambie nada.
- Quieres previsualizar el cambio antes de aplicarlo.
- Quieres integrar la refactorización en un script o trabajo de CI sin invocar un binario externo.
Rewrite no es la herramienta adecuada cuando:
- La transformación depende de información de tipos que PHP-Parser no puede resolver a partir de un solo archivo.
- La transformación necesita añadir o eliminar imports entre archivos.
- Quieres un formateador en lugar de una refactorización (usa PHP-CS-Fixer).