关于 dry-refactoring
dry-refactoring 是一套针对 jscpd 检出的源代码重复内容的引导式重构流程。它帮助开发者分类重复片段、评估重构价值、选择抽取策略,并通过再次扫描验证修改。
主要功能
- 01运行 jscpd 并解读重复报告
- 02优先处理精确及近似重复
- 03重构前筛选候选项
- 04选择抽取与参数化策略
- 05重新扫描验证修改
适用场景
- 01重构 jscpd 发现的复制粘贴代码
- 02审查改名或近似重复候选项
- 03检查修改后的重复度变化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kucherenko/ jscpd --skill dry-refactoring安装后在 Claude Code 中输入/dry-refactoring确认已加载。
详细介绍
dry-refactoring
用于消除源代码中复制粘贴重复内容的引导式工作流。检测克隆后,请运行 jscpd,再使用本工作流。
先决条件
首先,运行 jscpd 来识别重复内容:
npx jscpd --reporters ai <path>
在混合使用相关格式(例如 JavaScript 和 TypeScript)的代码库中,添加 --cross-formats,以便检测跨两种格式的克隆:
npx jscpd --reporters ai --cross-formats "js-ts" <path>
对于较大的代码库,添加 --summary,在克隆列表旁查看重构热点概览——其中会列出文件和文件夹,并通过 dup% 列显示各文件中的重复内容占比:
npx jscpd --reporters ai --summary <path>
默认扫描只报告完全相同的副本,应优先重构这类内容:完全相同的克隆几乎总是实际的复制粘贴内容。再进行两轮扫描,可以找到粘贴后经过编辑的副本。这两轮扫描的噪声更多:它们会有意忽略名称、值或少量语句,因此也会找出只是看起来相似的代码块(模型和 DTO、配置表、测试设置、生成的代码、共享惯用写法)。只有在处理完完全相同的克隆后,才逐类使用严格设置运行这些扫描,并将报告的结果视为需要查看的线索,而非必须修复的问题:
# Type-2: renamed copies (other variable names, other constants), reported as "(renamed)".
# Raise --min-tokens: with identifiers ignored, a short block is mostly placeholders.
npx jscpd --reporters ai --ignore-identifiers --min-tokens 70 <path>
# Type-3: near-miss copies (one or two edited lines, or JS/TS/Python functions with the same structure),
# reported as "[~0.91 gap]" and "[~0.85 ast]". Widen only if the tight run finds nothing.
npx jscpd --reporters ai --max-gap-lines 1 --similarity <path>
有关完整的选项参考,包括跨格式分组语法、克隆类型后缀以及如何阅读摘要,请参阅 jscpd 技能。
工作流
- 在目标路径上使用
--reporters ai运行 jscpd(对于较大的代码库,添加--summary以确定起点:dup%高且令牌数量大的文件最值得优先处理) - 解析每条克隆记录,识别两个重复位置(文件 + 行范围)及其类型:没有后缀表示完全相同的副本,
(renamed)表示仅名称或值不同,[~N gap]表示中间有少量经过编辑的行,[~N ast]表示结构相同的一对函数 - 从源文件中读取两个代码片段
- 理解重复代码的作用;对于重命名克隆和相似克隆,准确列出两边的差异
- 处理前先筛选重命名克隆和相似克隆。 如果符合以下任一情况,请跳过这对代码并说明原因:两边虽然形状相同,但执行的操作不同(例如针对不同枚举的
switch,或语义无关的两个 reducer);相似之处是有意保留的样板代码(模型、DTO、配置、路由表、测试固件);代码由工具生成;共享抽象需要使用类似processData这样含义模糊的名称;或代码对少于约 10 行。只有能够删除代码并为提取出的内容起一个准确名称的代码对,才进入下一步 - 设计重构方案:提取共享函数、类、模块或常量;具体策略取决于类型(见下文)
- 应用重构——更新两个位置以及所有其他用法
- 使用相同的标志重新运行 jscpd,确认克隆已消除,且修改过的文件的
dup%降低;(renamed)克隆不会出现在默认运行结果中,因此要再次使用--ignore-identifiers检查 - 按影响从大到小处理剩余克隆:先处理完全相同的克隆,再处理重命名克隆,最后处理相似克隆。单独报告跳过的候选项及其原因,避免有人误将归一化运行的计数当成实际重复内容
重构策略
提取函数——重复的是一段逻辑时:
// Before: same block in two places
// After: shared function called from both places
提取模块/工具——重复内容跨越不同领域的多个文件时:
// Move shared logic to a shared utility file and import it
提取常量或配置——重复的是数据或配置时。
模板/基类——重复的是结构时(例如,重复的类结构)。
参数化——适用于 (renamed) 克隆。这两段代码是在不同名称或值上运行的同一算法,因此应将差异部分变为参数:
// Before: computeCartTotal(items) and computeBasketTotal(entries), same body, other names;
// limits-dev.js and limits-prod.js, same shape, other numbers
// After: one function whose parameters are the identifiers that differed,
// or one function reading the values that differed from a config object
如果重命名克隆唯一的差异是字面量,那么缺少的是常量或配置项,而不是函数。
统一近似副本——适用于 [~N gap] 克隆。阅读未匹配的行:差异通常只在一处,例如一个守卫条件、一条日志调用或一个额外字段。提取共有部分,并将差异部分作为参数传入:
// Before: saveUser and saveAccount, identical except one inserted validation line
// After: one saveRecord(record, { validate }) with the inserted line behind the option,
// or the inserted line moved to the caller before the shared call
如果差异改变了含义,而非只是增加一个步骤,则保留两个函数,但提取它们共有的部分。
合并相似函数——适用于 [~N ast] 克隆。结构相同,但名称、字面量和部分语句不同。先比较这两个函数;ast 分数表示它们有多少内容相同(0.9 表示只有一处改动的副本,0.75 表示有几处新增语句并修改了名称的副本)。提取共有框架,并通过参数、策略对象或回调注入差异部分:
// Before: buildInvoice(order, customer, taxRate) and buildCreditNote(refund, account, vatRate):
// same loop, same rounding, one extra guard and one extra log call in the second
// After: buildDocument(source, party, rate, { filter, onBuilt }) used by both
低于约 0.8 时,这些代码通常只是采用了相同惯用写法,而非共享实现;除非摘要显示这些文件本身也是热点,否则不要处理。
务必确保:
- 更新所有调用点,而不只是 jscpd 报告的两个调用点
- 重构后测试仍然通过
- 提取出的抽象具有清晰、明确的名称
- 重新运行时使用与发现克隆的那次运行相同的检测标志
提示
- 优先处理行数最多的克隆——它们影响最大
- 使用摘要中的
dup%列安排处理顺序:重复行占比高的大文件能带来最大的收益 - 测试文件之间的克隆可能表明缺少测试辅助工具
- 不相关模块之间的克隆可能意味着缺少共享工具
- 跨格式克隆(同一逻辑分别出现在
.js和.ts文件中,并通过--cross-formats找到)通常意味着代码移植后未删除原始实现——应整合为一个实现(通常保留 TypeScript 版本)并更新导入,而不是再提取第三份共享副本 - 同一文件中有很多
(renamed)克隆通常意味着缺少一个抽象,而非缺少多个抽象:提取前先寻找共有结构。多个 测试 文件中有很多(renamed)克隆通常无需处理:测试用例本来就应当相似 - 在团队审查这些检测结果是否适用于当前代码库之前,不要使用 Type-2/Type-3 检测设置 CI 门禁(
--threshold、--fail-on-new-clones);应对完全相同的检测运行设置门禁 --similarity支持 JavaScript、TypeScript、Python、Java、Kotlin、Scala、C#、Go、Rust、C、C++、PHP、Ruby、Swift 和 Clojure,也支持 Markdown 代码块以及 Vue、Svelte 和 Astro 脚本中的代码;对于其他语言,应依赖完全匹配和--max-gap-lines检测- 使用
--min-lines 10过滤噪声,聚焦有意义的重复代码 - 设置 CI 门禁时,应为每组检测标志分别使用一个
--baseline:重命名和相似度检测与完全匹配检测采用不同的克隆指纹