Como criar uma regra personalizada de downgrade do Rector
Visão geral
Seção intitulada “Visão geral”O pipeline de backport do NextPDF faz o downgrade do código-fonte em PHP 8.4 do nextpdf/nextpdf para que ele rode em PHP 8.1 e, no caso do core, em PHP 7.4. Ele usa os conjuntos de downgrade integrados do Rector para a maioria dos recursos da linguagem, além de um pequeno conjunto de regras personalizadas para os recursos que o Rector não cobre.
Este guia mostra como adicionar uma nova regra personalizada. Ela segue a mesma estrutura das três regras que já estão no repositório: DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector e DowngradeTraitConstantsRector. As três ficam em rector/rules/, são registradas em rector/config/ e têm testes baseados em fixtures em tests/Rector/.
Use este guia quando um recurso do NextPDF usar uma sintaxe de PHP que o alvo de build não suporta e o Rector não tiver um downgrade integrado para ela. Antes de começar, confirme que o Rector realmente não oferece uma regra. A sequência integrada withDowngradeSets() já trata classes readonly, constantes de classe tipadas, o operador pipe e muitos outros recursos.
Instalação
Seção intitulada “Instalação”Você escreve e executa regras personalizadas dentro do repositório nextpdf-backport. O composer.json dele declara as ferramentas de desenvolvimento.
- Clone o repositório de backport e instale as dependências.
- Confirme que o Rector e o PHPUnit estão disponíveis.
git clone https://github.com/nextpdf-labs/backport.gitcd backportcomposer installvendor/bin/rector --versionvendor/bin/phpunit --versionO repositório exige PHP 8.4 para rodar porque as regras manipulam árvores de sintaxe da 8.4, mesmo que a saída do build tenha como alvo PHP 8.1 ou 7.4. O composer.jsonrequire fixa o php em >=8.4 <9.0. As regras personalizadas são carregadas automaticamente no namespace NextPDF\Backport\, que mapeia para rector/rules/; os testes são carregados automaticamente em NextPDF\Backport\Tests\, que mapeia para tests/.
Anatomia de uma regra
Seção intitulada “Anatomia de uma regra”Toda regra personalizada estende Rector\Rector\AbstractRector e implementa três métodos. O contrato é o mesmo para as três regras existentes.
| Membro | Tipo | Finalidade |
|---|---|---|
getRuleDefinition() | RuleDefinition | Uma descrição legível por humanos e pares before/after de CodeSample para o gerador de documentação de regras. |
getNodeTypes() | array<class-string<Node>> | As classes de nó da Abstract Syntax Tree (AST) que a regra visita. O Rector chama refactor() apenas para essas classes. |
refactor(Node $node) | ?Node ou `Stmt[] | null` |
A declaração dos tipos de nó orienta a regra. DowngradeAsymmetricVisibilityRector tem como alvo [Property::class, Param::class] porque a visibilidade assimétrica (public private(set)) pode aparecer tanto em uma propriedade de classe quanto em um parâmetro de construtor promovido. DowngradeTraitConstantsRector tem como alvo [Trait_::class] porque reescreve o corpo inteiro do trait em uma única passagem. DowngradeCloneWithRector tem como alvo [FileNode::class, Return_::class, Expression::class] porque o clone-with (clone($obj, [...])) pode aparecer tanto em posições de return quanto de atribuição; ele usa a visita ao FileNode para reiniciar um contador por arquivo.
Uma regra que retorna null a partir de refactor() sinaliza “nenhuma alteração”. Uma regra que retorna um nó sinaliza uma substituição. Uma regra que retorna Stmt[], uma lista de instruções, expande uma instrução em várias. É assim que DowngradeCloneWithRector transforma um único return clone($this, [...]); em uma atribuição de clone, uma atribuição sobrescrita de propriedade e um return final.
O que os conjuntos integrados já fazem
Seção intitulada “O que os conjuntos integrados já fazem”Os dois arquivos de configuração do pipeline chamam ->withDowngradeSets(php81: true) e ->withDowngradeSets(php74: true). Esses conjuntos encadeiam todas as regras de downgrade integradas para o alvo. As regras personalizadas existem apenas para cobrir lacunas: visibilidade assimétrica do PHP 8.4, clone-with do PHP 8.5 e constantes de trait do PHP 8.2. O Rector não faz o downgrade de nenhuma delas por conta própria. Só escreva uma regra personalizada depois de confirmar que a lacuna é a mesma.
Passo a passo: escreva uma regra
Seção intitulada “Passo a passo: escreva uma regra”Este procedimento adiciona uma nova regra. O exemplo em uso espelha as regras existentes; substitua pelo seu próprio recurso.
- Crie a classe da regra em
rector/rules/. Nomeie-a comoDowngrade<Feature>Rectore coloque-a no namespaceNextPDF\Backportpara que o mapeamento de autoload existente a reconheça. - Estenda
Rector\Rector\AbstractRectore marque a classe comofinal. - Implemente
getNodeTypes()para retornar o conjunto mais restrito de classes de nó AST de que a regra precisa. Um conjunto mais restrito faz o Rector visitar menos nós. - Implemente
refactor(). Verifique se o nó é um dos tipos declarados, proteja contra tudo que não seja a sintaxe exata que você está mirando, transforme-o e retorne o novo nó,Stmt[]ounullpara nenhuma alteração. - Implemente
getRuleDefinition()com um before/afterCodeSamplepara cada caso distinto que a regra trata. - Mantenha o arquivo no PHPStan Level 10: tipifique todos os parâmetros, retornos e propriedades e use generics do PHPDoc para descrever os formatos dos arrays.
A regra de visibilidade assimétrica é o menor exemplo completo. Ela remove as flags de visibilidade de escrita (set) e garante que uma visibilidade de leitura básica permaneça definida:
<?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; }}A proteção no primeiro if é fundamental. Quando a propriedade não tem flag de visibilidade de escrita (set), a regra retorna null e deixa o nó inalterado. Uma regra que transforma incondicionalmente reescreveria código que ela deveria deixar intacto.
Fixtures e testes
Seção intitulada “Fixtures e testes”Cada regra tem um teste baseado em fixtures que executa a regra sobre arquivos .php.inc e verifica se a saída corresponde ao esperado. A infraestrutura de teste vem do Rector\Testing\PHPUnit\AbstractRectorTestCase do Rector.
Um caso de teste é pequeno e consistente entre as três regras 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'; }}O teste aponta para um arquivo de configuração por regra em tests/Rector/config/ que registra apenas a regra em teste, de modo que cada fixture exercite uma única regra de forma isolada:
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeTraitConstantsRector;use Rector\Config\RectorConfig;
return RectorConfig::configure() ->withRules([ DowngradeTraitConstantsRector::class, ]);Uma fixture é um arquivo .php.inc com a entrada, um separador ----- e a saída esperada. Quando a regra não faz nenhuma alteração, omita o separador e o segundo bloco. Uma fixture de transformação para a regra de constantes de trait fica assim:
<?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 criar os testes de uma nova regra:
- Crie
tests/Rector/Fixtures/Downgrade<Feature>/e adicione um.php.incpor caso. - Cubra tanto os casos de transformação com um separador
-----quanto os casos de skip sem separador, como uma propriedade sem visibilidade de escrita (set) que deve permanecer inalterada. - Adicione um
tests/Rector/config/downgrade_<feature>.phpque registra apenas a sua regra. - Adicione um
tests/Rector/Downgrade<Feature>RectorTest.phpque itera sobre o diretório de fixtures e aponta para a configuração. - Execute a suíte.
composer testO repositório também inclui RectorRulesBehaviorTest e RectorRulesMetadataTest, que verificam o comportamento entre regras e confirmam que o getRuleDefinition() de cada regra está bem formado. Execute o composer test completo para que essas verificações incluam a sua nova regra.
Integração ao build
Seção intitulada “Integração ao build”Uma regra não fica ativa no build enquanto você não a registrar nos arquivos de configuração do pipeline. Há dois alvos de build, cada um com a sua própria configuração em rector/config/.
- Abra
rector/config/rector-php81.phpe adicione a classe da sua regra à lista->withRules([...]). - Se o recurso também precisar de downgrade para o build do core em PHP 7.4, adicione a mesma classe a
rector/config/rector-php74.php. - Adicione um comentário nomeando a versão do PHP que introduziu o recurso, no mesmo padrão das 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, ]);O orquestrador de build (scripts/build.php) mescla os repositórios de origem, executa o Rector com essas configurações, ajusta o composer.json gerado e executa uma verificação de sintaxe php -l sobre a saída. Valide a sua regra com o PHPStan e o build completo antes de depender dela.
composer analysecomposer build:dryCasos extremos e armadilhas
Seção intitulada “Casos extremos e armadilhas”- A ordem de registro não importa, mas a ordem das regras importa conceitualmente. O mecanismo de múltiplas passagens do Rector percorre o código novamente até que nenhuma regra faça outra alteração, então você não ordena as regras manualmente na configuração. Ainda assim, documente qualquer dependência de ordenação no docblock da classe, como
DowngradeCloneWithRectorfaz: a expansão dele produz$clone->prop = $val, o que falharia em uma propriedade readonly, entãoDowngradeReadonlyPropertyRectordeve ser executado para o mesmo alvo. - Passe atributos vazios ao construir um nó de substituição a partir de um tipo de nó diferente.
DowngradeTraitConstantsRectorconstrói umaPropertya partir de umaClassConste passa[]como atributos em vez dos atributos do nó de origem. Se você mantiver os atributos originais, deixará um ponteiroorigNodeapontando para o tipo de nó errado e disparará uma asserção no printer que preserva a formatação. - Reinicie o estado por arquivo na visita ao
FileNode.DowngradeCloneWithRectordeclaraFileNode::classemgetNodeTypes()apenas para reiniciar o contador de variáveis temporárias no início de cada arquivo, de modo que os nomes de variáveis gerados não colidam entre arquivos. - Proteja com precisão e, em seguida, retorne
null. Uma transformação clone-with deve confirmar que o nome da chamada éclonee que o segundo argumento é um literal de array antes de agir; um simplesclone $objnunca chega à regra como uma chamada de função, e uma chamada de dois argumentos cujo segundo argumento não é um array permanece intacta. - Remova os modificadores que o alvo não consegue expressar. Quando a regra de constantes de trait transforma uma constante em uma propriedade estática, ela preserva a visibilidade e adiciona
static, mas não deve carregar um modificadorfinal, porque propriedades do PHP 8.1 não podem ser final. - Mantenha a regra no PHPStan Level 10. O repositório executa
composer analyseno level 10 sobrerector/rulesescripts. Tipifique todas as assinaturas e anote os formatos dos arrays; uma regra que não sobreviveria ao analisador é um defeito, não um rascunho.
Veja também
Seção intitulada “Veja também”- Guia do desenvolvedor do Backport Builder — a arquitetura do pipeline, o modelo de branches e os artefatos de release relacionados a essas regras.
- Referência da API do Backport — a superfície publicada para as ferramentas de build do backport.
- Configuração do Backport — alvos de build e seleção de conjuntos de downgrade.
- Solução de problemas do Backport — como diagnosticar falhas em uma árvore de downgrade gerada.
- Documentação do Rector — a referência upstream para
AbstractRector,RuleDefinitione as classes de nó AST usadas emgetNodeTypes().