Rector 升级指南:从 1.x 迁移到 2.x 的自定义规则改写实战(FileNode / AbstractRector / ScopeFetcher)
【免费下载链接】rectorInstant Upgrades and Automated Refactoring of any PHP 5.3+ code项目地址: https://gitcode.com/GitHub_Trending/re/rector
本指南基于当前仓库根目录的 UPGRADING.md,系统梳理 Rector 从 1.x 升级到 2.0、以及从 2.2.14 升级到 2.3 过程中对自定义规则开发者的破坏性变更,包括FileWithoutNamespace到FileNode的迁移、beforeTraverse()的弃用、AbstractScopeAwareRector的移除与ScopeFetcher的引入、getRuleDefinition()的废弃以及SetListInterface的删除。读完本文,你将能够把基于旧 API 编写的自定义 Rector 规则一键迁移到 2.x 架构,并理解FileNode在文件级改造(如自动添加declare(strict_types=1))中的底层工作方式。
升级总览:两个版本跨度,三类变更
Rector 的升级路径分为两个阶段,分别对应不同的破坏性变更:
- 2.2.14 → 2.3:聚焦节点抽象层重构,核心是
FileWithoutNamespace被FileNode取代,beforeTraverse()生命周期方法被冻结。 - 1.x → 2.0:底层解析与静态分析引擎全面换代(PHP-Parser 5、PHPStan 2、PHP 7.4 运行门槛),同时对自定义规则 API 做了三处简化:移除
AbstractScopeAwareRector、移除强制的getRuleDefinition()、移除SetListInterface。
下面逐一展开,并在每一节结合当前仓库源码验证文档所述行为。
一、2.2.14 → 2.3:FileNode取代FileWithoutNamespace
1.1 变更背景
在 2.2.14 及之前,Rector 使用Rector\PhpParser\Node\FileWithoutNamespace表示“没有命名空间的文件”,它只能承载无命名空间文件的顶层语句。这带来两个问题:
- 有命名空间的文件与无命名空间的文件被分到两种节点模型中,规则编写者必须区分处理;
- 修改文件顶层结构(例如插入
declare(strict_types=1))时,两种文件各有一套逻辑。
变更内容:FileWithoutNamespace已废弃,由FileNode(src/PhpParser/Node/FileNode.php)取代。FileNode同时表示有命名空间和无命名空间的文件,并且允许直接修改文件内部的语句(stmts)。
1.2beforeTraverse()被标记为@final
文档明确:beforeTraverse()现在被标记为@final,规则作者不应再覆写它,而应改用getNodeTypes()配合FileNode::class来声明对文件级节点的兴趣。
这一点在源码中得到直接印证:src/Rector/AbstractRector.php 中beforeTraverse()已被声明为final,且仅返回null:
/** * @return Node[]|null * * @internal */ final public function beforeTraverse(array $nodes): ?array { return null; }同时,RectorNodeTraverser 的traverse()会在进入节点遍历前调用每个 visitor 的beforeTraverse(),若返回非null则替换整个节点数组——这正是旧代码中“节点 hacking”所依赖的入口。如今该入口被冻结,所有文件级操作都必须收敛到refactor()内完成。
1.3 迁移示例:从beforeTraverse到FileNode::refactor
文档给出的迁移前后对比是理解这次变更的最佳素材。迁移前(旧写法):
use Rector\PhpParser\Node\FileWithoutNamespace; use Rector\Rector\AbstractRector; final class SomeRector extends AbstractRector { public function getNodeTypes(): array { return [FileWithoutNamespace::class]; } public function beforeTraverse(array $nodes): array { // some node hacking } /** * @param FileWithoutNamespace $node */ public function refactor(Node $node): ?Node { // ... } }迁移后(新写法):不再覆写beforeTraverse(),getNodeTypes()返回[FileNode::class],所有逻辑在refactor()中通过操作$node->stmts完成。以“给文件顶部插入declare(strict_types=1)”为例:
use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; final class SomeRector extends AbstractRector { public function getNodeTypes(): array { return [FileNode::class]; } /** * @param FileNode $node */ public function refactor(Node $node): ?Node { foreach ($node->stmts as $stmt) { // check if has declare_strict already? // ... // create it $declareStrictTypes = $this->createDeclareStrictTypesNode(); // add it $node->stmts = array_merge([$declareStrictTypes], $node->stmts); } return $node; } }注意这里$node->stmts是FileNode的公共可写属性(见 FileNode.php),直接对它做array_merge即可改变文件顶层结构,返回$node后遍历器会将其写回。
1.4 同时处理命名空间文件与无命名空间文件
由于FileNode同时覆盖两种文件,若要操作“文件内首个语句块”,需要同时挂钩两个节点:FileNode(处理无命名空间文件)与PhpParser\Node\Stmt\Namespace_(处理有命名空间文件):
use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; use PhpParser\Node\Stmt\Namespace_; final class SomeRector extends AbstractRector { public function getNodeTypes(): array { return [FileNode::class, Namespace_::class]; } /** * @param FileNode|Namespace_ $node */ public function refactor(Node $node): ?Node { if ($node instanceof FileNode && $node->isNamespaced()) { // handled in the Namespace_ node return null; } foreach ($node->stmts as $stmt) { // modify stmts in desired way here } return $node; } }关键点在于FileNode::isNamespaced()(FileNode.php):它遍历$stmts,若发现任意Namespace_子节点则返回true。上面的守卫逻辑保证:有命名空间的文件交给Namespace_节点处理,无命名空间的文件才在FileNode中处理,二者互不重复。
1.5 从源码看FileNode的能力边界
FileNode并不仅仅是一个“语句容器”,从 FileNode.php 的实现可以看到它为文件级改造提供了一整套能力:
isNamespaced(): bool:判断文件是否包含Namespace_节点(L186-L196)。getNamespace(): ?Namespace_:当文件中恰好只有一个命名空间时返回它,否则返回null(L197-L205)。getUses()/getUsesAndGroupUses():收集文件根部的use语句(含GroupUse)(L209-L227)。addImports()/removeImports():在文件或命名空间内增删 use 导入,并自动去重、维护UsedImports追踪状态(L59-L109、L150-L170)。getType()返回'Stmt_FileNode':配合BetterStandardPrinter::pStmt_FileNode()完成打印(L172-L178)。
此外,src/PhpParser/Enum/NodeGroup.php 的STMTS_AWARE常量将FileNode与ClassMethod、Function_、Namespace_等并列,意味着遍历器会对它按“含Stmt[] $stmts公共属性”的节点统一处理——这是它能够承载文件级改写的基础保障。
二、从 1.x 升级到 2.0:环境与底层引擎换代
2.0 是一次底层大版本跃迁,先满足运行环境要求,再谈规则迁移。
2.1 PHP 版本要求
Rector 现在需要PHP 7.4 或更高版本才能运行。这主要是为了支撑 PHP-Parser 5 与 PHPStan 2 对解析和静态分析能力的新要求,也意味着 CI 中运行 Rector 的环境需同步提升。
2.2 解析引擎:PHP-Parser 5
Rector 2.0 底层切换到 PHP-Parser 5。这对规则作者的影响主要体现在节点 API 层面——部分PhpParser\Node类的构造方式、子节点命名(sub node names)或遍历行为可能有细微差异。若你的自定义规则直接操作底层 AST 节点,升级后应重点检查:
- 节点构造参数与属性名是否与 PHP-Parser 5 对齐(例如
DeclareItem、PropertyItem等新拆分出的节点类型,见 vendor/nikic/php-parser/lib/PhpParser/Node 目录); - 自定义
NodeVisitor与NodeTraverser的交互是否仍符合预期。
官方针对 PHP-Parser 有独立的 5.0 升级指南(见 UPGRADING.md 中引用的上游文档),迁移时应一并对照。
2.3 静态分析引擎:PHPStan 2
Rector 的类型系统构建在 PHPStan 之上,升级到 PHPStan 2 后,类型 API 可能发生变化。规则中若直接引用PHPStan\Type\*、PHPStan\Analyser\Scope等类型,应确认所用方法与 PHPStan 2 的签名兼容。这也是下面ScopeFetcher新接口出现的重要背景之一。
三、自定义规则作者的三大迁移点(2.0 破坏性变更)
这是从 1.x 升级到 2.0 时,规则作者必须逐一处理的三个 API 变化。
3.1AbstractScopeAwareRector移除:改用AbstractRector+ScopeFetcher
Rector\Rector\AbstractScopeAwareRector在 2.0 中被彻底移除。该类的设计初衷是让规则直接拿到Scope,但官方认为“为取一个辅助对象而多一层抽象”让自定义规则创建变得含糊且复杂。
迁移前:
use Rector\Rector\AbstractScopeAwareRector; final class SimpleRector extends AbstractScopeAwareRector { public function refactorWithScope(Node $node, Scope $scope): ?Node { // ... } }迁移后:继承标准的AbstractRector,仅在确实需要时通过ScopeFetcher取Scope:
use Rector\Rector\AbstractRector; use Rector\PHPStan\ScopeFetcher; final class SimpleRector extends AbstractRector { public function refactor(Node $node): ?Node { if (...) { // this allow to fetch scope only when needed $scope = ScopeFetcher::fetch($node); } // ... } }ScopeFetcher的实现非常轻量(src/PHPStan/ScopeFetcher.php):它从节点的SCOPEattribute 中取出MutatingScope并作为PHPStan\Analyser\Scope返回;若节点上没有可用的 Scope(例如改动后的新节点未刷新 scope),会抛出ShouldNotHappenException,提示“先修复变更节点的 scope 刷新”。这提醒我们:只有在修改前读取原有节点的类型信息时才应调用fetch(),对于新建节点不要依赖 scope。
3.2getRuleDefinition()不再是必选
1.x 时代,每个规则都强制实现getRuleDefinition(): RuleDefinition,返回描述与代码示例,用于文档生成与规则检索。但实际上很多本地自定义规则只是草草填个空壳,纯粹为了“让 Rector 高兴”。
2.0 中getRuleDefinition()方法已从AbstractRector移除,规则作者不再需要它:
use Rector\Rector\AbstractRector; -use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; -use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; final class SimpleRector extends AbstractRector { - public function getRuleDefinition(): RuleDefinition - { - return new RuleDefinition('// @todo fill the description', [ - new CodeSample( - <<<'CODE_SAMPLE' -// @todo fill code before -CODE_SAMPLE - , - <<<'CODE_SAMPLE' -// @todo fill code after -CODE_SAMPLE - ), - ]); - } // valuable code here }值得注意的是:核心规则(进入官方规则集)仍然保留getRuleDefinition()——例如 rules/TypeDeclaration/Rector/StmtsAwareInterface/DeclareStrictTypesRector.php 就仍带有RuleDefinition与CodeSample。这是因为官方规则需要文档便于开发者搜索理解;而本地自定义规则不再被强制要求。文档建议:如果你担心几个月后看不懂自己的规则,就把说明写在类上方的 docblock 里——这是最朴素可靠的文档位置。
3.3SetListInterface删除
SetListInterface(Rector\Set\Contract\SetListInterface)作为一个已废弃的接口,在 2.0 中被彻底删除。如果你自定义过 set list,只需把接口摘掉即可:
-use Rector\Set\Contract\SetListInterface; -final class YourSetList implements SetListInterface +final class YourSetList在当前仓库的 src/Set/Contract 与 src/Set/Enum 目录中已找不到该接口的踪迹,证实其确实已从代码库移除。config/set/下的各 PHP 版本与功能 set 文件(如 config/set/code-quality.php、config/set/php81.php)现在都以普通常量数组形式组织,不再依赖该接口。
四、实战验证:仓库内真实使用FileNode的规则
UPGRADING.md 中的示例并非空谈——当前仓库已经存在一个真实规则,几乎就是文档示例的生产版本:rules/TypeDeclaration/Rector/StmtsAwareInterface/DeclareStrictTypesRector.php(“Adddeclare(strict_types=1)if missing in a namespaced file”)。
该规则的关键实现与文档示例一一对应:
/** * @param FileNode $node */ public function refactor(Node $node): ?FileNode { // shebang files cannot have declare strict types if ($this->getFile()->hasShebang()) { return null; } // only add to namespaced files, as global namespace files are often included in other files if (!$node->isNamespaced()) { return null; } // when first stmt is Declare_, verify if there is strict_types definition already, // as multiple declare is allowed, with declare(strict_types=1) only allowed on very first stmt if ($this->declareStrictTypeFinder->hasDeclareStrictTypes($node)) { return null; } $declaresStrictType = $this->nodeFactory->createDeclaresStrictType(); $node->stmts = array_merge([$declaresStrictType, new Nop()], $node->stmts); return $node; } /** * @return array<class-string<Node>> */ public function getNodeTypes(): array { return [FileNode::class]; }它演示了文件级规则的标准套路,也补充了 UPGRADING.md 示例未覆盖的工程细节:
- 前置守卫:
hasShebang()跳过 shebang 脚本文件(declare必须位于文件首行,shebang 会与之冲突);isNamespaced()排除无命名空间文件(全局命名空间文件常被 include 进其他文件,加strict_types可能影响包含方)。 - 去重检查:通过
DeclareStrictTypeFinder确认首条语句不是已有的declare(strict_types=1)——PHP 允许重复declare,但strict_types指令只允许出现在第一条语句。 - 语句插入:用
$this->nodeFactory->createDeclaresStrictType()构造节点,再以array_merge([$declaresStrictType, new Nop()], $node->stmts)插到stmts头部,中间补一个Nop(空行节点)保证格式美观。
这套“守卫 → 去重 → 构造 → 前置合并 → 返回节点”的模式,是所有FileNode文件级规则的推荐写法,可参考 rules/TypeDeclaration/Rector/StmtsAwareInterface/SafeDeclareStrictTypesRector.php 与 vendor/rector/rector-phpunit/rules/CodeQuality/Rector/StmtsAwareInterface/DeclareStrictTypesTestsRector.php 查看更多同族实现。
五、迁移清单与常见陷阱
将以上变更整理成一份可勾选的迁移清单:
| 检查项 | 1.x 写法 | 2.x 写法 | 依据 |
|---|---|---|---|
| 文件级节点 | FileWithoutNamespace | FileNode | UPGRADING.md、FileNode.php |
| 文件首部 hook | 覆写beforeTraverse() | getNodeTypes()返回FileNode::class | AbstractRector.php |
| 获取 Scope | AbstractScopeAwareRector::refactorWithScope() | ScopeFetcher::fetch($node) | ScopeFetcher.php |
| 规则说明 | 强制getRuleDefinition() | 可选,或用类 docblock | 核心规则仍保留(见 DeclareStrictTypesRector) |
| 自定义 set list | implements SetListInterface | 纯常量数组 | src/Set/Contract |
| 运行环境 | — | PHP ≥ 7.4、PHP-Parser 5、PHPStan 2 | UPGRADING.md |
需要特别留意的三个陷阱:
ScopeFetcher::fetch()的时机:它依赖节点上的SCOPEattribute(由 Rector 的NodeScopeAndMetadataDecorator在解析阶段注入)。对刚创建、尚未刷新 scope 的节点调用会直接抛异常,因此只应在“读取既有节点”时使用。FileNode与Namespace_的去重协作:有命名空间文件同时会命中FileNode与Namespace_两个节点,务必像 1.4 节那样用isNamespaced()做守卫,否则同一份 stmts 会被处理两次。declare(strict_types=1)的位置约束:它必须是文件第一条语句;插入时若文件已有其他declare,需先校验首个语句(参考DeclareStrictTypesRector的去重逻辑)。
结语
Rector 2.x 的升级方向非常清晰:简化规则作者的心智负担——用统一的FileNode替代两种文件模型,用按需的ScopeFetcher替代强制的 scope 注入,用可选的 docblock 替代空洞的RuleDefinition,并彻底清理已废弃的SetListInterface。对自定义规则开发者而言,迁移工作量集中在本文列出的几处机械改动上;而对文件级规则(自动插入declare、调整 use 导入、统计顶层语句等)的编写者来说,FileNode提供的isNamespaced()、addImports()、removeImports()等能力让这类改造第一次有了正式、稳定的入口。建议在升级后对照文末清单逐项检查,并以仓库中DeclareStrictTypesRector等规则为模板重写自己的文件级规则。
【免费下载链接】rectorInstant Upgrades and Automated Refactoring of any PHP 5.3+ code项目地址: https://gitcode.com/GitHub_Trending/re/rector
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考