Crear una regla personalizada de regresión de versión para Rector
De un vistazo
Sección titulada «De un vistazo»La pipeline de backport de NextPDF toma el código fuente en PHP 8.4 de nextpdf/nextpdf y reduce su versión para que se ejecute en PHP 8.1 (y, para el core, en PHP 7.4). Usa los conjuntos integrados de regresión de versión de Rector para la mayoría de las características del lenguaje, junto con un pequeño conjunto de reglas personalizadas para características que Rector no cubre de forma nativa.
Esta guía muestra cómo agregar una nueva regla personalizada. Conviene seguir la estructura de las tres reglas que ya están en el repositorio: DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector y DowngradeTraitConstantsRector. Las tres viven en rector/rules/, se registran en rector/config/ y están cubiertas por pruebas basadas en fixtures en tests/Rector/.
Usar esta guía cuando una característica de NextPDF emplee una sintaxis de PHP que el target de compilación no admite y Rector no tenga una regresión de versión integrada para ella. Antes de empezar, confirmar que Rector realmente carece de una regla. La cadena integrada withDowngradeSets() ya gestiona las clases readonly, las constantes de clase tipadas, el operador de tubería y muchas otras características.
Instalación
Sección titulada «Instalación»Las reglas personalizadas se crean y se ejecutan dentro del repositorio nextpdf-backport. Las herramientas de desarrollo se declaran en su composer.json.
- Clonar el repositorio de backport e instalar sus dependencias.
- Confirmar que Rector y PHPUnit se resuelven.
git clone https://github.com/nextpdf-labs/backport.gitcd backportcomposer installvendor/bin/rector --versionvendor/bin/phpunit --versionEl repositorio requiere PHP 8.4 para ejecutarse (las reglas manipulan árboles de sintaxis de la versión 8.4), aunque la salida de compilación tenga como target PHP 8.1 o 7.4. El composer.jsonrequire fija php en >=8.4 <9.0. Las reglas personalizadas se cargan automáticamente bajo el espacio de nombres NextPDF\Backport\, asignado a rector/rules/; las pruebas se cargan automáticamente bajo NextPDF\Backport\Tests\, asignado a tests/.
Anatomía de una regla
Sección titulada «Anatomía de una regla»Cada regla personalizada extiende Rector\Rector\AbstractRector e implementa tres métodos. El contrato es el mismo en las tres reglas existentes.
| Miembro | Tipo | Propósito |
|---|---|---|
getRuleDefinition() | RuleDefinition | Descripción legible para humanos, junto con pares before/after de CodeSample para el generador de documentación de reglas. |
getNodeTypes() | array<class-string<Node>> | Las clases de nodo del Abstract Syntax Tree (AST, árbol de sintaxis abstracta) que la regla quiere visitar. Rector llama a refactor() solo para estas. |
refactor(Node $node) | ?Node o `Stmt[] | null` |
La declaración del tipo de nodo determina todo el flujo. DowngradeAsymmetricVisibilityRector apunta a [Property::class, Param::class] porque la visibilidad asimétrica (public private(set)) puede aparecer tanto en una propiedad de clase como en un parámetro de constructor promovido. DowngradeTraitConstantsRector apunta a [Trait_::class] porque reescribe todo el cuerpo del trait en una sola pasada. DowngradeCloneWithRector apunta a [FileNode::class, Return_::class, Expression::class] porque clone-with (clone($obj, [...])) aparece tanto en posiciones de return como de asignación; usa la visita a FileNode para reiniciar un contador por archivo.
Una regla que devuelve null desde refactor() indica «sin cambios». Una regla que devuelve un nodo indica un reemplazo. Una regla que devuelve Stmt[] (una lista de sentencias) expande una sentencia en varias. Así es como DowngradeCloneWithRector convierte un único return clone($this, [...]); en una asignación de clon, una asignación de propiedad por cada sobrescritura y un return final.
Lo que ya hacen los conjuntos integrados
Sección titulada «Lo que ya hacen los conjuntos integrados»Las dos configuraciones de la pipeline llaman a ->withDowngradeSets(php81: true) y ->withDowngradeSets(php74: true). Esos conjuntos encadenan cada regla integrada de regresión de versión para el target. Las reglas personalizadas existen solo para los huecos: la visibilidad asimétrica de PHP 8.4, el clone-with de PHP 8.5 y las constantes de trait de PHP 8.2. Rector no reduce la versión de ninguna de estas por sí solo. Escribir una regla personalizada solo después de confirmar ese mismo hueco.
Paso a paso: escribir una regla
Sección titulada «Paso a paso: escribir una regla»Este procedimiento agrega una nueva regla. El ejemplo de referencia refleja la estructura de las reglas existentes; sustituirlo por la característica correspondiente.
- Crear la clase de la regla en
rector/rules/. NombrarlaDowngrade<Feature>Rectory colocarla en el espacio de nombresNextPDF\Backportpara que la recoja la asignación de autocarga existente. - Extender
Rector\Rector\AbstractRectory marcar la clase comofinal. - Implementar
getNodeTypes()para que devuelva el conjunto más reducido de clases de nodo AST que necesita la regla. Un conjunto más reducido significa que Rector visita menos nodos. - Implementar
refactor(). Asegurar que el nodo sea uno de los tipos declarados, proteger la sintaxis exacta a la que apunta la regla, transformarla y devolver el nuevo nodo (oStmt[], onullpara no hacer ningún cambio). - Implementar
getRuleDefinition()con un par before/after deCodeSamplepor cada caso distinto que la regla gestione. - Mantener el archivo en PHPStan Level 10: cada parámetro, retorno y propiedad está tipado, y los genéricos de PHPDoc describen las formas de los arrays.
La regla de visibilidad asimétrica es el ejemplo completo más compacto. Elimina los indicadores de visibilidad de escritura y garantiza que se mantenga una visibilidad de lectura base:
<?php
declare(strict_types=1);
namespace NextPDF\Backport;
use PhpParser\Modifiers;use PhpParser\Node;use PhpParser\Node\Param;use PhpParser\Node\Stmt\Property;use Rector\Rector\AbstractRector;use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
final class DowngradeAsymmetricVisibilityRector extends AbstractRector{ public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( 'Remove asymmetric visibility modifiers (public private(set) -> public)', [ new CodeSample( 'public private(set) float $x = 0.0;', 'public float $x = 0.0;', ), ], ); }
/** * @return array<class-string<Node>> */ public function getNodeTypes(): array { return [Property::class, Param::class]; }
public function refactor(Node $node): ?Node { \assert($node instanceof Property || $node instanceof Param);
if (($node->flags & Modifiers::VISIBILITY_SET_MASK) === 0) { return null; }
$node->flags &= ~Modifiers::VISIBILITY_SET_MASK;
if (($node->flags & Modifiers::VISIBILITY_MASK) === 0) { $node->flags |= Modifiers::PUBLIC; }
return $node; }}La protección en el primer if es la parte crítica. Cuando la propiedad no tiene indicador de visibilidad de escritura, la regla devuelve null y deja el nodo intacto. Una regla que transforma sin condiciones reescribiría código que no debe tocar.
Fixtures y pruebas
Sección titulada «Fixtures y pruebas»Cada regla tiene una prueba basada en fixtures que ejecuta la regla contra archivos .php.inc y comprueba que la salida coincida. El arnés proviene de Rector\Testing\PHPUnit\AbstractRectorTestCase de Rector.
Un caso de prueba es pequeño y uniforme en las tres reglas existentes:
<?php
declare(strict_types=1);
namespace NextPDF\Backport\Tests\Rector;
use Iterator;use PHPUnit\Framework\Attributes\DataProvider;use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class DowngradeTraitConstantsRectorTest extends AbstractRectorTestCase{ #[DataProvider('provideData')] public function test(string $filePath): void { $this->doTestFile($filePath); }
public static function provideData(): Iterator { return self::yieldFilesFromDirectory(__DIR__ . '/Fixtures/DowngradeTraitConstants'); }
public function provideConfigFilePath(): string { return __DIR__ . '/config/downgrade_trait_constants.php'; }}La prueba apunta a un archivo de configuración por regla en tests/Rector/config/, que registra solo la regla bajo prueba, de modo que cada fixture ejercita una sola regla de forma aislada:
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeTraitConstantsRector;use Rector\Config\RectorConfig;
return RectorConfig::configure() ->withRules([ DowngradeTraitConstantsRector::class, ]);Un fixture es un archivo .php.inc con la entrada, un separador ----- y la salida esperada. Cuando la regla no hace ningún cambio, se omiten el separador y el segundo bloque. Un fixture que transforma para la regla de constantes de trait tiene este aspecto:
<?php
trait HasLimit{ private const MAX_SIZE = 1024;
public function getLimit(): int { return self::MAX_SIZE; }}?>-----<?php
trait HasLimit{ private static $MAX_SIZE = 1024;
public function getLimit(): int { return self::$MAX_SIZE; }}?>Para crear las pruebas de una nueva regla:
- Crear
tests/Rector/Fixtures/Downgrade<Feature>/y agregar un.php.incpor cada caso. - Cubrir tanto los casos que transforman (con un separador
-----) como los casos que se omiten (sin separador); por ejemplo, una propiedad sin visibilidad de escritura debe pasar sin cambios. - Agregar un
tests/Rector/config/downgrade_<feature>.phpque registre solo la regla. - Agregar un
tests/Rector/Downgrade<Feature>RectorTest.phpque produzca el directorio de fixtures y apunte a la configuración. - Ejecutar la suite.
composer testEl repositorio también incluye RectorRulesBehaviorTest y RectorRulesMetadataTest, que comprueban el comportamiento entre reglas y que el getRuleDefinition() de cada regla esté bien formado. Ejecutar el composer test completo para que esos controles incluyan la nueva regla.
Conectar a la compilación
Sección titulada «Conectar a la compilación»Una regla no está activa en la compilación hasta que se registra en las configuraciones de la pipeline. Hay dos targets de compilación, cada uno con su propia configuración en rector/config/.
- Abrir
rector/config/rector-php81.phpy agregar la clase de regla a la lista->withRules([...]). - Si la característica también debe reducir su versión para la compilación del core en PHP 7.4, agregar la misma clase a
rector/config/rector-php74.php. - Agregar un comentario que nombre la versión de PHP que introdujo la característica, igual que las entradas existentes.
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeAsymmetricVisibilityRector;use NextPDF\Backport\DowngradeCloneWithRector;use NextPDF\Backport\DowngradeTraitConstantsRector;use Rector\Config\RectorConfig;use Rector\DowngradePhp81\Rector\Property\DowngradeReadonlyPropertyRector;
return RectorConfig::configure() ->withDowngradeSets(php81: true) ->withRules([ // PHP 8.4 — asymmetric visibility (not covered by built-in sets) DowngradeAsymmetricVisibilityRector::class,
// PHP 8.4 — clone-with syntax (not covered by built-in sets) DowngradeCloneWithRector::class,
// PHP 8.2 — trait constants (not covered by built-in sets) DowngradeTraitConstantsRector::class,
// PHP 8.1 — readonly property removal (for clone-with expansion safety) DowngradeReadonlyPropertyRector::class, ]);El orquestador de compilación (scripts/build.php) fusiona los repositorios fuente, ejecuta Rector con estas configuraciones, ajusta el composer.json generado y ejecuta una comprobación de sintaxis php -l sobre la salida. Verificar la regla con PHPStan y con la compilación completa antes de depender de ella.
composer analysecomposer build:dryCasos límite y trampas
Sección titulada «Casos límite y trampas»- El orden de registro no importa, pero el orden de las reglas sí importa conceptualmente. El mecanismo de múltiples pasadas de Rector vuelve a recorrer el árbol hasta que ninguna regla hace un cambio adicional, así que no es necesario ordenar las reglas a mano en la configuración. Aun así, documentar cualquier dependencia de orden en el docblock de la clase, como hace
DowngradeCloneWithRector: su expansión produce$clone->prop = $val, que fallaría en una propiedad readonly, así queDowngradeReadonlyPropertyRectordebe ejecutarse para el mismo target. - Pasar atributos vacíos al construir un nodo de reemplazo a partir de un tipo de nodo distinto.
DowngradeTraitConstantsRectorconstruye unaPropertya partir de unClassConsty pasa[]para los atributos en lugar de los atributos del nodo fuente. Arrastrar los atributos originales dejaría un punteroorigNodehacia el tipo de nodo equivocado y dispararía una aserción en el impresor que preserva el formato. - Reiniciar el estado por archivo en la visita a
FileNode.DowngradeCloneWithRectordeclaraFileNode::classengetNodeTypes()únicamente para reiniciar su contador de variables temporales al inicio de cada archivo, de modo que los nombres de variable generados no choquen entre archivos. - Proteger con precisión y luego devolver
null. Una transformación clone-with debe confirmar que el nombre de la llamada escloney que el segundo argumento es un literal de array antes de actuar; unclone $objsimple nunca llega a la regla como una llamada de función, y una llamada de dos argumentos cuyo segundo argumento no es un array se deja intacta. - Eliminar los modificadores que el target no puede expresar. Cuando la regla de constantes de trait convierte una constante en una propiedad estática, conserva la visibilidad y agrega
static, pero no debe llevar un modificadorfinal, porque las propiedades de PHP 8.1 no pueden ser final. - Mantener la regla en PHPStan Level 10. El repositorio ejecuta
composer analyseen el nivel 10 sobrerector/rulesyscripts. Tipar cada firma y anotar las formas de los arrays; una regla que no sobreviviría al analizador es un defecto, no un borrador.
Consulta también
Sección titulada «Consulta también»- Guía para desarrolladores de Backport Builder — arquitectura de la pipeline, modelo de ramas y artefactos de release que rodean a estas reglas.
- Referencia de la API de Backport — superficie publicada de las herramientas de compilación de backport.
- Configuración de Backport — targets de compilación y selección del conjunto de regresión de versión.
- Solución de problemas de Backport — diagnóstico de fallos en un árbol generado de regresión de versión.
- Documentación de Rector — la referencia upstream para
AbstractRector,RuleDefinitiony las clases de nodo AST que se usan engetNodeTypes().