news 2026/9/15 12:20:56

Rector 升级指南:从 1.x 迁移到 2.x 的自定义规则改写实战(FileNode / AbstractRector / ScopeFetcher)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rector 升级指南:从 1.x 迁移到 2.x 的自定义规则改写实战(FileNode / AbstractRector / ScopeFetcher)

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 过程中对自定义规则开发者的破坏性变更,包括FileWithoutNamespaceFileNode的迁移、beforeTraverse()的弃用、AbstractScopeAwareRector的移除与ScopeFetcher的引入、getRuleDefinition()的废弃以及SetListInterface的删除。读完本文,你将能够把基于旧 API 编写的自定义 Rector 规则一键迁移到 2.x 架构,并理解FileNode在文件级改造(如自动添加declare(strict_types=1))中的底层工作方式。

升级总览:两个版本跨度,三类变更

Rector 的升级路径分为两个阶段,分别对应不同的破坏性变更:

  • 2.2.14 → 2.3:聚焦节点抽象层重构,核心是FileWithoutNamespaceFileNode取代,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表示“没有命名空间的文件”,它只能承载无命名空间文件的顶层语句。这带来两个问题:

  1. 有命名空间的文件与无命名空间的文件被分到两种节点模型中,规则编写者必须区分处理;
  2. 修改文件顶层结构(例如插入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 迁移示例:从beforeTraverseFileNode::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->stmtsFileNode的公共可写属性(见 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常量将FileNodeClassMethodFunction_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 对齐(例如DeclareItemPropertyItem等新拆分出的节点类型,见 vendor/nikic/php-parser/lib/PhpParser/Node 目录);
  • 自定义NodeVisitorNodeTraverser的交互是否仍符合预期。

官方针对 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,仅在确实需要时通过ScopeFetcherScope

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 就仍带有RuleDefinitionCodeSample。这是因为官方规则需要文档便于开发者搜索理解;而本地自定义规则不再被强制要求。文档建议:如果你担心几个月后看不懂自己的规则,就把说明写在类上方的 docblock 里——这是最朴素可靠的文档位置。

3.3SetListInterface删除

SetListInterfaceRector\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 示例未覆盖的工程细节:

  1. 前置守卫hasShebang()跳过 shebang 脚本文件(declare必须位于文件首行,shebang 会与之冲突);isNamespaced()排除无命名空间文件(全局命名空间文件常被 include 进其他文件,加strict_types可能影响包含方)。
  2. 去重检查:通过DeclareStrictTypeFinder确认首条语句不是已有的declare(strict_types=1)——PHP 允许重复declare,但strict_types指令只允许出现在第一条语句。
  3. 语句插入:用$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 写法依据
文件级节点FileWithoutNamespaceFileNodeUPGRADING.md、FileNode.php
文件首部 hook覆写beforeTraverse()getNodeTypes()返回FileNode::classAbstractRector.php
获取 ScopeAbstractScopeAwareRector::refactorWithScope()ScopeFetcher::fetch($node)ScopeFetcher.php
规则说明强制getRuleDefinition()可选,或用类 docblock核心规则仍保留(见 DeclareStrictTypesRector)
自定义 set listimplements SetListInterface纯常量数组src/Set/Contract
运行环境PHP ≥ 7.4、PHP-Parser 5、PHPStan 2UPGRADING.md

需要特别留意的三个陷阱:

  1. ScopeFetcher::fetch()的时机:它依赖节点上的SCOPEattribute(由 Rector 的NodeScopeAndMetadataDecorator在解析阶段注入)。对刚创建、尚未刷新 scope 的节点调用会直接抛异常,因此只应在“读取既有节点”时使用。
  2. FileNodeNamespace_的去重协作:有命名空间文件同时会命中FileNodeNamespace_两个节点,务必像 1.4 节那样用isNamespaced()做守卫,否则同一份 stmts 会被处理两次。
  3. 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 12:20:42

OpenClaw权限问题解决方案:从基础到高级部署

1. OpenClaw本地无权限问题的典型表现当你在Windows或Linux系统上部署OpenClaw时&#xff0c;可能会遇到以下几种典型的权限错误提示&#xff1a;Windows系统常见错误&#xff1a;"拒绝访问"弹窗&#xff08;错误代码0x80070005&#xff09;控制台输出"ERROR: P…

作者头像 李华
网站建设 2026/9/15 12:20:04

代付系统源码拆解:状态机、权限控制与API回调设计

简介&#xff1a;一套面向代付业务场景的手工/API代付系统源码&#xff0c;适合有PHP开发基础的工程师或小微支付团队研究、二次开发及业务部署。系统整体设计简洁&#xff0c;支持单笔与批量代付&#xff0c;同时提供API自动对接与后台手动出款两条路径&#xff0c;后台-代理-…

作者头像 李华
网站建设 2026/9/15 12:17:49

用ima构建个人知识库:从导入到AI检索的完整教程

很多人收藏了一堆好文章、存了一堆PDF和笔记&#xff0c;真到用的时候却翻不到、找不到&#xff0c;知识散落得到处都是。腾讯出的免费工具 ima&#xff0c;就是来解决这个问题的——它能把网页、文档、碎片想法统一收进一个个人知识库&#xff0c;然后用 AI 检索的方式直接问、…

作者头像 李华
网站建设 2026/9/15 12:16:10

Unity后处理实战指南:从URP配置到性能优化与调参心法

很多朋友做Unity项目做到最后&#xff0c;总觉得画面“差口气”。模型面数不低&#xff0c;贴图也是高清的&#xff0c;灯光该打的也打了&#xff0c;但渲染出来就是干巴巴的&#xff0c;没有那种“商业项目”的质感。这时候&#xff0c;八成是后处理&#xff08;Post Processi…

作者头像 李华