Bỏ qua để đến nội dung

Viết một quy tắc downgrade Rector tùy chỉnh

Quy trình backport NextPDF hạ cấp mã nguồn PHP 8.4 của nextpdf/nextpdf để chạy được trên PHP 8.1 và, với phần lõi, PHP 7.4. Quy trình này dùng các bộ downgrade tích hợp sẵn của Rector cho hầu hết tính năng ngôn ngữ, kèm một nhóm nhỏ quy tắc tùy chỉnh cho những tính năng Rector chưa xử lý.

Hướng dẫn này trình bày cách thêm một quy tắc tùy chỉnh mới. Quy tắc đó đi theo cùng cấu trúc với ba quy tắc đã có trong kho lưu trữ: DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRectorDowngradeTraitConstantsRector. Cả ba đều nằm trong rector/rules/, được đăng ký trong rector/config/ và có các bài kiểm thử dựa trên fixture trong tests/Rector/.

Hãy dùng hướng dẫn này khi một tính năng NextPDF sử dụng cú pháp PHP mà mục tiêu build không hỗ trợ, đồng thời Rector không có bộ downgrade tích hợp cho cú pháp đó. Trước khi bắt đầu, hãy xác nhận Rector thực sự thiếu quy tắc tương ứng. Chuỗi withDowngradeSets() tích hợp sẵn đã xử lý lớp readonly, hằng số lớp có kiểu, toán tử pipe và nhiều tính năng khác.

Bạn viết và chạy các quy tắc tùy chỉnh bên trong kho lưu trữ nextpdf-backport. composer.json trong kho này khai báo các công cụ phát triển.

  1. Sao chép (clone) kho lưu trữ backport và cài đặt các phụ thuộc của nó.
  2. Xác nhận rằng Rector và PHPUnit được phân giải.
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

Kho lưu trữ yêu cầu PHP 8.4 để chạy vì các quy tắc thao tác trên cây cú pháp 8.4, dù kết quả build nhắm tới PHP 8.1 hoặc 7.4. composer.jsonrequire ghim php ở mức >=8.4 <9.0. Các quy tắc tùy chỉnh được autoload dưới namespace NextPDF\Backport\, ánh xạ tới rector/rules/; các bài kiểm thử được autoload dưới NextPDF\Backport\Tests\, ánh xạ tới tests/.

Mỗi quy tắc tùy chỉnh mở rộng Rector\Rector\AbstractRector và triển khai ba phương thức. Contract giống nhau cho cả ba quy tắc hiện có.

Thành phầnKiểuMục đích
getRuleDefinition()RuleDefinitionMô tả dễ đọc cho con người và các cặp before/after CodeSample cho trình tạo tài liệu quy tắc.
getNodeTypes()array<class-string<Node>>Các lớp nút Cây Cú pháp Trừu tượng (AST) mà quy tắc ghé thăm. Rector chỉ gọi refactor() cho các lớp này.
refactor(Node $node)?Node hoặc `Stmt[]null`

Khai báo kiểu nút sẽ định hướng quy tắc. DowngradeAsymmetricVisibilityRector nhắm tới [Property::class, Param::class] vì asymmetric visibility (public private(set)) có thể xuất hiện trên cả thuộc tính lớp lẫn tham số hàm khởi tạo được promote. DowngradeTraitConstantsRector nhắm tới [Trait_::class] vì nó viết lại toàn bộ thân trait trong một lượt. DowngradeCloneWithRector nhắm tới [FileNode::class, Return_::class, Expression::class] vì clone-with (clone($obj, [...])) có thể xuất hiện ở cả vị trí return và vị trí gán; nó dùng lượt ghé thăm FileNode để đặt lại bộ đếm theo từng tệp.

Khi một quy tắc trả về null từ refactor(), điều đó báo hiệu “không thay đổi”. Khi trả về một nút, quy tắc báo hiệu có một thay thế. Khi trả về Stmt[], tức danh sách câu lệnh, quy tắc mở rộng một câu lệnh thành nhiều câu lệnh. Đó là cách DowngradeCloneWithRector biến một return clone($this, [...]); đơn lẻ thành một phép gán clone, một phép gán thuộc tính cho từng giá trị ghi đè, và một return cuối cùng.

Hai cấu hình quy trình gọi ->withDowngradeSets(php81: true)->withDowngradeSets(php74: true). Các bộ này nối vào mọi quy tắc downgrade tích hợp sẵn cho mục tiêu. Quy tắc tùy chỉnh chỉ dành cho các khoảng trống: asymmetric visibility của PHP 8.4, clone-with của PHP 8.5 và trait constants của PHP 8.2. Rector không tự hạ cấp bất kỳ tính năng nào trong số này. Chỉ viết quy tắc tùy chỉnh sau khi bạn đã xác nhận đúng khoảng trống đó.

Quy trình này thêm một quy tắc mới. Ví dụ minh họa bám theo các quy tắc hiện có; hãy thay bằng tính năng của riêng bạn.

  1. Tạo lớp quy tắc trong rector/rules/. Đặt tên là Downgrade<Feature>Rector và đặt nó trong namespace NextPDF\Backport để ánh xạ autoload hiện có nhận ra nó.
  2. Mở rộng Rector\Rector\AbstractRector và đánh dấu lớp là final.
  3. Triển khai getNodeTypes() để trả về tập lớp nút AST hẹp nhất mà quy tắc cần. Tập càng hẹp thì Rector càng ghé thăm ít nút.
  4. Triển khai refactor(). Khẳng định nút thuộc một trong các kiểu đã khai báo, đặt bộ bảo vệ cho đúng cú pháp bạn nhắm tới, biến đổi nó, rồi trả về nút mới, Stmt[], hoặc null nếu không thay đổi.
  5. Triển khai getRuleDefinition() với một before/after CodeSample cho mỗi trường hợp riêng biệt mà quy tắc xử lý.
  6. Giữ tệp ở PHPStan Level 10: gán kiểu cho mọi tham số, giá trị trả về và thuộc tính, đồng thời dùng generics của PHPDoc để mô tả hình dạng mảng.

Quy tắc asymmetric-visibility là ví dụ hoàn chỉnh nhỏ nhất. Nó loại bỏ các cờ set-visibility và bảo đảm vẫn còn read visibility cơ sở:

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

Bộ bảo vệ ở câu if đầu tiên rất quan trọng. Khi thuộc tính không có cờ set-visibility, quy tắc trả về null và giữ nguyên nút. Một quy tắc biến đổi vô điều kiện sẽ viết lại cả những đoạn mã lẽ ra phải được giữ nguyên.

Mỗi quy tắc có một bài kiểm thử dựa trên fixture, chạy quy tắc trên các tệp .php.inc và khẳng định kết quả khớp. Khung kiểm thử đến từ Rector\Testing\PHPUnit\AbstractRectorTestCase của Rector.

Một trường hợp kiểm thử nhỏ gọn và nhất quán trên cả ba quy tắc hiện có:

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

Bài kiểm thử trỏ tới tệp cấu hình riêng cho từng quy tắc trong tests/Rector/config/, chỉ đăng ký quy tắc đang kiểm thử để mỗi fixture chạy một quy tắc độc lập:

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

Fixture là tệp .php.inc chứa đầu vào, dấu phân tách ----- và kết quả mong đợi. Khi quy tắc không thay đổi gì, hãy bỏ dấu phân tách và khối thứ hai. Một fixture biến đổi cho quy tắc trait-constants có dạng như sau:

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

Để soạn các bài kiểm thử cho một quy tắc mới:

  1. Tạo tests/Rector/Fixtures/Downgrade<Feature>/ và thêm một .php.inc cho mỗi trường hợp.
  2. Bao quát các trường hợp biến đổi bằng dấu phân tách ----- và các trường hợp bỏ qua không có dấu phân tách, chẳng hạn một thuộc tính không có set visibility phải được truyền qua nguyên vẹn.
  3. Thêm một tests/Rector/config/downgrade_<feature>.php chỉ đăng ký quy tắc của bạn.
  4. Thêm một tests/Rector/Downgrade<Feature>RectorTest.php sinh ra thư mục fixture và trỏ tới cấu hình.
  5. Chạy bộ kiểm thử.
Terminal window
composer test

Kho lưu trữ cũng có RectorRulesBehaviorTestRectorRulesMetadataTest, khẳng định hành vi liên quy tắc và xác nhận rằng getRuleDefinition() của mỗi quy tắc được tạo đúng quy cách. Chạy đầy đủ composer test để các cổng đó nhìn thấy quy tắc mới của bạn.

Một quy tắc chưa hoạt động trong build cho đến khi bạn đăng ký nó trong các cấu hình quy trình. Có hai mục tiêu build, mỗi mục tiêu có cấu hình riêng trong rector/config/.

  1. Mở rector/config/rector-php81.php và thêm lớp quy tắc của bạn vào danh sách ->withRules([...]).
  2. Nếu tính năng cũng phải được hạ cấp cho build lõi PHP 7.4, hãy thêm cùng lớp đó vào rector/config/rector-php74.php.
  3. Thêm một chú thích nêu rõ phiên bản PHP đã giới thiệu tính năng, khớp với các mục hiện có.
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,
]);

Trình điều phối build (scripts/build.php) hợp nhất các kho lưu trữ nguồn, chạy Rector với các cấu hình này, điều chỉnh composer.json được sinh ra, rồi chạy kiểm tra cú pháp php -l trên kết quả đầu ra. Xác minh quy tắc của bạn bằng PHPStan và bản build đầy đủ trước khi tin dùng nó.

Terminal window
composer analyse
composer build:dry
  • Thứ tự đăng ký không quan trọng, nhưng thứ tự quy tắc có ý nghĩa về mặt khái niệm. Cơ chế đa lượt của Rector duyệt lại cho đến khi không quy tắc nào tạo thêm thay đổi, nên bạn không tự xếp thứ tự các quy tắc trong cấu hình. Dù vậy, hãy ghi lại mọi phụ thuộc về thứ tự trong docblock của lớp, như DowngradeCloneWithRector đã làm: phép mở rộng của nó tạo ra $clone->prop = $val, vốn sẽ thất bại trên một thuộc tính readonly, nên DowngradeReadonlyPropertyRector phải chạy cho cùng mục tiêu.
  • Truyền thuộc tính rỗng khi dựng một nút thay thế từ một loại nút khác. DowngradeTraitConstantsRector dựng một Property từ một ClassConst và truyền [] cho thuộc tính thay vì dùng thuộc tính của nút nguồn. Nếu mang theo các thuộc tính gốc, bạn sẽ để lại con trỏ origNode trỏ tới loại nút sai và kích hoạt một assertion trong trình in bảo toàn định dạng.
  • Đặt lại trạng thái theo từng tệp trong lượt ghé thăm FileNode. DowngradeCloneWithRector khai báo FileNode::class trong getNodeTypes() chỉ để đặt lại bộ đếm biến tạm ở đầu mỗi tệp, để các tên biến được sinh ra không xung đột giữa các tệp.
  • Đặt bộ bảo vệ chính xác, rồi trả về null. Một phép biến đổi clone-with phải xác nhận tên lời gọi là clone và đối số thứ hai là một mảng theo kiểu literal trước khi hành động; một clone $obj thông thường không bao giờ đến được quy tắc dưới dạng lời gọi hàm, còn lời gọi hai đối số có đối số thứ hai không phải là mảng thì được để yên.
  • Loại bỏ các bổ ngữ mà mục tiêu không thể biểu diễn. Khi quy tắc trait-constants biến một hằng số thành thuộc tính static, nó giữ nguyên visibility và thêm static, nhưng không được mang theo bổ ngữ final vì thuộc tính của PHP 8.1 không thể là final.
  • Giữ quy tắc ở PHPStan Level 10. Kho lưu trữ chạy composer analyse ở level 10 trên rector/rulesscripts. Gán kiểu cho mọi chữ ký và chú thích hình dạng mảng; quy tắc không vượt qua trình phân tích là lỗi, không phải bản nháp.