mcpField
C
code-migration

code-migration

github.com/kucherenko开发6.5k103 次安装今天更新
代码仓库

关于 code-migration

code-migration 技能借助 jscpd 比较源目录与目标目录中的函数,用于衡量跨语言或跨平台移植的覆盖情况。它同时比较测试和代码,并结合覆盖率规划先移植测试、再移植实现。

主要功能

  1. 01配对不同目录中的对应函数
  2. 02分别比较测试与实现代码
  3. 03报告未匹配函数及相似度
  4. 04利用覆盖率关联测试与函数
  5. 05支持控制台、JSON、Markdown 和 HTML 报告

适用场景

  1. 01跟踪跨语言移植的完成情况
  2. 02找出目标实现中尚未移植的函数
  3. 03根据源代码覆盖率安排测试移植
  4. 04生成迁移进度报告供团队查看

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/kucherenko/jscpd --skill code-migration

安装后在 Claude Code 中输入/code-migration确认已加载。

详细介绍

代码迁移

jscpd --compare SOURCE TARGET 会将一个文件夹中的每个函数与另一个文件夹中执行相同任务的函数配对,不限语言组合,并列出没有对应项的函数。此技能用它来衡量移植进度:哪些内容已移植、哪些尚未完成,以及刚编写的移植代码是否被识别。

移植分两个阶段进行:先移植测试,再移植测试所验证的代码。源代码的测试覆盖率报告会指出哪些测试覆盖了哪些函数,因此每个函数都与证明其正常工作的测试一同移植。请参阅计划:先移植测试,再移植代码。

全文使用两个术语:

  • source:移植所依据的实现,例如 iOS 应用、Python 库或 JavaScript 包;
  • target:移植到的实现,例如 Android 应用、Rust crate 或 TypeScript 重写版本。

两者都不必是旧版或新版。source 可以继续用于生产环境并持续变化,而 target 也可能已经具备 source 所没有的功能。如果两个实现都在持续使用(ios/ 和 android/),同一份报告也能展示它们的功能对等情况;请参阅对两个实现进行对等性比较。

jscpd 可识别 JavaScript、TypeScript、JSX、TSX、Vue、Svelte、Astro、Python、Rust、Go、Java、Kotlin、C#、C、C++、PHP、Ruby、Scala 和 Swift 中的函数。compare-codebases 技能会说明如何配对函数以及如何检查比较结果;jscpd 技能涵盖该工具的其他内容。

设置

--compare 使用在 jscpd 内运行的代码嵌入模型来配对函数。请下载一次(548 MB,CodeRankEmbed)。开始下载前,请先征得用户同意:

npx jscpd --semantic-download

jscpd 会缓存向量,因此再次运行时只会嵌入代码有变化的函数,只需几秒钟。

选定两个路径,并在整个移植过程中保持不变:先写 source,再写 target。必须提供两个路径,且两者不能重叠(app/ 和 app/android/ 会被拒绝)。开始时 target 可以为空。

一次运行会衡量两个阶段:报告包含 Code 块和 Tests 块(JSON 中的 code 和 tests 部分),测试只会与测试配对。jscpd 会根据不同语言的惯例识别测试:例如 *_test.go、test_*.py、*.test.ts 或 *Test.java 等测试文件,tests/、__tests__/ 或 src/test/ 等文件夹,Rust #[cfg(test)] 模块中的测试,以及 it('rounds cents', () => …) 等 JavaScript 测试用例。第 1 阶段读取 Tests 块,第 2 阶段读取 Code 块。

不要纳入供应商代码和生成代码

--compare 会统计并嵌入两条路径中的每个函数。供应商依赖(cargo vendor 或 go mod vendor 生成的 vendor/、third_party/)、已安装的软件包(node_modules/、.venv/)、构建输出(target/、build/、dist/)和生成代码都不属于移植内容,但仅一个供应商 crate 目录树就包含数千个函数。如果路径中包含这些内容,首次运行会嵌入所有函数,耗时数十分钟而不是几秒钟,而且百分比反映的是依赖,而不是移植进度。

jscpd 会跳过 .gitignore 排除的内容,但仅限于 Git 仓库内。首次运行前:

  1. 检查两条路径中是否有这些文件夹。为移植创建的目标目录一旦构建或供应商化其依赖,就会出现这些文件夹,因此首次构建后要再次检查。

  2. 向用户建议一个 .gitignore,列出目标语言和工具生成的文件夹,以及报告文件夹。对于使用 napi-rs 构建的 Rust 插件:

    /target/
    /vendor/
    node_modules/
    *.node
    .jscpd-compare/
    

    如果目标目录不在 Git 仓库中,请告知用户,jscpd 只有在运行 git init 后才会读取 .gitignore。

  3. 在 .gitignore 生效之前,使用 --ignore 指定相同的文件夹,并确保每次运行使用相同的 glob:

    npx jscpd --compare node-lib/ rust-lib/ --ignore "**/vendor/**,**/target/**,**/node_modules/**"
    

总计行会显示是否有内容漏过。若目标中的函数远多于源代码,或者小幅改动后某次运行嵌入了数百个函数,就要怀疑路径中含有供应商代码或生成代码。

衡量

为自己和用户运行控制台报告。下面以一个移植到 TypeScript 的 Python 计费库为例:

npx jscpd --compare billing-py/ billing-ts/
 71% 5 of 7 functions in billing-py/ have a counterpart in billing-ts/
 80% 4 of 5 functions in billing-ts/ have a counterpart in billing-py/

billing-py/
  file         paired  similarity  counterpart
  billing.py   4 / 5   0.89        billing.ts
  shipping.py  1 / 2   0.91        shipping.ts

billing-ts/
  file         paired  similarity  counterpart
  billing.ts   3 / 4   0.89        billing.py
  shipping.ts  1 / 1   0.91        shipping.py

Paired under other names (1):
  billing-py/                   billing-ts/             similarity
  billing.py:28 tax_for_region  billing.ts:27 salesTax  0.87 high

Only in billing-py/ (2):
  billing.py (1)
    46  due_date                6 lines
  shipping.py (1)
    18  estimate_delivery_days  8 lines

Only in billing-ts/ (1):
  billing.ts (1)
    39  toCurrency  8 lines
  • 第一行显示移植进度:源代码中在目标代码里有对应函数的占比。源代码中标为 “Only in” 的部分就是尚待完成的工作。
  • 第二行和目标中的 “Only in” 描述目标代码本身:移植所需的辅助函数、源代码中没有的功能,或 jscpd 未识别出的移植代码(参见 未识别项)。
  • 每个文件行列出配对的函数数量、配对函数的平均相似度(如果有 low 配对,还会列出其数量),以及对应文件:另一侧包含其大部分对应函数的文件。根据对应文件判断缺失函数应放在哪里。
  • “Paired under other names” 列出名称存在差异的配对,即使忽略大小写和下划线后仍然不同:重命名的移植代码、构造函数、平台名称。每一对都会显示其相似度和等级(参见 查看配对)。这些代码已完成移植,只是按名称搜索时找不到。
  • 目标目录为空时,报告只会显示一行总计,以及 billing-ts/ has no functions yet。

如果要规划和跟踪工作,请查看 JSON 报告,而不是控制台输出:

npx jscpd --compare billing-py/ billing-ts/ -r json -o .jscpd-compare --silent

jscpd 会分别衡量测试和代码,且只会将测试与测试配对,因此 .jscpd-compare/jscpd-compare.json 中有结构相同的 code 和 tests 两个部分。每个部分都有 sides[0](源代码)和 sides[1](目标代码),每一侧包含 path、functions、matched、percentage、files(file、functions、matched、counterpart、similarity、lowPairs)、unmatched(file、name、start、end)和 readyToPort(未匹配函数中所有被调用函数均有对应项的函数,包含其 callers 数量,按被调用次数从高到低排列)。此外还有 pairs,其中每对包含 a(源代码)、b(目标代码)、similarity、level(high、medium、low)、renamed(名称是否不同)和 matchedBy(code 或 name)。路径均相对于各自一侧的文件夹。将 .jscpd-compare/ 加入 .gitignore,或将报告写到仓库之外。

-r console-full 还会打印每一对及其相似度;-r markdown 会生成 jscpd-compare.md,其中的表格可粘贴到 PR 描述或跟踪问题中;-r html 会生成 jscpd-compare.html,这是一张迁移图,用户可以打开查看并排显示的两个依赖关系图,也可以查看相同配对的表格。

计划:先写测试,再写代码

先移植测试,再移植代码。在任何目标代码尚不存在之前,移植后的测试就定义了每个函数移植完成的标准:函数加入后,其测试要么通过,要么指出仍有哪些差异。如果先写移植代码再写测试,就只能根据你对源代码的理解来检查。

通过覆盖率将测试与函数关联

测试名称并不能说明它运行了哪些函数:对 checkout 的测试也会运行 apply_discount 和 tax_for_region。覆盖率则可以。在移植任何内容之前,先建立一份映射,列出每个源函数由哪些测试执行。

  1. 获取源测试的覆盖率报告,说明每个测试运行了哪些行:按单个测试记录,或者在项目工具不支持按单个测试记录时按测试文件记录。
  2. 从 JSON 报告的 code 部分获取源函数及其行号。每个被计数的函数要么位于源的 unmatched 列表中,要么位于某个配对的 a 侧;每项都包含 file、name、start 和 end。低于 --min-tokens 或 --min-lines 阈值的函数不会列在那里;请从覆盖率报告自身的函数列表中获取这些函数,大多数报告格式都有此列表。
  3. 如果测试运行了函数 start 到 end 之间的一行,就算该测试覆盖了该函数。将映射按两个方向写入报告旁边的文件(例如 .jscpd-compare/test-map.json):函数到测试、测试到函数;源测试发生变化时,重新生成映射。

将此映射用于以下三件事:

  • 确定哪些测试要随函数一起移植,以及移植后的测试需要哪些函数才能通过;
  • 找出没有测试覆盖的源函数。移植这样的函数之前,先在源代码侧为其编写测试,记录它当前的行为(特征测试),并与其他测试一起移植。没有测试的移植代码无从验证;
  • 确定工作顺序:依赖函数较少的测试优先,因为它们能最快通过。

覆盖率说明的是测试运行了哪些代码,而不是测试检查了什么。测试只是为了到达另一个函数而经过某个函数时,该函数与测试的关联较弱;优先选择名称或断言针对该函数的测试。

阶段 1:移植测试
  1. 从 JSON 报告的 tests 部分获取源代码中未匹配的测试。以回调形式编写的 JavaScript 或 TypeScript 测试用例,例如 it('rounds cents', () => …),按其标题对应;因此,它可以与 pytest 中的 test_rounds_cents 或 Rust 中的 rounds_cents 这样的命名测试配对。
  2. 按文件移植测试,放入目标项目的测试目录结构中,并针对目标将提供的 API 编写。输入和预期值必须保持原样。如果目标必须表现不同(例如平台限制、语言的数字类型),请在测试中添加注释说明差异,并告知用户。
  3. 移植后的测试会调用尚不存在的函数,因此会失败。在编译型语言中,这会导致整个测试构建中止。在函数加入之前,不要让这类测试参与构建;例如,可以将模块声明注释掉、使用 cfg 功能或排除对应源文件集,并在阶段 2 中逐一启用。不要为了让测试通过编译而编写空函数:以源代码函数名命名的桩函数可能会按名称配对,并被计为已移植。
  4. 运行目标项目的测试。已通过的移植测试表明目标已有对应功能;失败或已禁用的测试结合测试映射,就是阶段 2 的待办清单。
第 2 阶段:移植代码

一次移植一个函数,并确保其测试已就位。

  1. 运行 JSON 报告,并从报告 code 部分中获取源代码的 unmatched 列表。
  2. 安排工作顺序。从测试已移植的函数开始;先移植被调用的函数,再移植调用它们的函数,因为调用方先于辅助函数移植时,便无函数可调用。源代码的 readyToPort 列表就是这个顺序的前列:其中的函数都已具备对应的被调用函数,且按调用次数从多到少排列。取其中下一个函数开始移植,并在每批移植后重新运行报告,因为每次移植都可能让其调用方满足移植条件。在这个顺序中,先完成一个文件,再开始下一个文件,这样每个目标文件都能一次填满。
  3. 编写移植代码前,先确认目标中还没有该函数。标有 renamed: true 的配对项是改了名称的已移植函数;它们不在 unmatched 中,但用户询问函数去向时,请检查这些项。然后在目标代码中搜索 jscpd 漏掉的等效实现:查看 files 中对应的文件,以及其他名称下的实现(构造函数、合并后的函数,或替代该辅助函数的平台或库调用)。如果已经存在等效实现,就不要重复移植。记下这个等效实现,然后继续。
  4. 将移植代码写入对应文件,或写入目标布局指定的文件。函数名应符合目标语言的命名惯例,同时保持可辨认性(encodeBinary 在 Rust 或 Python 中会变成 encode_binary),因为 jscpd 会按名称匹配较短的函数。遵循同一文件中已完成配对的风格。console-full 会列出这些配对。
  5. 按测试映射启用该函数已移植的测试,并运行它们。如果测试失败,应修复移植代码,而不是测试,除非用户同意目标的行为有所不同。报告中的配对表示两个函数看起来相似;测试则用于确认它们的行为是否相同。
  6. 运行这些测试对应的目标代码覆盖率,并确认它们确实运行了新函数。如果已移植的测试没有执行到该函数,就说明它在目标中绑定到了其他内容(辅助函数、模拟对象或旧路径),因此无法证明移植代码正确。
  7. 使用相同路径和选项再次运行比较。确认该函数已从 unmatched 中移除,其配对项与目标中的正确函数匹配,并查看匹配等级。high 配对表示已完成。移植代码的 low 配对通常意味着它与源代码在结构上差异较大;请检查两者并确认行为一致。配对到了其他函数,则说明移植代码未被识别,或与错误的函数相似;继续之前请检查两者。
  8. 向用户汇报进度,并同时提供报告中显示的两项指标:已移植且通过的测试,以及已移植的函数(functions 73% → 76%, 3 ported: …; tests 41 of 44 ported, 38 passing),并说明你决定不移植的函数及原因。

重复以上步骤,直到源代码的 unmatched 列表中只剩下你决定不移植的函数。常见原因包括:死代码(在支持该语言的源代码上运行 npx jscpd --dead-code 检查)、目标平台或其库已提供的功能(JSON 解析器、重试辅助函数)、目标平台上没有对应实现的平台衔接代码(iOS 委托回调、Android 通知渠道),以及用户决定不移植的功能。将这些内容逐条记录在说明文件或 PR 中,以解释剩余比例。

如果移植过程中源代码持续变化,已经配对的函数可能会在重写后重新出现在 unmatched 中,也可能会有新的源代码函数加入列表。每批工作开始前都要重新运行报告,不要沿用旧列表。

两种实现之间的对等性

当两种实现都存在时(例如 iOS 和 Android 应用),运行 npx jscpd --compare ios/ android/ 可以查看对等性。阅读两个“Only in”列表:

  • 仅存在于一侧的函数,要么是平台专属代码(Android 通知渠道、iOS 委托回调),要么是另一侧缺少的功能。实现之前,先向用户报告每项缺失功能;
  • 要添加缺失功能时,将已有该功能的一侧作为该功能的源:为测试补充覆盖率,移植测试,再移植代码,参见计划:先测试,再编写代码。

jscpd 会在已经匹配的模块内配对函数。模块是指两侧文件共同拥有的最深层文件夹下一级的文件夹,因此应让两侧目录树保持平行(ios/<plugin> 和 android/<plugin>),以获得最佳配对效果。

阅读配对结果

  • level 按所用模型的量表评估相似度,因此在不同模型下含义一致。使用默认 CodeRankEmbed 时,跨语言的 high(0.7125 及以上)几乎总是同一个函数。medium 通常表示经过重构的同一函数。low 需要阅读两个函数,因为相关代码也会配对(例如,一个统计 UTF-8 字节数的函数与一个将字符串转换为这些字节的函数配对)。在 Tauri 插件中,每个 low 配对都连接了名称不同的函数,其中一组还连接了两个不同的插件。
  • matchedBy: code 表示这两个函数彼此是最接近的匹配项,且相似度较为突出。
  • matchedBy: name 表示忽略大小写和下划线后,两个函数名称相同,且代码足够相似。这会识别较短的移植代码。请阅读这些配对,因为同名函数也可能执行不同的操作。
  • 相似度检测不到细微的行为差异。两个版本即使已经出现偏差,仍可能配对。只有测试和代码审查才能发现偏差。

未识别项

jscpd 只比较函数:类型、常量、含数据的枚举、SQL 和 UI 标记(storyboard、Android 布局)不会出现在报告中,因此需要自行移植并检查。已知的配对盲点:

  • 跨语言的构造函数(Java 构造函数与 Rust 的 new、Kotlin 的 constructor、Swift 的 init)只有在代码足够相似时才会配对;
  • 移植时重命名的短函数(例如,将 add_history 重命名为 _finder_penalty_add_history);
  • 一个函数被拆分为多个函数,或多个函数被合并为一个函数:报告可能只配对最接近的部分,并将其余部分列为未匹配项;
  • 匿名函数(回调、闭包)完全不参与配对,JavaScript 和 TypeScript 测试用例除外,例如 it('rounds cents', () => …),这类用例按其标题配对。

如果某个函数仅列在目标侧,而你知道它是从源函数移植而来,那它就属于上述情况之一。不要仅为了提高数字而重命名可正常工作的代码,也绝不要添加使用源名称的桩代码或空函数:jscpd 可能会按名称配对桩代码,进度报告就会把尚未完成的工作记为已完成。

选项

  • --min-tokens(使用 --compare 时为 30)和 --min-lines(5)决定哪些函数会计入总数。更短的函数仍会作为配对对象进行匹配。提高这些值可聚焦于重要函数;并在每次运行时保持这些值不变,以便比较百分比。
  • --ignore 可排除文件,例如供应商依赖、构建输出、生成代码或测试夹具(参见排除供应商代码和生成代码);--pattern 可将比较范围缩小到部分文件,例如某个模块的测试。每次运行时保持这些 glob 模式不变。
  • --format 可将遍历范围缩小到某些语言,例如当源代码中混有待移植代码和构建脚本时。
  • --semantic-model 和 --semantic-url 可选择其他嵌入模型或兼容 OpenAI 的 API(npx jscpd --semantic-models 会列出经过阈值校准的模型)。每次运行时保持模型不变,因为每个模型都有自己的评分量表。
  • 无论进度如何,退出代码都是 0,因此该命令本身不会导致构建失败。

规则

  • 每次运行都要保持这两个路径、它们的顺序、选项和模型不变,否则数字就无法比较。
  • 不要将供应商依赖项、已安装的软件包和构建输出纳入任一路径:在 git 仓库中使用 .gitignore 排除,并建议用户这样做;或者使用 --ignore。
  • 先移植测试,再移植代码;函数只能与覆盖率映射表关联到它的测试一同移植。
  • 将报告视为阅读指引和待办事项清单。它不能证明移植正确,测试才能。
  • 绝不要钻百分比的空子:不要使用存根,不要只为了 jscpd 而重命名,也不要在未经用户决定的情况下删除源函数来缩小分母。
  • 如果报告与代码阅读结果不一致,以代码为准,并告知用户哪一对看起来有误。

评论

支持 Markdown