Lewati ke konten

Menulis aturan kustom Rector untuk downgrade

Pipeline backport NextPDF melakukan downgrade pada kode sumber PHP 8.4 dari nextpdf/nextpdf agar dapat berjalan di PHP 8.1 dan, untuk inti, PHP 7.4. Pipeline ini menggunakan set downgrade bawaan Rector untuk sebagian besar fitur bahasa, ditambah sejumlah kecil aturan kustom untuk fitur yang belum dicakup Rector.

Panduan ini menunjukkan cara menambahkan aturan kustom baru. Aturan baru mengikuti struktur yang sama dengan tiga aturan yang sudah ada di repositori: DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector, dan DowngradeTraitConstantsRector. Ketiganya berada di rector/rules/, didaftarkan di rector/config/, dan diuji dengan fixture di tests/Rector/.

Gunakan panduan ini ketika fitur NextPDF memakai sintaks PHP yang tidak didukung target build dan Rector tidak menyediakan downgrade bawaan untuknya. Sebelum mulai, pastikan Rector memang belum memiliki aturan terkait. Rantai withDowngradeSets() bawaan sudah menangani kelas readonly, konstanta kelas bertipe, operator pipe, dan banyak fitur lainnya.

Anda menulis dan menjalankan aturan kustom di dalam repositori nextpdf-backport. Berkas composer.json repositori tersebut mendeklarasikan alat pengembangan.

  1. Klon repositori backport dan pasang dependensinya.
  2. Pastikan Rector dan PHPUnit dapat di-resolve.
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

Repositori ini memerlukan PHP 8.4 untuk berjalan karena aturannya memanipulasi pohon sintaks 8.4, meskipun keluaran build menargetkan PHP 8.1 atau 7.4. composer.jsonrequire mengunci php pada >=8.4 <9.0. Aturan kustom dimuat otomatis di bawah namespace NextPDF\Backport\, yang dipetakan ke rector/rules/; pengujian dimuat otomatis di bawah NextPDF\Backport\Tests\, yang dipetakan ke tests/.

Setiap aturan kustom memperluas Rector\Rector\AbstractRector dan mengimplementasikan tiga metode. Kontraknya sama untuk ketiga aturan yang sudah ada.

AnggotaTipeTujuan
getRuleDefinition()RuleDefinitionDeskripsi yang dapat dibaca manusia serta pasangan before/after CodeSample untuk generator dokumentasi aturan.
getNodeTypes()array<class-string<Node>>Kelas node Abstract Syntax Tree (AST) yang dikunjungi aturan. Rector memanggil refactor() hanya untuk kelas-kelas ini.
refactor(Node $node)?Node atau `Stmt[]null`

Deklarasi tipe node mengarahkan aturan. DowngradeAsymmetricVisibilityRector menargetkan [Property::class, Param::class] karena asymmetric visibility (public private(set)) dapat muncul pada properti kelas maupun parameter konstruktor yang dipromosikan. DowngradeTraitConstantsRector menargetkan [Trait_::class] karena aturan ini menulis ulang seluruh isi trait dalam satu lintasan. DowngradeCloneWithRector menargetkan [FileNode::class, Return_::class, Expression::class] karena clone-with (clone($obj, [...])) dapat muncul di posisi return maupun penugasan; aturan ini memakai kunjungan ke FileNode untuk mereset penghitung per berkas.

Aturan yang mengembalikan null dari refactor() menandakan bahwa tidak ada perubahan. Aturan yang mengembalikan sebuah node menandakan penggantian. Aturan yang mengembalikan Stmt[], yaitu daftar pernyataan, memperluas satu pernyataan menjadi beberapa pernyataan. Inilah cara DowngradeCloneWithRector mengubah satu return clone($this, [...]); menjadi penugasan clone, satu penugasan properti per override, dan satu return terakhir.

Kedua konfigurasi pipeline memanggil ->withDowngradeSets(php81: true) dan ->withDowngradeSets(php74: true). Set tersebut merangkai semua aturan downgrade bawaan untuk target masing-masing. Aturan kustom hanya mengisi celah: asymmetric visibility PHP 8.4, clone-with PHP 8.5, dan konstanta trait PHP 8.2. Rector tidak melakukan downgrade untuk fitur-fitur ini secara mandiri. Tulis aturan kustom hanya setelah Anda memastikan ada celah yang sama.

Prosedur ini menambahkan aturan baru. Contoh yang digunakan mencerminkan aturan yang sudah ada; ganti dengan fitur Anda sendiri.

  1. Buat kelas aturan di rector/rules/. Beri nama Downgrade<Feature>Rector dan tempatkan di namespace NextPDF\Backport agar pemetaan autoload yang sudah ada menemukannya.
  2. Perluas Rector\Rector\AbstractRector dan tandai kelas sebagai final.
  3. Implementasikan getNodeTypes() untuk mengembalikan kumpulan kelas node AST tersempit yang dibutuhkan aturan. Kumpulan yang lebih sempit membuat Rector mengunjungi lebih sedikit node.
  4. Implementasikan refactor(). Pastikan node tersebut termasuk salah satu tipe yang dideklarasikan, pasang guard untuk sintaks persis yang Anda targetkan, lakukan transformasi, lalu kembalikan node baru, Stmt[], atau null bila tidak ada perubahan.
  5. Implementasikan getRuleDefinition() dengan satu before/after CodeSample untuk setiap kasus berbeda yang ditangani aturan.
  6. Jaga berkas tetap sesuai PHPStan Level 10: beri tipe pada setiap parameter, nilai kembalian, dan properti, serta gunakan generic PHPDoc untuk mendeskripsikan bentuk larik.

Aturan asymmetric-visibility adalah contoh lengkap paling kecil. Aturan ini menghapus flag set-visibility dan memastikan visibilitas baca dasar tetap ada:

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

Guard pada if pertama sangat penting. Ketika properti tidak memiliki flag set-visibility, aturan mengembalikan null dan membiarkan node tidak berubah. Aturan yang melakukan transformasi tanpa syarat akan menulis ulang kode yang seharusnya dibiarkan apa adanya.

Setiap aturan memiliki pengujian berbasis fixture yang menjalankan aturan terhadap berkas .php.inc dan memastikan keluarannya cocok. Harness pengujiannya berasal dari Rector\Testing\PHPUnit\AbstractRectorTestCase milik Rector.

Test case berukuran kecil dan konsisten di antara ketiga aturan yang sudah ada:

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

Pengujian ini menunjuk ke berkas konfigurasi per aturan di tests/Rector/config/ yang hanya mendaftarkan aturan yang sedang diuji, sehingga setiap fixture menguji satu aturan secara terisolasi:

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 adalah berkas .php.inc yang berisi masukan, pemisah -----, dan keluaran yang diharapkan. Ketika aturan tidak membuat perubahan, hilangkan pemisah dan blok kedua. Fixture yang mengalami transformasi untuk aturan trait-constants terlihat seperti ini:

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

Untuk menulis pengujian aturan baru:

  1. Buat tests/Rector/Fixtures/Downgrade<Feature>/ dan tambahkan satu .php.inc per kasus.
  2. Cakup kasus yang mengalami transformasi dengan pemisah ----- dan kasus yang dilewati tanpa pemisah, seperti properti tanpa set visibility yang harus diteruskan tanpa perubahan.
  3. Tambahkan tests/Rector/config/downgrade_<feature>.php yang mendaftarkan hanya aturan Anda.
  4. Tambahkan tests/Rector/Downgrade<Feature>RectorTest.php yang menghasilkan direktori fixture dan menunjuk ke konfigurasi.
  5. Jalankan suite pengujiannya.
Terminal window
composer test

Repositori ini juga menyertakan RectorRulesBehaviorTest dan RectorRulesMetadataTest, yang menegaskan perilaku lintas aturan dan memastikan bahwa getRuleDefinition() setiap aturan terbentuk dengan baik. Jalankan composer test secara penuh agar semua gate tersebut melihat aturan baru Anda.

Aturan tidak aktif dalam build sampai Anda mendaftarkannya di konfigurasi pipeline. Ada dua target build, masing-masing dengan konfigurasinya sendiri di rector/config/.

  1. Buka rector/config/rector-php81.php dan tambahkan kelas aturan Anda ke daftar ->withRules([...]).
  2. Jika fitur tersebut juga harus diturunkan versinya untuk build inti PHP 7.4, tambahkan kelas yang sama ke rector/config/rector-php74.php.
  3. Tambahkan komentar yang menyebutkan versi PHP yang memperkenalkan fitur tersebut, sesuai entri yang sudah ada.
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,
]);

Orkestrator build (scripts/build.php) menggabungkan repositori sumber, menjalankan Rector dengan konfigurasi ini, menyesuaikan composer.json yang dihasilkan, dan menjalankan pemeriksaan sintaks php -l terhadap keluarannya. Verifikasi aturan Anda dengan PHPStan dan build penuh sebelum mengandalkannya.

Terminal window
composer analyse
composer build:dry
  • Urutan pendaftaran tidak penting, tetapi urutan aturan penting secara konseptual. Mekanisme multi-lintasan Rector menelusuri ulang sampai tidak ada lagi aturan yang membuat perubahan, sehingga Anda tidak perlu mengurutkan aturan secara manual di konfigurasi. Meski begitu, dokumentasikan setiap ketergantungan urutan di docblock kelas, seperti yang dilakukan DowngradeCloneWithRector: ekspansinya menghasilkan $clone->prop = $val, yang akan gagal pada properti readonly, sehingga DowngradeReadonlyPropertyRector harus dijalankan untuk target yang sama.
  • Berikan atribut kosong saat membangun node pengganti dari jenis node yang berbeda. DowngradeTraitConstantsRector membangun Property dari ClassConst dan memberikan [] sebagai atribut alih-alih atribut node sumber. Jika Anda membawa atribut asli, pointer origNode tetap mengarah ke jenis node yang salah dan memicu asersi pada printer yang menjaga format.
  • Reset status per berkas pada kunjungan FileNode. DowngradeCloneWithRector mendeklarasikan FileNode::class di getNodeTypes() hanya untuk mereset penghitung variabel sementaranya di awal setiap berkas, sehingga nama variabel yang dihasilkan tidak berbenturan di antara berkas.
  • Pasang guard dengan tepat, lalu kembalikan null. Transformasi clone-with harus memastikan bahwa nama panggilannya adalah clone dan bahwa argumen kedua adalah literal larik sebelum bertindak; clone $obj biasa tidak pernah mencapai aturan sebagai panggilan fungsi, dan panggilan dua-argumen yang argumen keduanya bukan larik dibiarkan apa adanya.
  • Buang modifier yang tidak dapat diekspresikan target. Ketika aturan trait-constants mengubah konstanta menjadi properti statis, aturan tersebut mempertahankan visibilitas dan menambahkan static, tetapi tidak boleh membawa modifier final karena properti PHP 8.1 tidak bisa bersifat final.
  • Jaga aturan tetap di PHPStan Level 10. Repositori menjalankan composer analyse pada level 10 atas rector/rules dan scripts. Beri tipe pada setiap signature dan anotasikan bentuk larik; aturan yang tidak lolos analisis adalah cacat, bukan draf.
  • Panduan pengembang Backport Builder — arsitektur pipeline, model branch, dan artefak rilis yang terkait dengan aturan-aturan ini.
  • Referensi API Backport — API surface yang dipublikasikan untuk perkakas build backport.
  • Konfigurasi Backport — target build dan pemilihan downgrade-set.
  • Pemecahan masalah Backport — cara mendiagnosis kegagalan dalam pohon downgrade yang dihasilkan.
  • Dokumentasi Rector — referensi hulu untuk AbstractRector, RuleDefinition, dan kelas node AST yang digunakan di getNodeTypes().