Rector 中的 PHP 文本差异计算与解析:sebastian/diff 组件全解析
Rector 中的 PHP 文本差异计算与解析sebastian/diff 组件全解析【免费下载链接】rectorInstant Upgrades and Automated Refactoring of any PHP 5.3 code项目地址: https://gitcode.com/GitHub_Trending/re/rectorsebastian/diff 是一套从 PHPUnit 中独立出来的 PHP 差异diff实现组件提供基于 Myers 算法的文本差异计算、多种 unified diff 输出格式以及 unified diff 文本的反向解析能力。在 rector 项目中它被封装为DefaultDiffer用于在命令行终端渲染代码改动前后old/new 文件内容的差异报告。读完本文你将掌握该组件的安装、三种输出构建器的选型与配置、Parser解析后的对象图结构以及它在 rector 变更报告链路中的真实调用方式。一、组件概览与安装该组件以sebastian/diff为包名BSD-3-Clause 许可作者为 PHPUnit 的维护者 Sebastian Bergmann 等见 vendor/sebastian/diff/composer.json。原 README 文档位于 vendor/sebastian/diff/README.md完整描述了安装、diff 生成、diff 解析三大核心用法。作为 Composer 依赖安装到当前项目# 作为项目运行时依赖 composer require sebastian/diff如果仅在开发阶段需要例如运行测试套件时比对期望输出则应作为 dev 依赖安装composer require --dev sebastian/diffrector 项目自身便是通过 Composer 引入该组件的典型场景——从 src/Differ/DefaultDiffer.php 的导入语句可以看出它在命名空间RectorPrefix202609\SebastianBergmann\Diff下使用该组件RectorPrefix202609是 rector 对 vendor 依赖进行命名空间前缀隔离的产物以避免与用户项目中的同名依赖冲突。二、使用 Differ 生成差异文本Differ类是生成两个字符串之间差异的核心入口。它接收一个实现了DiffOutputBuilderInterface的输出构建器并通过diff(string $from, string $to): string返回格式化后的差异文本。原文档给出的最小示例?php declare(strict_types1); use SebastianBergmann\Diff\Differ; use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; $differ new Differ(new UnifiedDiffOutputBuilder); print $differ-diff(foo, bar);输出如下--- Original New -foo bar从 src/Differ.php 的实现可以看到diff()的内部流程先把$from/$to按行拆分成 token 数组支持字符串与数组两种输入再由diffToArray()计算编辑脚本最后把[token, type]构成的中间数组交给输出构建器渲染成最终字符串。Differ还定义了 5 个行类型常量常量值含义Differ::OLD0未变更行Differ::ADDED1新增行Differ::REMOVED2删除行Differ::DIFF_LINE_END_WARNING3两侧行尾符不一致的警告行Differ::NO_LINE_END_EOF_WARNING4“文件末尾无换行”警告行diffToArray()还会自动检测新旧输入的行尾符是否一致detectUnmatchedLineEndings()见 src/Differ.php当一侧使用\r\n、另一侧使用\n时会在结果头部插入#Warning: Strings contain different line endings!警告条目。三、三种输出构建器与自定义接口Differ的格式输出完全委托给构造时传入的DiffOutputBuilderInterface实现。该组件内置三种构建器覆盖了从严格可回打的 unified diff到仅差异行的常见需求。3.1 UnifiedDiffOutputBuilder经典宽松格式原 README 示例使用的UnifiedDiffOutputBuilder生成unified diff格式不输出 hunk 行号即 形式是 PHPUnit 比对失败信息所使用的格式。需要说明的是当前仓库携带的 9.0-dev 版本中UnifiedDiffOutputBuilder与AbstractChunkOutputBuilder已被移除官方建议改用StrictUnifiedDiffOutputBuilder见 vendor/sebastian/diff/ChangeLog.md 的版本变更记录原文档示例保留它主要是展示经典的宽松格式用法。3.2 StrictUnifiedDiffOutputBuilder严格 unified diffStrictUnifiedDiffOutputBuilder生成带 hunk 头部 -from,range to,range 的严格 unified diff行为类似diff -u可直接兼容patch或git apply回打。其构造器接收一个关联数组选项默认值定义在 src/Output/StrictUnifiedDiffOutputBuilder.php选项默认值说明addLineNumberstrue为false时 hunk 头渲染为 不带行号为true时渲染为 -from,range to,range collapseRangestrue长度为 1 的 range 是否省略,1后缀commonLineThreshold6连续相同行达到该数量后结束当前 hunk 并开启新 hunkcontextLines3每个 hunk 携带的上下文行数与diff -u/-U NUM语义一致为保证patch/git apply兼容建议至少为 3emitDiffLineEndWarningfalse是否渲染Differ插入的行尾符不一致警告行该行不属于严格 unified diff 格式emitNoLineEndEofWarningtrue为false时抑制\ No newline at end of file标记适合与文件无关的字符串比对场景fromFile/toFilenull用于生成--- xxx/ yyy头部fromFileDate/toFileDatenull可选的文件时间戳追加在头部文件名后\t分隔headernull若提供则作为头部原样输出不再由文件名字段拼接构造函数会对每个选项做严格类型校验类型不合法会抛出ConfigurationException见 src/Output/StrictUnifiedDiffOutputBuilder.php。输出过程使用php://memory内存流缓冲若最终未检测到任何变更则返回空字符串如果输出的最后一个字符不是换行符会自动补一个换行src/Output/StrictUnifiedDiffOutputBuilder.php。3.3 DiffOnlyOutputBuilder仅差异行DiffOnlyOutputBuilder输出宽松 unified diff 格式中仅与变更相关的行——只保留新增行与-删除行不含行号、不含未变更上下文行。构造器可传入自定义头部字符串默认头部为--- Original\n New\n见 src/Output/DiffOnlyOutputBuilder.php。适合需要极简变更摘要的场景。3.4 自定义输出格式如果三种内置构建器都不满足需求可以实现DiffOutputBuilderInterface接口——唯一需要实现的方法签名是getDiff(array $diff): string入参即Differ::diffToArray()产出的listarray{0: mixed, 1: int}编辑脚本。接口定义位于 src/Output/DiffOutputBuilderInterface.php。这也解释了Differ的扩展点设计算法与格式解耦任何自定义渲染器都能复用 Myers 差异计算。四、底层算法Myers 线性空间 diff差异计算的核心算法是 src/MyersDiff.php 中的MyersDiff类。从类注释可以看到其理论依据Eugene W. Myers 1986 年论文An O(ND) Difference Algorithm and Its Variations第 4b 节描述的线性空间变体时间复杂度O((N M) * D)空间复杂度O(N M)N、M 为输入长度D 为编辑距离。calculate()返回[token, type]编辑脚本并在normalize()阶段对连续非匹配片段做排序归一化——把 REMOVED 行统一放在 ADDED 行之前输出以符合传统diff(1)的输出习惯src/MyersDiff.php。Differ::diffToArray()在调用算法前还会做一次公共前缀/后缀剥离优化getArrayDiffParted()见 src/Differ.php把首尾相同的行直接标记为 OLD 而不送入算法进一步缩小算法处理区间。五、使用 Parser 解析 unified diff与生成方向相反Parser类负责把 unified diff 文本解析成结构化的对象图。原文档示例通过 Git 获取两次提交之间的 diff 再交给解析器use SebastianBergmann\Diff\Parser; use SebastianBergmann\Git; $git new Git(/usr/local/src/money); $diff $git-getDiff( 948a1a07768d8edd10dcefa8315c1cbeffb31833, c07a373d2399f3e686234c4f7f088d635eb9641b ); $parser new Parser; print_r($parser-parse($diff));解析器对输入逐行处理识别四类结构正则定义见 src/Parser.php--- file/ file成对出现时开启一个新的文件 diffFROM_FILE_HEADER/TO_FILE_HEADER -start[,range] end[,range] 作为 hunk 头CHUNK_HEADER决定后续收集的行数diff --git、index ...等 git 元数据行被跳过METADATA_HEADER以空格、、-、\开头的行作为 hunk 内容行收集CHUNK_LINE。解析结果是一个Diff对象数组示例输出中的对象图层次如下Array ( [0] SebastianBergmann\Diff\Diff Object ( [from:SebastianBergmann\Diff\Diff:private] a/tests/MoneyTest.php [to:SebastianBergmann\Diff\Diff:private] b/tests/MoneyTest.php [chunks:SebastianBergmann\Diff\Diff:private] Array ( [0] SebastianBergmann\Diff\Chunk Object ( [start:SebastianBergmann\Diff\Chunk:private] 87 [startRange:SebastianBergmann\Diff\Chunk:private] 7 [end:SebastianBergmann\Diff\Chunk:private] 87 [endRange:SebastianBergmann\Diff\Chunk:private] 7 [lines:SebastianBergmann\Diff\Chunk:private] Array ( [0] SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] 3 [content:SebastianBergmann\Diff\Line:private] * covers SebastianBergmann\Money\Money::add ) ... ) ) ) ) )三层对象模型对应三个类src/Diff.php一个文件级 diff字段为from/to通常是a/...、b/...路径与chunks数组实现了IteratorAggregate可直接迭代得到其中的Chunk。src/Chunk.php一个 hunk字段为start/startRange原文件起始行号与行数与end/endRange新文件对应值lines为该 hunk 的行列表同样实现IteratorAggregate迭代产出Line。src/Line.php一行差异内容type取值Line::UNCHANGED 3、Line::ADDED 1、Line::REMOVED 2并提供isAdded()/isRemoved()/isUnchanged()便捷判断方法content为该行原始文本。上例中type为 3 的行即未变更的上下文行为 2 的testAnotherMoneyWithSameCurrencyObjectCanBeAdded()是删除行为 1 的testAnotherMoneyObjectWithSameCurrencyCanBeAdded()是新增行——与示例源码中方法名的替换关系一一对应。六、关于 Chunk 行号的边界语义原文档末尾专门提示了一个容易踩坑的边界规则如果某个 hunk 的行数为 0即getStartRange()或getEndRange()返回 0则getStart()或getEnd()返回的行号会比直觉少 1——此时它表示该 chunk 应插入或删除的位置之后的那一行号在其他情况下它给出被替换行区间的第一个行号。换言之对于纯插入或纯删除range 为 0的 hunk起始行号语义是锚点行而不是变更内容的第一行。在基于行号做二次加工如定位变更点、生成映射表时需要特别注意这一偏移。七、在 rector 中的真实应用变更差异报告rector 在源码中对该组件做了一层薄封装src/Differ/DefaultDiffer.php 在构造函数中创建StrictUnifiedDiffOutputBuilder并配置fromFile Original、toFile New随后注入Differfinal class DefaultDiffer { public function __construct() { $strictUnifiedDiffOutputBuilder new StrictUnifiedDiffOutputBuilder( [fromFile Original, toFile New] ); $this-differ new Differ($strictUnifiedDiffOutputBuilder); } public function diff(string $old, string $new): string { return $this-differ-diff($old, $new); } }即 rector 的终端差异输出采用严格 unified diff 格式带 -... ... hunk 头头部为--- Original/ New。该封装的实际消费方是 src/ChangesReporting/ValueObjectFactory/FileDiffFactory.php当--diff选项开启时createFileDiffWithLineChanges()用DefaultDiffer-diff($oldContent, $newContent)计算每个文件改动前后的差异再交给ColorConsoleDiffFormatter上色最终随FileDiff含相对路径、纯文本 diff、终端彩色 diff 与行级变更信息进入变更报告输出。这正是你在终端运行 rector 后看到文件被修改、并展示逐行 diff效果的核心链路。八、版本与兼容性提示当前仓库 vendor 目录下的 sebastian/diff 为 9.0-dev 系列见 vendor/sebastian/diff/composer.json 中branch-alias的9.0-dev要求 PHP 8.49.0 起UnifiedDiffOutputBuilder与AbstractChunkOutputBuilder已被移除请迁移至StrictUnifiedDiffOutputBuilder从 src/Differ.php、src/MyersDiff.php 等文件的命名空间可见rector 通过前缀化RectorPrefix202609隔离 vendor 依赖因此在使用该组件时务必通过 rector 的封装DefaultDiffer或遵循其前缀命名空间避免与项目中同名的原生SebastianBergmann\Diff类冲突。结合 vendor/sebastian/diff/README.md 与上述源码你可以快速在任意 PHP 8.4 项目中落地字符串差异计算Differ 输出构建器与 unified diff 解析Parser并复用 rector 已验证的严格输出配置构建自己的代码比对、变更报告或测试断言工具。【免费下载链接】rectorInstant Upgrades and Automated Refactoring of any PHP 5.3 code项目地址: https://gitcode.com/GitHub_Trending/re/rector创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考