Tworzenie własnej reguły obniżania dla Rectora
W skrócie
Dział zatytułowany „W skrócie”Potok backportu NextPDF obniża kod źródłowy PHP 8.4 pakietu nextpdf/nextpdf, aby mógł działać na PHP 8.1, a w przypadku rdzenia – na PHP 7.4. Dla większości konstrukcji języka korzysta z wbudowanych zestawów obniżania Rectora, uzupełnionych o niewielki zbiór własnych reguł dla funkcji, których Rector nie obsługuje.
W tym przewodniku pokazano, jak dodać nową własną regułę. Nowa reguła ma taką samą strukturę jak trzy reguły już obecne w repozytorium: DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector i DowngradeTraitConstantsRector. Wszystkie trzy znajdują się w rector/rules/, są zarejestrowane w rector/config/ i mają testy oparte na fikstkach w tests/Rector/.
Skorzystaj z tego przewodnika, gdy funkcja NextPDF używa składni PHP nieobsługiwanej przez cel kompilacji, a Rector nie ma dla niej wbudowanego obniżania. Zanim zaczniesz, upewnij się, że Rector faktycznie nie udostępnia takiej reguły. Wbudowany łańcuch withDowngradeSets() obsługuje już klasy readonly, typowane stałe klasowe, operator potoku oraz wiele innych funkcji.
Instalacja
Dział zatytułowany „Instalacja”Własne reguły piszesz i uruchamiasz wewnątrz repozytorium nextpdf-backport. Narzędzia deweloperskie deklaruje jego plik composer.json.
- Sklonuj repozytorium backport i zainstaluj jego zależności.
- Sprawdź, czy Rector i PHPUnit zostały poprawnie rozwiązane jako zależności.
git clone https://github.com/nextpdf-labs/backport.gitcd backportcomposer installvendor/bin/rector --versionvendor/bin/phpunit --versionRepozytorium wymaga do działania PHP 8.4, ponieważ reguły operują na drzewach składni PHP 8.4, mimo że wynik kompilacji jest przeznaczony dla PHP 8.1 lub 7.4. Sekcja composer.jsonrequire przypina php do >=8.4 <9.0. Własne reguły są automatycznie ładowane w przestrzeni nazw NextPDF\Backport\, mapowanej na rector/rules/; testy są automatycznie ładowane w NextPDF\Backport\Tests\, mapowanej na tests/.
Anatomia reguły
Dział zatytułowany „Anatomia reguły”Każda własna reguła rozszerza Rector\Rector\AbstractRector i implementuje trzy metody. Kontrakt jest taki sam we wszystkich trzech istniejących regułach.
| Składowa | Typ | Przeznaczenie |
|---|---|---|
getRuleDefinition() | RuleDefinition | Czytelny dla człowieka opis oraz pary before/after CodeSample dla generatora dokumentacji reguł. |
getNodeTypes() | array<class-string<Node>> | Klasy węzłów abstrakcyjnego drzewa składni (AST), które reguła odwiedza. Rector wywołuje refactor() tylko dla tych klas. |
refactor(Node $node) | ?Node lub `Stmt[] | null` |
Deklaracja typów węzłów określa zakres działania reguły. DowngradeAsymmetricVisibilityRector działa na [Property::class, Param::class], ponieważ widoczność asymetryczna (public private(set)) może wystąpić zarówno na właściwości klasy, jak i na promowanym parametrze konstruktora. DowngradeTraitConstantsRector działa na [Trait_::class], ponieważ przepisuje całe ciało cechy w jednym przebiegu. DowngradeCloneWithRector działa na [FileNode::class, Return_::class, Expression::class], ponieważ clone-with (clone($obj, [...])) może wystąpić zarówno w pozycji return, jak i przypisania; odwiedzenie FileNode wykorzystuje do wyzerowania licznika przypisanego do pliku.
Gdy reguła zwraca null z refactor(), sygnalizuje „brak zmian”. Gdy zwraca węzeł, sygnalizuje zamianę. Gdy zwraca Stmt[], czyli listę instrukcji, rozwija jedną instrukcję na kilka. W ten sposób DowngradeCloneWithRector zamienia pojedyncze return clone($this, [...]); na przypisanie klonu, po jednym przypisaniu właściwości dla każdego nadpisania oraz końcowe return.
Co robią już wbudowane zestawy
Dział zatytułowany „Co robią już wbudowane zestawy”Dwie konfiguracje potoku wywołują ->withDowngradeSets(php81: true) oraz ->withDowngradeSets(php74: true). Te zestawy łączą w łańcuch wszystkie wbudowane reguły obniżania dla danego celu. Własne reguły istnieją wyłącznie dla luk: widoczności asymetrycznej z PHP 8.4, clone-with z PHP 8.5 oraz stałych cech z PHP 8.2. Rector nie obniża żadnej z nich samodzielnie. Własną regułę napisz dopiero po potwierdzeniu takiej samej luki.
Krok po kroku: napisz regułę
Dział zatytułowany „Krok po kroku: napisz regułę”Ta procedura dodaje nową regułę. Poniższy przykład odwzorowuje istniejące reguły; podstaw własną funkcję.
- Utwórz klasę reguły w
rector/rules/. Nazwij jąDowngrade<Feature>Rectori umieść w przestrzeni nazwNextPDF\Backport, aby istniejące mapowanie automatycznego ładowania ją wykryło. - Rozszerz
Rector\Rector\AbstractRectori oznacz klasę jakofinal. - Zaimplementuj
getNodeTypes(), aby zwracał najwęższy zbiór klas węzłów AST, jakiego reguła potrzebuje. Węższy zbiór sprawia, że Rector odwiedza mniej węzłów. - Zaimplementuj
refactor(). Asercją potwierdź, że węzeł jest jednym z zadeklarowanych typów, obsłuż wyłącznie dokładnie tę składnię, którą wspierasz, przekształć ją i zwróć nowy węzeł,Stmt[]lubnullw przypadku braku zmian. - Zaimplementuj
getRuleDefinition()z jedną parą before/afterCodeSampledla każdego odrębnego przypadku, który reguła obsługuje. - Utrzymuj plik na poziomie PHPStan Level 10: otypuj każdy parametr, zwracaną wartość i właściwość oraz używaj typów generycznych PHPDoc do opisania kształtów tablic.
Reguła asymmetric-visibility to najmniejszy kompletny przykład. Usuwa flagi widoczności zapisu i dba o to, aby pozostała podstawowa widoczność odczytu:
<?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; }}Zabezpieczenie w pierwszym if jest kluczowe. Gdy właściwość nie ma flagi widoczności zapisu, reguła zwraca null i pozostawia węzeł bez zmian. Reguła, która przekształcałaby bezwarunkowo, przepisałaby kod, który powinna pozostawić nietknięty.
Fikstki i testowanie
Dział zatytułowany „Fikstki i testowanie”Każda reguła ma test oparty na fikstkach, który uruchamia regułę na plikach .php.inc i asercją sprawdza, że wynik jest zgodny. Szkielet testowy pochodzi z Rector\Testing\PHPUnit\AbstractRectorTestCase z Rectora.
Przypadek testowy jest niewielki i spójny we wszystkich trzech istniejących regułach:
<?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'; }}Test wskazuje osobny dla każdej reguły plik konfiguracyjny w tests/Rector/config/, który rejestruje tylko testowaną regułę, dzięki czemu każda fikstka ćwiczy jedną regułę w izolacji:
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeTraitConstantsRector;use Rector\Config\RectorConfig;
return RectorConfig::configure() ->withRules([ DowngradeTraitConstantsRector::class, ]);Fikstka to plik .php.inc zawierający dane wejściowe, separator ----- oraz oczekiwany wynik. Gdy reguła nie wprowadza zmiany, pomiń separator i drugi blok. Fikstka przekształcająca dla reguły trait-constants wygląda następująco:
<?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; }}?>Aby napisać testy nowej reguły:
- Utwórz
tests/Rector/Fixtures/Downgrade<Feature>/i dodaj jeden plik.php.incna każdy przypadek. - Uwzględnij zarówno przypadki przekształcające z separatorem
-----, jak i przypadki pomijające bez separatora, na przykład właściwość bez widoczności zapisu, która musi przejść bez zmian. - Dodaj
tests/Rector/config/downgrade_<feature>.php, który rejestruje tylko Twoją regułę. - Dodaj
tests/Rector/Downgrade<Feature>RectorTest.php, który udostępnia katalog fikstek i wskazuje na konfigurację. - Uruchom zestaw testów.
composer testRepozytorium zawiera też RectorRulesBehaviorTest oraz RectorRulesMetadataTest, które asercjami sprawdzają zachowanie między regułami i potwierdzają, że getRuleDefinition() każdej reguły jest poprawnie zbudowane. Uruchom pełne composer test, aby te kontrole objęły Twoją nową regułę.
Podłączanie do kompilacji
Dział zatytułowany „Podłączanie do kompilacji”Reguła nie jest aktywna w kompilacji, dopóki nie zarejestrujesz jej w konfiguracjach potoku. Istnieją dwa cele kompilacji, każdy z własną konfiguracją w rector/config/.
- Otwórz
rector/config/rector-php81.phpi dodaj klasę nowej reguły do listy->withRules([...]). - Jeśli funkcja musi również zostać obniżona dla kompilacji rdzenia PHP 7.4, dodaj tę samą klasę do
rector/config/rector-php74.php. - Dodaj komentarz wskazujący wersję PHP, w której wprowadzono funkcję, zgodnie z istniejącymi wpisami.
<?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, ]);Orkiestrator kompilacji (scripts/build.php) scala repozytoria źródłowe, uruchamia Rectora z tymi konfiguracjami, dostosowuje wygenerowany composer.json i przeprowadza kontrolę składni php -l na wyniku. Zweryfikuj swoją regułę za pomocą PHPStan i pełnej kompilacji, zanim zaczniesz na niej polegać.
composer analysecomposer build:dryPrzypadki brzegowe i pułapki
Dział zatytułowany „Przypadki brzegowe i pułapki”- Kolejność rejestracji nie ma znaczenia, ale kolejność działania reguł jest istotna koncepcyjnie. Mechanizm wieloprzebiegowy Rectora ponownie przechodzi drzewo, dopóki żadna reguła nie wprowadzi kolejnej zmiany, więc nie szeregujesz reguł ręcznie w konfiguracji. Mimo to udokumentuj każdą zależność kolejności w bloku dokumentacyjnym klasy, tak jak robi to
DowngradeCloneWithRector: jego rozwinięcie wytwarza$clone->prop = $val, co zakończyłoby się niepowodzeniem na właściwości readonly, więcDowngradeReadonlyPropertyRectormusi działać dla tego samego celu. - Przekazuj puste atrybuty podczas budowania węzła zastępczego z innego rodzaju węzła.
DowngradeTraitConstantsRectorbudujePropertyzClassConsti przekazuje[]jako atrybuty zamiast atrybutów węzła źródłowego. Jeśli przeniesiesz oryginalne atrybuty, pozostawisz wskaźnikorigNodedo niewłaściwego rodzaju węzła i wywołasz asercję w drukarce zachowującej formatowanie. - Zeruj stan przypisany do pliku podczas odwiedzania
FileNode.DowngradeCloneWithRectordeklarujeFileNode::classwgetNodeTypes()wyłącznie po to, aby wyzerować licznik zmiennych tymczasowych na początku każdego pliku, dzięki czemu generowane nazwy zmiennych nie kolidują między plikami. - Zabezpieczaj precyzyjnie, a następnie zwracaj
null. Transformacja clone-with musi przed działaniem potwierdzić, że nazwa wywołania toclonei że drugi argument jest literałem tablicowym; zwykłeclone $objnigdy nie trafia do reguły jako wywołanie funkcji, a wywołanie dwuargumentowe, którego drugi argument nie jest tablicą, pozostaje nietknięte. - Usuwaj modyfikatory, których cel nie może wyrazić. Gdy reguła trait-constants zamienia stałą na właściwość statyczną, zachowuje widoczność i dodaje
static, lecz nie może przenieść modyfikatorafinal, ponieważ właściwości w PHP 8.1 nie mogą być final. - Utrzymuj regułę na poziomie PHPStan Level 10. Repozytorium uruchamia
composer analysena poziomie 10 dlarector/rulesiscripts. Otypuj każdą sygnaturę i opisz kształty tablic; reguła, która nie przetrwałaby analizatora, to defekt, a nie szkic.
Zobacz też
Dział zatytułowany „Zobacz też”- Przewodnik dewelopera Backport Builder — architektura potoku, model gałęzi i artefakty wydań związane z tymi regułami.
- Dokumentacja API Backport — publiczna powierzchnia narzędzi kompilacji backport.
- Konfiguracja Backport — cele kompilacji i wybór zestawu obniżania.
- Rozwiązywanie problemów Backport — jak diagnozować błędy w wygenerowanym drzewie obniżania.
- Dokumentacja Rector — źródłowe odniesienie dla
AbstractRector,RuleDefinitionoraz klas węzłów AST używanych wgetNodeTypes().