Salta ai contenuti

Creare una regola di downgrade personalizzata per Rector

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à.

Le regole personalizzate vengono create ed eseguite all’interno del repository nextpdf-backport. Gli strumenti di sviluppo sono dichiarati nel rispettivo composer.json.

  1. Clonare il repository di backport e installarne le dipendenze.
  2. Verificare che Rector e PHPUnit siano risolti correttamente.
Terminal window
git clone https://github.com/nextpdf-labs/backport.git
cd backport
composer install
Terminal window
vendor/bin/rector --version
vendor/bin/phpunit --version

Il 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/.

Ogni regola personalizzata estende Rector\Rector\AbstractRector e implementa tre metodi. Il contratto è identico in tutte e tre le regole esistenti.

MembroTipoScopo
getRuleDefinition()RuleDefinitionDescrizione 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.

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.

Questa procedura descrive come aggiungere una nuova regola. L’esempio di riferimento rispecchia la struttura delle regole esistenti; sostituirlo con la propria funzionalità.

  1. Creare la classe della regola in rector/rules/. Assegnarle il nome Downgrade<Feature>Rector e collocarla nello spazio dei nomi NextPDF\Backport affinché la mappatura di autoload esistente la rilevi.
  2. Estendere Rector\Rector\AbstractRector e contrassegnare la classe come final.
  3. 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.
  4. 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 (oppure Stmt[] o null per nessuna modifica).
  5. Implementare getRuleDefinition() con una coppia before/after di CodeSample per ciascun caso distinto gestito dalla regola.
  6. 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:

rector/rules/DowngradeAsymmetricVisibilityRector.php
<?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.

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:

tests/Rector/DowngradeTraitConstantsRectorTest.php
<?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:

tests/Rector/config/downgrade_trait_constants.php
<?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:

tests/Rector/Fixtures/DowngradeTraitConstants/private_constant.php.inc
<?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:

  1. Creare tests/Rector/Fixtures/Downgrade<Feature>/ e aggiungere un .php.inc per ogni caso.
  2. 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.
  3. Aggiungere un tests/Rector/config/downgrade_<feature>.php che registra solo la propria regola.
  4. Aggiungere un tests/Rector/Downgrade<Feature>RectorTest.php che restituisce la directory delle fixture e punta alla configurazione.
  5. Eseguire la suite.
Terminal window
composer test

Il 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.

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/.

  1. Aprire rector/config/rector-php81.php e aggiungere la classe della nuova regola all’elenco ->withRules([...]).
  2. 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.
  3. Aggiungere un commento che indichi la versione di PHP che ha introdotto la funzionalità, in linea con le voci esistenti.
rector/config/rector-php81.php
<?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.

Terminal window
composer analyse
composer build:dry
  • 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ò DowngradeReadonlyPropertyRector deve essere eseguito per lo stesso target.
  • Passare attributi vuoti quando si costruisce un nodo sostitutivo a partire da un tipo di nodo diverso. DowngradeTraitConstantsRector costruisce una Property a partire da una ClassConst e passa [] come attributi anziché quelli del nodo sorgente. Copiare gli attributi originali lascerebbe un puntatore origNode al 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. DowngradeCloneWithRector dichiara FileNode::class in getNodeTypes() 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 sia clone e che il secondo argomento sia un array letterale prima di agire; un semplice clone $obj non 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 modificatore final, perché le proprietà di PHP 8.1 non possono essere final.
  • Mantenere la regola al livello 10 di PHPStan. Il repository esegue composer analyse al livello 10 su rector/rules e scripts. Tipizzare ogni firma e annotare le forme degli array; una regola che non supererebbe l’analizzatore è un difetto, non una bozza accettabile.