コンテンツにスキップ

カスタム Rector ダウングレードルールを作成する

NextPDF バックポートパイプラインは、nextpdf/nextpdf の PHP 8.4 ソースをダウングレードし、PHP 8.1(およびコアでは PHP 7.4)で実行できるようにします。ほとんどの言語機能には Rector の組み込みダウングレードセットを使用し、Rector が標準ではカバーしていない機能には、少数のカスタムルールを追加で使用します。

このガイドでは、新しいカスタムルールを追加する方法を説明します。内容は、リポジトリにすでにある 3 つのルール、DowngradeAsymmetricVisibilityRectorDowngradeCloneWithRectorDowngradeTraitConstantsRector の構造に沿っています。3 つはいずれも rector/rules/ に置かれ、rector/config/ に登録され、tests/Rector/ のフィクスチャベースのテストでカバーされています。

NextPDF の機能が、ビルドターゲットでサポートされていない PHP 構文を使用していて、かつ Rector に対応する組み込みダウングレードがない場合に、このガイドを使用してください。作業を始める前に、Rector に本当に対象ルールが存在しないことを確認してください。組み込みの withDowngradeSets() チェーンは、readonly クラス、型付きクラス定数、パイプ演算子、そのほか多くの機能をすでに処理します。

カスタムルールは nextpdf-backport リポジトリ内で作成して実行します。開発ツールは、その composer.json で宣言されています。

  1. バックポートリポジトリをクローンし、その依存関係をインストールします。
  2. Rector と PHPUnit が依存関係として 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

ビルド出力のターゲットが PHP 8.1 または 7.4 であっても、このリポジトリを実行するには PHP 8.4 が必要です(ルールが 8.4 の構文木を操作するため)。composer.jsonrequirephp>=8.4 <9.0 に固定します。カスタムルールは NextPDF\Backport\ 名前空間でオートロードされ、rector/rules/ にマッピングされます。テストは NextPDF\Backport\Tests\ でオートロードされ、tests/ にマッピングされます。

すべてのカスタムルールは Rector\Rector\AbstractRector を継承し、3 つのメソッドを実装します。このコントラクトは、既存の 3 つのルールすべてで共通です。

メンバー目的
getRuleDefinition()RuleDefinition人が読める説明と、ルールドキュメント生成器向けの before/after CodeSample ペア。
getNodeTypes()array<class-string<Node>>ルールが訪問する抽象構文木(AST)のノードクラス。Rector はこれらに対してのみ refactor() を呼び出します。
refactor(Node $node)?Node または `Stmt[]null`

ノード型の宣言が処理範囲を決めます。DowngradeAsymmetricVisibilityRector[Property::class, Param::class] を対象にします。これは、非対称可視性(public private(set))がクラスプロパティと昇格コンストラクターパラメーターの両方に現れ得るためです。DowngradeTraitConstantsRector はトレイト本体全体を 1 回のパスで書き換えるため、[Trait_::class] を対象にします。DowngradeCloneWithRector[FileNode::class, Return_::class, Expression::class] を対象にします。これは、clone-with(clone($obj, [...]))が return と代入の両方の位置に現れるためです。FileNode の訪問は、ファイルごとのカウンターをリセットするために使用します。

ルールが refactor() から null を返す場合は、「変更なし」を示します。ノードを返すルールは置換を示します。Stmt[](ステートメントのリスト)を返すルールは、1 つのステートメントを複数に展開します。これが、DowngradeCloneWithRector が 1 つの return clone($this, [...]); を、clone 代入、オーバーライドごとに 1 つのプロパティ代入、そして最後の return に変換する仕組みです。

2 つのパイプライン設定は ->withDowngradeSets(php81: true)->withDowngradeSets(php74: true) を呼び出します。これらのセットは、ターゲット向けのすべての組み込みダウングレードルールを連結します。カスタムルールは、ギャップを埋めるためだけに存在します。具体的には、PHP 8.4 の非対称可視性、PHP 8.5 の clone-with、PHP 8.2 のトレイト定数です。Rector はこれらのいずれも単独ではダウングレードしません。同様のギャップを確認したうえでのみ、カスタムルールを記述してください。

次の手順で新しいルールを追加します。ここで使用する例は既存のルールの構成に沿っています。対象の機能に置き換えてください。

  1. ルールクラスを rector/rules/ に作成します。既存のオートロードマッピングに取り込まれるよう、クラス名を Downgrade<Feature>Rector とし、NextPDF\Backport 名前空間に配置します。
  2. ベースクラスの Rector\Rector\AbstractRector を継承し、クラスを final としてマークします。
  3. ルールが必要とする最小限の AST ノードクラスのセットを返すように getNodeTypes() を実装します。対象を狭くするほど、Rector が訪問するノードは少なくなります。
  4. メソッド refactor() を実装します。ノードが宣言された型の 1 つであることをアサートし、対象とする正確な構文をガードし、それを変換して、新しいノード(または Stmt[]、変更なしの場合は null)を返します。
  5. ルールが処理する個別のケースごとに 1 つの before/after CodeSample を用意して、メソッド getRuleDefinition() を実装します。
  6. ファイルを PHPStan Level 10 に保ちます。つまり、すべてのパラメーター、戻り値、プロパティが型付けされ、PHPDoc のジェネリクスが配列の形状を記述します。

非対称可視性ルールは、最小限で完結した例です。このルールは set 可視性フラグを削除し、基本となる read 可視性が残ることを保証します。

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

最初の if のガードが重要です。プロパティに set 可視性フラグがない場合、ルールは null を返し、ノードをそのままにします。無条件に変換するルールは、触れるべきでないコードまで書き換えてしまいます。

各ルールには、.php.inc ファイルに対してルールを実行し、出力が一致することをアサートするフィクスチャベースのテストがあります。このハーネスは Rector の Rector\Testing\PHPUnit\AbstractRectorTestCase が提供します。

テストケースは小さく、既存の 3 つのルールで共通です。

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

テストは、テスト対象のルールのみを登録する tests/Rector/config/ 内のルールごとの設定ファイルを参照するため、フィクスチャは 1 つのルールを単独で検証します。

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

フィクスチャは .php.inc ファイルで、入力、----- セパレーター、および期待される出力を含みます。ルールが変更を加えない場合は、セパレーターと 2 番目のブロックを省略します。トレイト定数ルール用の変換フィクスチャは、次のようになります。

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

新しいルールのテストは、次の手順で作成します。

  1. ディレクトリ tests/Rector/Fixtures/Downgrade<Feature>/ を作成し、ケースごとに 1 つの .php.inc を追加します。
  2. 変換するケース(----- セパレーターあり)とスキップするケース(セパレーターなし)の両方をカバーします。たとえば、set 可視性のないプロパティは変更されずにそのまま通過する必要があります。
  3. 対象のルールのみを登録する tests/Rector/config/downgrade_<feature>.php を追加します。
  4. フィクスチャディレクトリを yield し、設定を指す tests/Rector/Downgrade<Feature>RectorTest.php を追加します。
  5. テストスイートを実行します。
Terminal window
composer test

リポジトリにはさらに RectorRulesBehaviorTestRectorRulesMetadataTest が含まれており、ルール間の動作と、各ルールの getRuleDefinition() が適切な形式であることをアサートします。これらのゲートが新しいルールを認識できるよう、composer test 全体を実行します。

ルールは、パイプライン設定に登録されるまでビルドで有効になりません。2 つのビルドターゲットがあり、それぞれが rector/config/ に独自の設定を持ちます。

  1. 設定ファイル rector/config/rector-php81.php を開き、対象のルールクラスを ->withRules([...]) のリストに追加します。
  2. その機能を PHP 7.4 コアビルド向けにもダウングレードする必要がある場合は、同じクラスを rector/config/rector-php74.php に追加します。
  3. 既存のエントリに合わせて、その機能が導入された PHP バージョンを示すコメントを追加します。
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,
]);

ビルドオーケストレーター(scripts/build.php)は、ソースリポジトリをマージし、これらの設定で Rector を実行し、生成された composer.json を調整し、出力に対して php -l の構文チェックを実行します。ルールを前提にする前に、PHPStan とフルビルドでルールを検証してください。

Terminal window
composer analyse
composer build:dry
  • 登録順序自体は重要ではありませんが、概念上のルール順序は重要です。 Rector のマルチパス機構は、どのルールもこれ以上変更を加えなくなるまで再走査するため、設定内でルールを手作業で順序付けする必要はありません。それでも、DowngradeCloneWithRector が行っているように、順序依存性はクラスの docblock に記録してください。その展開は $clone->prop = $val を生成しますが、これは readonly プロパティでは失敗するため、同じターゲットに対して DowngradeReadonlyPropertyRector を実行する必要があります。
  • 異なる種類のノードから置換ノードを構築する場合は、空の属性を渡します。 DowngradeTraitConstantsRectorPropertyClassConst から構築し、属性にはソースノードの属性ではなく [] を渡します。元の属性を引き継ぐと、origNode ポインターが誤った種類のノードを指したままになり、フォーマット保持プリンターでアサーションが発生します。
  • ファイルごとの状態は FileNode の訪問でリセットします。 DowngradeCloneWithRector は、各ファイルの先頭で一時変数カウンターをリセットし、生成される変数名がファイル間で衝突しないようにするためだけに、FileNode::classgetNodeTypes() で宣言します。
  • 正確にガードしてから null を返します。 clone-with 変換では、処理を行う前に呼び出し名が clone であり、2 番目の引数が配列リテラルであることを確認する必要があります。単純な clone $obj は関数呼び出しとしてルールに到達することはなく、2 番目の引数が配列でない 2 引数の呼び出しはそのまま残されます。
  • ターゲットが表現できない修飾子は取り除きます。 トレイト定数ルールが定数を static プロパティに変換するとき、可視性は保持して static を追加しますが、PHP 8.1 のプロパティは final にできないため、final 修飾子を引き継いではなりません。
  • ルールを PHPStan Level 10 に保ちます。 リポジトリは composer analyse を、rector/rulesscripts に対してレベル 10 で実行します。すべてのシグネチャに型を付け、配列の形状をアノテートします。アナライザーを通過できないルールは、ドラフトではなく欠陥です。
  • Backport Builder 開発者ガイド — これらのルールを含むパイプラインアーキテクチャ、ブランチモデル、リリースアーティファクト。
  • Backport API リファレンス — バックポートビルドツールの公開インターフェイス。
  • Backport の構成 — ビルドターゲットとダウングレードセットの選択。
  • Backport のトラブルシューティング — 生成されたダウングレードツリー内の障害診断。
  • Rector ドキュメント — AbstractRectorRuleDefinition、および getNodeTypes() で使用される AST ノードクラスに関する上流のリファレンス。