Créer une règle Rector de rétrogradation personnalisée
Le pipeline de backport de NextPDF rétrograde le code source PHP 8.4 de nextpdf/nextpdf afin qu’il s’exécute sur PHP 8.1 (et, pour le cœur, sur PHP 7.4). Il s’appuie sur les jeux de rétrogradation intégrés de Rector pour la plupart des fonctionnalités du langage, ainsi que sur un petit ensemble de règles personnalisées pour celles que Rector ne couvre pas nativement.
Ce guide t’explique comment ajouter une nouvelle règle personnalisée. Il reprend la structure des trois règles déjà présentes dans le dépôt : DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector et DowngradeTraitConstantsRector. Toutes les trois vivent dans rector/rules/, sont enregistrées dans rector/config/ et disposent de tests basés sur des fixtures dans tests/Rector/.
Utilise ce guide lorsqu’une fonctionnalité de NextPDF emploie une syntaxe PHP que la cible de build ne prend pas en charge et pour laquelle Rector ne fournit aucune rétrogradation intégrée. Avant de commencer, confirme que Rector ne dispose réellement d’aucune règle. La chaîne intégrée withDowngradeSets() gère déjà les classes readonly, les constantes de classe typées, l’opérateur pipe et bien d’autres fonctionnalités.
Installation
Section intitulée « Installation »Tu écris et exécutes les règles personnalisées dans le dépôt nextpdf-backport. Ses outils de développement sont déclarés dans composer.json.
- Clone le dépôt de backport, puis installe ses dépendances.
- Vérifie que Rector et PHPUnit se résolvent correctement.
git clone https://github.com/nextpdf-labs/backport.gitcd backportcomposer installvendor/bin/rector --versionvendor/bin/phpunit --versionLe dépôt exige PHP 8.4 pour s’exécuter (les règles manipulent des arbres syntaxiques 8.4), même si la sortie de build cible PHP 8.1 ou PHP 7.4. Le composer.jsonrequire épingle php à >=8.4 <9.0. Les règles personnalisées sont chargées automatiquement sous le namespace NextPDF\Backport\ mappé sur rector/rules/ ; les tests le sont sous NextPDF\Backport\Tests\ mappé sur tests/.
Anatomie d’une règle
Section intitulée « Anatomie d’une règle »Chaque règle personnalisée étend Rector\Rector\AbstractRector et implémente trois méthodes. Le contrat est le même pour les trois règles existantes.
| Membre | Type | Rôle |
|---|---|---|
getRuleDefinition() | RuleDefinition | Description destinée aux humains, plus des paires before/after CodeSample pour le générateur de documentation des règles. |
getNodeTypes() | array<class-string<Node>> | Les classes de nœuds de l’arbre syntaxique abstrait (AST) que la règle doit visiter. Rector n’appelle refactor() que pour celles-ci. |
refactor(Node $node) | ?Node ou `Stmt[] | null` |
La déclaration du type de nœud conditionne tout. DowngradeAsymmetricVisibilityRector cible [Property::class, Param::class] parce que la visibilité asymétrique (public private(set)) peut apparaître aussi bien sur une propriété de classe que sur un paramètre de constructeur promu. DowngradeTraitConstantsRector cible [Trait_::class] parce qu’elle réécrit tout le corps du trait en une seule passe. DowngradeCloneWithRector cible [FileNode::class, Return_::class, Expression::class] parce que le clone-with (clone($obj, [...])) apparaît à la fois en position return et en position d’affectation ; elle utilise la visite de FileNode pour réinitialiser un compteur par fichier.
Une règle qui renvoie null depuis refactor() signale « aucun changement ». Une règle qui renvoie un nœud signale un remplacement. Une règle qui renvoie Stmt[] (une liste d’instructions) décompose une instruction en plusieurs. C’est ainsi que DowngradeCloneWithRector transforme un unique return clone($this, [...]); en une affectation de clone, une affectation de propriété par redéfinition, puis un return final.
Ce que font déjà les jeux intégrés
Section intitulée « Ce que font déjà les jeux intégrés »Les deux configurations du pipeline appellent ->withDowngradeSets(php81: true) et ->withDowngradeSets(php74: true). Ces jeux appliquent toutes les règles de rétrogradation intégrées pour la cible. Les règles personnalisées ne servent qu’à combler les manques : la visibilité asymétrique de PHP 8.4, le clone-with de PHP 8.5 et les constantes de trait de PHP 8.2. Rector ne rétrograde aucune de ces fonctionnalités de lui-même. N’écris une règle personnalisée qu’après avoir confirmé le même manque.
Pas à pas : écrire une règle
Section intitulée « Pas à pas : écrire une règle »Cette procédure ajoute une nouvelle règle. L’exemple suivi reprend la forme des règles existantes ; remplace-le par ta propre fonctionnalité.
- Crée la classe de règle dans
rector/rules/. Nomme-laDowngrade<Feature>Rectoret place-la dans le namespaceNextPDF\Backportpour que le mappage d’autoload existant la prenne en compte. - Étends
Rector\Rector\AbstractRectoret marque la classefinal. - Implémente
getNodeTypes()pour renvoyer l’ensemble le plus restreint de classes de nœuds AST dont la règle a besoin. Un ensemble plus restreint réduit le nombre de nœuds visités par Rector. - Implémente
refactor(). Vérifie que le nœud correspond à l’un des types déclarés, assure-toi que la syntaxe correspond précisément à celle que tu cibles, transforme-le et renvoie le nouveau nœud (ouStmt[], ounullsi aucun changement n’est nécessaire). - Implémente
getRuleDefinition()avec une paire before/afterCodeSamplepar cas distinct que la règle gère. - Maintiens le fichier au niveau 10 de PHPStan : tous les paramètres, valeurs de retour et propriétés sont typés, et les génériques PHPDoc décrivent la forme des tableaux.
La règle de visibilité asymétrique constitue le plus petit exemple complet. Elle supprime les drapeaux de visibilité d’écriture et garantit qu’une visibilité de lecture de base subsiste :
<?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 condition de garde du premier if est la partie critique. Quand la propriété ne possède aucun drapeau de visibilité d’écriture, la règle renvoie null et laisse le nœud intact. Une règle qui transformerait le nœud sans condition réécrirait du code qu’elle ne devrait pas toucher.
Fixtures et tests
Section intitulée « Fixtures et tests »Chaque règle possède un test basé sur des fixtures qui l’exécute sur des fichiers .php.inc et vérifie que la sortie correspond. L’infrastructure de test provient du Rector\Testing\PHPUnit\AbstractRectorTestCase de Rector.
Un cas de test est court et suit la même forme pour les trois règles existantes :
<?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'; }}Le test pointe vers un fichier de config par règle dans tests/Rector/config/ qui n’enregistre que la règle testée, afin qu’une fixture exerce une seule règle de manière isolée :
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeTraitConstantsRector;use Rector\Config\RectorConfig;
return RectorConfig::configure() ->withRules([ DowngradeTraitConstantsRector::class, ]);Une fixture est un fichier .php.inc avec l’entrée, un séparateur ----- et la sortie attendue. Quand la règle ne change rien, omets le séparateur et le second bloc. Une fixture de transformation pour la règle des constantes de trait ressemble à ceci :
<?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; }}?>Pour écrire les tests d’une nouvelle règle :
- Crée
tests/Rector/Fixtures/Downgrade<Feature>/et ajoute un.php.incpar cas. - Couvre à la fois les cas avec transformation (avec un séparateur
-----) et les cas ignorés (sans séparateur), par exemple une propriété sans visibilité d’écriture doit passer inchangée. - Ajoute un
tests/Rector/config/downgrade_<feature>.phpqui n’enregistre que ta règle. - Ajoute un
tests/Rector/Downgrade<Feature>RectorTest.phpqui produit le répertoire de fixtures et pointe vers la config. - Lance la suite de tests.
composer testLe dépôt contient aussi RectorRulesBehaviorTest et RectorRulesMetadataTest, qui vérifient le comportement entre règles et la validité du getRuleDefinition() de chaque règle. Lance composer test en entier pour que ces garde-fous couvrent ta nouvelle règle.
Câblage dans la build
Section intitulée « Câblage dans la build »Une règle n’est pas active dans la build tant qu’elle n’est pas enregistrée dans les configs du pipeline. Il existe deux cibles de build, chacune avec sa propre config dans rector/config/.
- Ouvre
rector/config/rector-php81.phpet ajoute ta classe de règle à la liste->withRules([...]). - Si la fonctionnalité doit aussi être rétrogradée pour la build du cœur en PHP 7.4, ajoute la même classe à
rector/config/rector-php74.php. - Ajoute un commentaire qui nomme la version de PHP ayant introduit la fonctionnalité, à l’image des entrées existantes.
<?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’orchestrateur de build (scripts/build.php) fusionne les dépôts source, exécute Rector avec ces configs, ajuste le composer.json généré, puis exécute une vérification de syntaxe php -l sur la sortie. Vérifie ta règle avec PHPStan et la build complète avant de t’y fier.
composer analysecomposer build:dryCas limites et pièges
Section intitulée « Cas limites et pièges »- L’ordre d’enregistrement n’a pas d’importance, mais l’ordre des règles en a, conceptuellement. Le mécanisme multi-passes de Rector parcourt de nouveau l’arbre jusqu’à ce qu’aucune règle n’apporte de changement supplémentaire ; tu n’as donc pas à ordonner les règles à la main dans la config. Malgré cela, documente toute dépendance d’ordre dans le docblock de la classe, comme le fait
DowngradeCloneWithRector: son expansion produit$clone->prop = $val, qui échouerait sur une propriété readonly, doncDowngradeReadonlyPropertyRectordoit s’exécuter pour la même cible. - Fournis des attributs vides quand tu construis un nœud de remplacement à partir d’un autre type de nœud.
DowngradeTraitConstantsRectorconstruit unePropertyà partir d’unClassConstet transmet[]pour les attributs au lieu des attributs du nœud source. Réutiliser les attributs d’origine laisserait un pointeurorigNodevers le mauvais type de nœud et déclencherait une assertion dans l’imprimeur qui préserve le format. - Réinitialise l’état par fichier lors de la visite du
FileNode.DowngradeCloneWithRectordéclareFileNode::classdansgetNodeTypes()uniquement pour réinitialiser son compteur de variables temporaires au début de chaque fichier, afin que les noms de variables générés n’entrent pas en collision d’un fichier à l’autre. - Filtre précisément, puis renvoie
null. Une transformation clone-with doit vérifier que le nom de l’appel estcloneet que le second argument est un littéral de tableau avant d’agir ; un simpleclone $objn’atteint jamais la règle en tant qu’appel de fonction, et un appel à deux arguments dont le second n’est pas un tableau reste tel quel. - Retire les modificateurs que la cible ne peut pas exprimer. Quand la règle des constantes de trait transforme une constante en propriété statique, elle conserve la visibilité et ajoute
static, mais elle ne doit pas conserver le modificateurfinal, car les propriétés de PHP 8.1 ne peuvent pas être final. - Maintiens la règle au niveau 10 de PHPStan. Le dépôt exécute
composer analyseau niveau 10 surrector/rulesetscripts. Type chaque signature et annote la forme des tableaux ; une règle qui ne passerait pas l’analyseur est un défaut, pas un brouillon.
Voir aussi
Section intitulée « Voir aussi »- Guide du développeur du Backport Builder : l’architecture du pipeline, le modèle de branches et les artefacts de release qui entourent ces règles.
- Référence de l’API Backport : la surface publiée de l’outillage de build du backport.
- Configuration du Backport : les cibles de build et la sélection du jeu de rétrogradation.
- Dépannage du Backport : diagnostiquer les échecs dans un arbre de rétrogradation généré.
- Documentation de Rector : la référence amont pour
AbstractRector,RuleDefinitionet les classes de nœuds AST utilisées dansgetNodeTypes().