사용자 정의 Rector 다운그레이드 규칙 작성
한눈에 보기
섹션 제목: “한눈에 보기”NextPDF 백포트 파이프라인은 nextpdf/nextpdf의 PHP 8.4 소스를 다운그레이드하여 PHP 8.1(그리고 코어의 경우 PHP 7.4)에서 실행되도록 합니다. 대부분의 언어 기능에는 Rector의 기본 제공 다운그레이드 세트를 사용하며, Rector가 기본적으로 다루지 않는 기능에는 소수의 사용자 정의 규칙을 추가로 사용합니다.
이 가이드는 새로운 사용자 정의 규칙을 추가하는 방법을 설명합니다. 저장소에 이미 있는 세 가지 규칙인 DowngradeAsymmetricVisibilityRector, DowngradeCloneWithRector, DowngradeTraitConstantsRector의 구조를 따릅니다. 세 규칙은 모두 rector/rules/에 위치하며, rector/config/에 등록되고, tests/Rector/의 픽스처 기반 테스트로 검증됩니다.
NextPDF 기능이 빌드 대상에서 지원하지 않는 PHP 구문을 사용하고, Rector에 이를 위한 기본 제공 다운그레이드가 없을 때 이 가이드를 참조하십시오. 시작하기 전에 Rector에 실제로 해당 규칙이 없는지 확인하십시오. 기본 제공 withDowngradeSets() 체인은 이미 readonly 클래스, 타입이 지정된 클래스 상수, 파이프 연산자를 비롯한 여러 기능을 처리합니다.
사용자 정의 규칙은 nextpdf-backport 저장소 안에서 작성하고 실행합니다. 개발 도구는 해당 저장소의 composer.json에 선언되어 있습니다.
- 백포트 저장소를 클론하고 의존성을 설치합니다.
- Rector와 PHPUnit이 정상적으로 확인되는지 검증합니다.
git clone https://github.com/nextpdf-labs/backport.gitcd backportcomposer installvendor/bin/rector --versionvendor/bin/phpunit --version빌드 출력은 PHP 8.1 또는 7.4를 대상으로 하지만, 저장소를 실행하려면 PHP 8.4가 필요합니다(규칙이 8.4 구문 트리를 조작하기 때문입니다). composer.jsonrequire 항목은 php를 >=8.4 <9.0으로 고정합니다. 사용자 정의 규칙은 NextPDF\Backport\ 네임스페이스로 자동 로드되며, 이 네임스페이스는 rector/rules/에 매핑됩니다. 테스트는 NextPDF\Backport\Tests\로 자동 로드되며, 이는 tests/에 매핑됩니다.
규칙 구조
섹션 제목: “규칙 구조”모든 사용자 정의 규칙은 Rector\Rector\AbstractRector를 확장하고 세 개의 메서드를 구현합니다. 이 계약(contract)은 기존 세 규칙 모두에서 동일합니다.
| 멤버 | 유형 | 용도 |
|---|---|---|
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는 트레이트 본문 전체를 한 번에 다시 작성하므로 [Trait_::class]를 대상으로 합니다. DowngradeCloneWithRector는 [FileNode::class, Return_::class, Expression::class]를 대상으로 합니다. clone-with(clone($obj, [...]))가 return과 할당 위치 모두에 나타나기 때문입니다. 이 규칙은 파일별 카운터를 초기화하기 위해 FileNode 방문을 사용합니다.
규칙이 refactor()에서 null을 반환하면 “변경 없음”을 의미합니다. 노드를 반환하면 해당 노드로 대체됩니다. Stmt[](문 목록)를 반환하면 하나의 문을 여러 문으로 확장합니다. 이러한 방식으로 DowngradeCloneWithRector는 하나의 return clone($this, [...]);를 clone 할당문, 재정의별 프로퍼티 할당문, 마지막 return으로 변환합니다.
기본 제공 세트가 이미 처리하는 항목
섹션 제목: “기본 제공 세트가 이미 처리하는 항목”두 파이프라인 구성은 ->withDowngradeSets(php81: true)와 ->withDowngradeSets(php74: true)를 호출합니다. 이 세트들은 대상에 필요한 모든 기본 제공 다운그레이드 규칙을 연결합니다. 사용자 정의 규칙은 이 세트가 처리하지 못하는 부분에만 존재합니다. 즉, PHP 8.4 비대칭 가시성, PHP 8.5 clone-with, PHP 8.2 트레이트 상수입니다. Rector는 이들 중 어느 것도 자체적으로 다운그레이드하지 않습니다. 동일한 빈틈을 확인한 후에만 사용자 정의 규칙을 작성하십시오.
단계별: 규칙 작성하기
섹션 제목: “단계별: 규칙 작성하기”이 절차는 새로운 규칙을 추가합니다. 진행 예제는 기존 규칙의 형태를 그대로 따르므로, 여러분의 기능으로 대체하십시오.
- 규칙 클래스를
rector/rules/에 생성합니다. 기존 자동 로드 매핑이 인식할 수 있도록 이름을Downgrade<Feature>Rector로 지정하고NextPDF\Backport네임스페이스에 배치합니다. - 클래스가
Rector\Rector\AbstractRector를 확장하도록 하고final로 표시합니다. - 규칙에 필요한 가장 좁은 AST 노드 클래스 집합을 반환하도록
getNodeTypes()를 구현합니다. 집합이 좁을수록 Rector가 방문하는 노드 수가 줄어듭니다. - 메서드
refactor()를 구현합니다. 노드가 선언된 유형 중 하나인지 단언하고, 대상으로 하는 정확한 구문에 가드를 둔 뒤 변환하여 새 노드(또는Stmt[], 변경이 없으면null)를 반환합니다. - 메서드
getRuleDefinition()을 구현하되, 규칙이 처리하는 개별 사례마다 before/afterCodeSample을 하나씩 둡니다. - 파일을 PHPStan 레벨 10으로 유지합니다. 즉, 모든 매개변수, 반환값, 프로퍼티에 타입을 지정하고, PHPDoc 제네릭으로 배열 형태를 기술합니다.
비대칭 가시성 규칙은 가장 작지만 완전한 예제입니다. 이 규칙은 set 가시성 플래그를 제거하고 기본 읽기 가시성이 유지되도록 보장합니다:
<?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에서 가져옵니다.
테스트 케이스는 작으며, 기존 세 규칙 전반에서 일관된 형태를 갖습니다:
<?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/의 규칙별 구성 파일을 가리키므로, 픽스처는 하나의 규칙만 격리해서 실행합니다:
<?php
declare(strict_types=1);
use NextPDF\Backport\DowngradeTraitConstantsRector;use Rector\Config\RectorConfig;
return RectorConfig::configure() ->withRules([ DowngradeTraitConstantsRector::class, ]);픽스처는 .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; }}?>새 규칙의 테스트를 작성하려면:
tests/Rector/Fixtures/Downgrade<Feature>/디렉터리를 생성하고 사례마다.php.inc파일을 하나씩 추가합니다.- 변환 사례(
-----구분자 포함)와 건너뛰기 사례(구분자 없음)를 모두 다룹니다. 예를 들어 set 가시성이 없는 프로퍼티는 변경 없이 그대로 통과해야 합니다. - 해당 규칙만 등록하는
tests/Rector/config/downgrade_<feature>.php를 추가합니다. - 픽스처 디렉터리를 산출하고 구성을 가리키는
tests/Rector/Downgrade<Feature>RectorTest.php를 추가합니다. - 테스트 스위트를 실행합니다.
composer test저장소에는 RectorRulesBehaviorTest와 RectorRulesMetadataTest도 포함되어 있으며, 이 테스트들은 교차 규칙 동작과 각 규칙의 getRuleDefinition()이 올바른 형식인지를 단언합니다. 이러한 게이트가 새 규칙을 인식하도록 전체 composer test를 실행하십시오.
빌드에 연결하기
섹션 제목: “빌드에 연결하기”규칙은 파이프라인 구성에 등록되기 전까지 빌드에서 활성화되지 않습니다. 빌드 대상은 두 가지이며, 각각 rector/config/에 자체 구성을 가지고 있습니다.
- 파일
rector/config/rector-php81.php를 열고->withRules([...])목록에 규칙 클래스를 추가합니다. - 해당 기능이 PHP 7.4 코어 빌드에서도 다운그레이드되어야 한다면 동일한 클래스를
rector/config/rector-php74.php에 추가합니다. - 기존 항목과 동일한 형식으로, 해당 기능을 도입한 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과 전체 빌드로 규칙을 검증하십시오.
composer analysecomposer build:dry엣지 케이스 및 주의 사항
섹션 제목: “엣지 케이스 및 주의 사항”- 등록 순서는 중요하지 않지만, 규칙 순서는 개념적으로 중요합니다. Rector의 다중 패스 메커니즘은 어떤 규칙도 더 이상 변경하지 않을 때까지 다시 순회하므로, 구성 파일에 적힌 규칙 순서를 그대로 강제하지 않습니다. 그렇더라도
DowngradeCloneWithRector처럼 순서 의존성이 있다면 클래스 docblock에 문서화하십시오. 이 규칙의 확장은$clone->prop = $val을 생성하는데, 이는 readonly 프로퍼티에서 실패하므로DowngradeReadonlyPropertyRector가 동일한 대상에 대해 실행되어야 합니다. - 다른 종류의 노드로부터 대체 노드를 빌드할 때는 빈 속성을 전달하십시오.
DowngradeTraitConstantsRector는ClassConst에서Property를 빌드하며, 속성에는 소스 노드의 속성이 아닌[]를 전달합니다. 원래 속성을 그대로 옮기면origNode포인터가 잘못된 노드 종류를 가리키게 되어 형식 보존 출력기(printer)에서 단언(assertion)이 발생합니다. FileNode방문 시 파일별 상태를 초기화하십시오.DowngradeCloneWithRector는 각 파일의 시작 시점에 임시 변수 카운터를 초기화하기 위한 목적으로만FileNode::class를getNodeTypes()에서 선언하므로, 생성된 변수 이름이 파일 간에 충돌하지 않습니다.- 정확하게 가드한 다음,
null을 반환하십시오. clone-with 변환은 실행하기 전에 호출 이름이clone이고 두 번째 인수가 배열 리터럴인지 확인해야 합니다. 일반적인clone $obj는 함수 호출이 아니므로 이 규칙에 도달하지 않으며, 두 번째 인수가 배열이 아닌 두 인수 호출은 그대로 둡니다. - 대상이 표현할 수 없는 한정자(modifier)는 제거하십시오. 트레이트 상수 규칙이 상수를 정적 프로퍼티로 변환할 때는 가시성을 보존하고
static를 추가하지만, PHP 8.1 프로퍼티는 final이 될 수 없으므로final한정자를 가져서는 안 됩니다. - 규칙을 PHPStan 레벨 10으로 유지하십시오. 저장소는
composer analyse를rector/rules와scripts에 대해 레벨 10으로 실행합니다. 모든 시그니처에 타입을 지정하고 배열 형태에 주석을 달아야 합니다. 분석기를 통과하지 못하는 규칙은 초안이 아니라 결함입니다.
참고 항목
섹션 제목: “참고 항목”- Backport Builder 개발자 가이드 — 이 규칙들을 둘러싼 파이프라인 아키텍처, 브랜치 모델, 릴리스 아티팩트를 설명합니다.
- Backport API 참조 — 백포트 빌드 도구의 공개 표면(surface)입니다.
- Backport 구성 — 빌드 대상과 다운그레이드 세트 선택을 설명합니다.
- Backport 문제 해결 — 생성된 다운그레이드 트리에서 발생하는 실패를 진단하는 방법입니다.
- Rector 문서 —
AbstractRector,RuleDefinition, 그리고getNodeTypes()에서 사용되는 AST 노드 클래스에 대한 상위(upstream) 참조입니다.