Creare una regola di downgrade personalizzata per Rector
In breve
Sezione intitolata “In breve”La pipeline di backport di NextPDF esegue il downgrade del codice sorgente PHP 8.4 di nextpdf/nextpdf affinché possa essere eseguito su PHP 8.1 (e, per il core, su PHP 7.4). Utilizza i set di downgrade integrati di Rector per la maggior parte delle funzionalità del linguaggio, insieme a un piccolo insieme di regole personalizzate per le funzionalità che Rector non gestisce in modo nativo.
Questa guida spiega come aggiungere una nuova regola personalizzata. La procedura segue la struttura delle tre regole già presenti nel repository: DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector e DowngradeTraitConstantsRector. Tutte e tre risiedono in rector/rules/, sono registrate in rector/config/ e sono coperte da test basati su fixture in tests/Rector/.
Utilizzare questa guida quando una funzionalità di NextPDF usa una sintassi PHP non supportata dal target di build e Rector non offre un downgrade integrato per quella sintassi. Prima di iniziare, verificare che in Rector manchi davvero una regola. La catena integrata withDowngradeSets() gestisce già le classi readonly, le costanti di classe tipizzate, l’operatore pipe e molte altre funzionalità.
Installazione
Sezione intitolata “Installazione”Le regole personalizzate vengono create ed eseguite all’interno del repository nextpdf-backport. Gli strumenti di sviluppo sono dichiarati nel rispettivo composer.json.
- Clonare il repository di backport e installarne le dipendenze.
- Verificare che Rector e PHPUnit siano risolti correttamente.
git clone https://github.com/nextpdf-labs/backport.gitcd backportcomposer installvendor/bin/rector --versionvendor/bin/phpunit --versionIl repository richiede PHP 8.4 per l’esecuzione (le regole manipolano gli alberi sintattici di PHP 8.4), anche se l’output di build ha come target PHP 8.1 o 7.4. Nel composer.jsonrequire, php è vincolato a >=8.4 <9.0. Le regole personalizzate vengono caricate automaticamente nello spazio dei nomi NextPDF\Backport\, mappato su rector/rules/; i test vengono caricati automaticamente in NextPDF\Backport\Tests\, mappato su tests/.
Anatomia di una regola
Sezione intitolata “Anatomia di una regola”Ogni regola personalizzata estende Rector\Rector\AbstractRector e implementa tre metodi. Il contratto è identico in tutte e tre le regole esistenti.
| Membro | Tipo | Scopo |
|---|---|---|
getRuleDefinition() | RuleDefinition | Descrizione leggibile e coppie before/after di CodeSample per il generatore della documentazione delle regole. |
getNodeTypes() | array<class-string<Node>> | Le classi dei nodi dell’Abstract Syntax Tree (AST) che la regola intende visitare. Rector chiama refactor() solo per questi nodi. |
refactor(Node $node) | ?Node o `Stmt[] | null` |
La dichiarazione del tipo di nodo determina tutto il resto. DowngradeAsymmetricVisibilityRector ha come target [Property::class, Param::class] perché la visibilità asimmetrica (public private(set)) può comparire sia su una proprietà di classe sia su un parametro promosso del costruttore. DowngradeTraitConstantsRector ha come target [Trait_::class] perché riscrive l’intero corpo del trait in un solo passaggio. DowngradeCloneWithRector ha come target [FileNode::class, Return_::class, Expression::class] perché clone-with (clone($obj, [...])) può comparire sia in posizione return sia in posizione di assegnazione; utilizza la visita di FileNode per reimpostare un contatore a livello di file.
Una regola che restituisce null da refactor() indica «nessuna modifica». Una regola che restituisce un nodo indica una sostituzione. Una regola che restituisce Stmt[] (un elenco di istruzioni) espande una singola istruzione in più istruzioni. È così che DowngradeCloneWithRector trasforma un singolo return clone($this, [...]); in un’assegnazione di clone, un’assegnazione di proprietà per ogni override e un return finale.
Cosa fanno già i set integrati
Sezione intitolata “Cosa fanno già i set integrati”Le due configurazioni della pipeline chiamano ->withDowngradeSets(php81: true) e ->withDowngradeSets(php74: true). Questi set concatenano tutte le regole di downgrade integrate per il target. Le regole personalizzate esistono solo per le lacune: la visibilità asimmetrica di PHP 8.4, clone-with di PHP 8.5 e le costanti dei trait di PHP 8.2. Rector non esegue il downgrade di nessuna di queste in modo autonomo. Scrivere una regola personalizzata solo dopo aver confermato la presenza di quella lacuna.
Passo dopo passo: scrivere una regola
Sezione intitolata “Passo dopo passo: scrivere una regola”Questa procedura descrive come aggiungere una nuova regola. L’esempio di riferimento rispecchia la struttura delle regole esistenti; sostituirlo con la propria funzionalità.
- Creare la classe della regola in
rector/rules/. Assegnarle il nomeDowngrade<Feature>Rectore collocarla nello spazio dei nomiNextPDF\Backportaffinché la mappatura di autoload esistente la rilevi. - Estendere
Rector\Rector\AbstractRectore contrassegnare la classe comefinal. - Implementare
getNodeTypes()in modo che restituisca l’insieme più ristretto di classi di nodi AST necessarie alla regola. Un insieme più ristretto fa sì che Rector visiti un numero inferiore di nodi. - Implementare
refactor(). Verificare con un’asserzione che il nodo sia uno dei tipi dichiarati, applicare un guard per la sintassi specifica che si intende gestire, trasformarlo e restituire il nuovo nodo (oppureStmt[]onullper nessuna modifica). - Implementare
getRuleDefinition()con una coppia before/after diCodeSampleper ciascun caso distinto gestito dalla regola. - Mantenere il file al livello 10 di PHPStan: ogni parametro, valore restituito e proprietà deve essere tipizzato, e i generics PHPDoc devono descrivere le forme degli array.
L’esempio completo più compatto è la regola per la visibilità asimmetrica. Rimuove i flag di set-visibility e garantisce che rimanga una visibilità di lettura di 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; }}Il guard sul primo if è il punto critico. Quando la proprietà non ha alcun flag di set-visibility, la regola restituisce null e lascia il nodo inalterato. Una regola che trasformasse in modo incondizionato riscriverebbe codice che non dovrebbe toccare.
Fixture e test
Sezione intitolata “Fixture e test”Ogni regola ha un test basato su fixture che applica la regola ai file .php.inc e verifica che l’output corrisponda. L’harness proviene da Rector\Testing\PHPUnit\AbstractRectorTestCase di Rector.
Il caso di test è essenziale e uniforme tra le tre regole esistenti:
<?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'; }}Il test punta a un file di configurazione specifico della regola in tests/Rector/config/, che registra solo la regola sottoposta a test. Così la fixture esercita una singola regola in isolamento:
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeTraitConstantsRector;use Rector\Config\RectorConfig;
return RectorConfig::configure() ->withRules([ DowngradeTraitConstantsRector::class, ]);Una fixture è un file .php.inc con l’input, un separatore ----- e l’output atteso. Quando la regola non apporta modifiche, omettere il separatore e il secondo blocco. Una fixture di trasformazione per la regola delle costanti dei trait ha questa forma:
<?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; }}?>Per creare i test di una nuova regola:
- Creare
tests/Rector/Fixtures/Downgrade<Feature>/e aggiungere un.php.incper ogni caso. - Coprire sia i casi di trasformazione (con un separatore
-----) sia i casi di skip (senza separatore) — per esempio, una proprietà priva di set-visibility deve passare invariata. - Aggiungere un
tests/Rector/config/downgrade_<feature>.phpche registra solo la propria regola. - Aggiungere un
tests/Rector/Downgrade<Feature>RectorTest.phpche restituisce la directory delle fixture e punta alla configurazione. - Eseguire la suite.
composer testIl repository include anche RectorRulesBehaviorTest e RectorRulesMetadataTest, che verificano il comportamento incrociato tra le regole e la correttezza formale del getRuleDefinition() di ciascuna regola. Eseguire il composer test completo in modo che questi gate rilevino la nuova regola.
Integrazione nella build
Sezione intitolata “Integrazione nella build”Una regola non è attiva nella build finché non è registrata nelle configurazioni della pipeline. Esistono due target di build, ciascuno con la propria configurazione in rector/config/.
- Aprire
rector/config/rector-php81.phpe aggiungere la classe della nuova regola all’elenco->withRules([...]). - Se la funzionalità deve essere sottoposta a downgrade anche per la build core di PHP 7.4, aggiungere la stessa classe a
rector/config/rector-php74.php. - Aggiungere un commento che indichi la versione di PHP che ha introdotto la funzionalità, in linea con le voci esistenti.
<?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, ]);L’orchestratore di build (scripts/build.php) unisce i repository sorgente, esegue Rector con queste configurazioni, adatta il composer.json generato ed esegue un controllo di sintassi php -l sull’output. Verificare la nuova regola con PHPStan e con la build completa prima di farvi affidamento.
composer analysecomposer build:dryCasi limite e insidie
Sezione intitolata “Casi limite e insidie”- L’ordine di registrazione non ha importanza, ma concettualmente l’ordine delle regole sì. Il meccanismo multi-pass di Rector ripercorre l’albero finché nessuna regola apporta ulteriori modifiche, quindi non occorre ordinare manualmente le regole nella configurazione. Detto questo, documentare qualsiasi dipendenza di ordinamento nel docblock della classe, come fa
DowngradeCloneWithRector: la sua espansione produce$clone->prop = $val, che fallirebbe su una proprietà readonly, perciòDowngradeReadonlyPropertyRectordeve essere eseguito per lo stesso target. - Passare attributi vuoti quando si costruisce un nodo sostitutivo a partire da un tipo di nodo diverso.
DowngradeTraitConstantsRectorcostruisce unaPropertya partire da unaClassConste passa[]come attributi anziché quelli del nodo sorgente. Copiare gli attributi originali lascerebbe un puntatoreorigNodeal tipo di nodo errato e farebbe scattare un’asserzione nel printer che preserva la formattazione. - Reimpostare lo stato per file durante la visita di
FileNode.DowngradeCloneWithRectordichiaraFileNode::classingetNodeTypes()al solo scopo di reimpostare il proprio contatore di variabili temporanee all’inizio di ogni file, così che i nomi delle variabili generate non collidano tra file diversi. - Applicare un guard preciso, quindi restituire
null. Una trasformazione clone-with deve confermare che il nome della chiamata siaclonee che il secondo argomento sia un array letterale prima di agire; un sempliceclone $objnon arriva mai alla regola come chiamata di funzione, e una chiamata a due argomenti il cui secondo argomento non è un array viene lasciata inalterata. - Rimuovere i modificatori che il target non è in grado di esprimere. Quando la regola delle costanti dei trait trasforma una costante in una proprietà statica, preserva la visibilità e aggiunge
static, ma non deve conservare un modificatorefinal, perché le proprietà di PHP 8.1 non possono essere final. - Mantenere la regola al livello 10 di PHPStan. Il repository esegue
composer analyseal livello 10 surector/rulesescripts. Tipizzare ogni firma e annotare le forme degli array; una regola che non supererebbe l’analizzatore è un difetto, non una bozza accettabile.
Vedere anche
Sezione intitolata “Vedere anche”- Guida per gli sviluppatori di Backport Builder — l’architettura della pipeline, il modello di branch e gli artefatti di rilascio che contestualizzano queste regole.
- Riferimento API di Backport — la superficie pubblica del tooling di build del backport.
- Configurazione di Backport — i target di build e la selezione dei set di downgrade.
- Risoluzione dei problemi di Backport — diagnosi degli errori in un albero di downgrade generato.
- Documentazione di Rector — il riferimento upstream per
AbstractRector,RuleDefinitione le classi di nodi AST utilizzate ingetNodeTypes().