Przejdź do głównej zawartości

Tworzenie własnej reguły obniżania dla Rectora

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.

Własne reguły piszesz i uruchamiasz wewnątrz repozytorium nextpdf-backport. Narzędzia deweloperskie deklaruje jego plik composer.json.

  1. Sklonuj repozytorium backport i zainstaluj jego zależności.
  2. Sprawdź, czy Rector i PHPUnit zostały poprawnie rozwiązane jako zależności.
Okno terminala
git clone https://github.com/nextpdf-labs/backport.git
cd backport
composer install
Okno terminala
vendor/bin/rector --version
vendor/bin/phpunit --version

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

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ładowaTypPrzeznaczenie
getRuleDefinition()RuleDefinitionCzytelny 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.

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.

Ta procedura dodaje nową regułę. Poniższy przykład odwzorowuje istniejące reguły; podstaw własną funkcję.

  1. Utwórz klasę reguły w rector/rules/. Nazwij ją Downgrade<Feature>Rector i umieść w przestrzeni nazw NextPDF\Backport, aby istniejące mapowanie automatycznego ładowania ją wykryło.
  2. Rozszerz Rector\Rector\AbstractRector i oznacz klasę jako final.
  3. 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.
  4. 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[] lub null w przypadku braku zmian.
  5. Zaimplementuj getRuleDefinition() z jedną parą before/after CodeSample dla każdego odrębnego przypadku, który reguła obsługuje.
  6. 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:

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;
}
}

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.

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:

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';
}
}

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:

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,
]);

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:

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;
}
}
?>

Aby napisać testy nowej reguły:

  1. Utwórz tests/Rector/Fixtures/Downgrade<Feature>/ i dodaj jeden plik .php.inc na każdy przypadek.
  2. 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.
  3. Dodaj tests/Rector/config/downgrade_<feature>.php, który rejestruje tylko Twoją regułę.
  4. Dodaj tests/Rector/Downgrade<Feature>RectorTest.php, który udostępnia katalog fikstek i wskazuje na konfigurację.
  5. Uruchom zestaw testów.
Okno terminala
composer test

Repozytorium 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łę.

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

  1. Otwórz rector/config/rector-php81.php i dodaj klasę nowej reguły do listy ->withRules([...]).
  2. 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.
  3. Dodaj komentarz wskazujący wersję PHP, w której wprowadzono funkcję, zgodnie z istniejącymi wpisami.
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,
]);

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

Okno terminala
composer analyse
composer build:dry
  • 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ęc DowngradeReadonlyPropertyRector musi działać dla tego samego celu.
  • Przekazuj puste atrybuty podczas budowania węzła zastępczego z innego rodzaju węzła. DowngradeTraitConstantsRector buduje Property z ClassConst i przekazuje [] jako atrybuty zamiast atrybutów węzła źródłowego. Jeśli przeniesiesz oryginalne atrybuty, pozostawisz wskaźnik origNode do niewłaściwego rodzaju węzła i wywołasz asercję w drukarce zachowującej formatowanie.
  • Zeruj stan przypisany do pliku podczas odwiedzania FileNode. DowngradeCloneWithRector deklaruje FileNode::class w getNodeTypes() 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 to clone i że drugi argument jest literałem tablicowym; zwykłe clone $obj nigdy 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ść modyfikatora final, ponieważ właściwości w PHP 8.1 nie mogą być final.
  • Utrzymuj regułę na poziomie PHPStan Level 10. Repozytorium uruchamia composer analyse na poziomie 10 dla rector/rules i scripts. Otypuj każdą sygnaturę i opisz kształty tablic; reguła, która nie przetrwałaby analizatora, to defekt, a nie szkic.