Saltar al contenido

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

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

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: 0x1F se convierte en 0x1f, 1_000 se convierte en 1000, las secuencias de escape como "\x41" se escriben como el carácter ("A") y los heredoc flexibles pierden la sangría.
PHP
// 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:

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]];

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

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() 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 absoluta
  • originalContents: contenido del archivo antes de la reescritura
  • rewrittenContents: contenido del archivo después de la reescritura
  • replacementCount: número de reemplazos estructurales
  • changed(): helper que devuelve true cuando originalContents !== 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).