dry-refactoring について
dry-refactoringは、jscpdが検出したソースコードの重複を解消するためのガイド付きワークフローです。重複の分類、リファクタリング候補の評価、抽出方法の選択、再スキャンによる確認を支援します。
主な機能
- 01jscpdの実行と重複レポートの解釈
- 02完全一致と類似重複の優先順位付け
- 03変更前の候補選別
- 04抽出とパラメーター化の方針
- 05再スキャンによる変更確認
活用シーン
- 01jscpdで見つけたコピーコードの整理
- 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>
デフォルトのスキャンで報告されるのは完全一致のコピーだけで、まずリファクタリングするべきなのはこれらです。完全一致のクローンは、ほぼ間違いなく実際のコピー&ペーストです。あと2回のスキャンで、貼り付け後に編集されたコピーを見つけます。これらはノイズが多くなります。意図的に名前、値、または一部の文を無視するため、見た目が似ているだけのブロック(モデルや DTO、設定テーブル、テストのセットアップ、生成コード、共通の慣用表現)も検出します。完全一致のクローンに対処してから、設定を厳しくして、一度に1種類ずつ実行してください。報告されたものは修正すべき欠陥ではなく、内容を確認するための手がかりとして扱ってください。
# 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%が高く、トークン数も多いファイルほど効果が大きくなります) - 各クローンの行を解析して、重複している2つの場所(ファイル+行範囲)と種類を特定します。接尾辞がない場合は完全一致のコピー、
(renamed)は名前または値だけが異なるもの、[~N gap]は途中に数行の編集があるもの、[~N ast]は同じ構造を持つ関数のペアです - ソースファイルから両方のコード断片を読みます
- 重複コードの処理内容を把握し、名前が変更されたクローンや類似クローンについては、両者の違いを正確に列挙します
- 名前が変更されたクローンや類似クローンは、手を加える前にトリアージします。 次のいずれかに該当する場合は、そのペアをスキップし、その旨を伝えてください。同じ形でも両者の処理内容が異なる(異なる列挙型を使う
switch、意味が関係のない2つのリデューサーなど)。共通している部分が意図的な定型コードである(モデル、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
リテラルだけが異なる (renamed) クローンは、関数の不足ではなく、定数または設定項目の不足です。
わずかに異なるコピーを統合 — [~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
差分によって処理の意味が変わる場合は、関数を2つのままにして、共通部分を抽出してください。
類似関数を統合 — [~N ast] クローンの場合。構造は一致していますが、名前やリテラル、一部の文が異なります。まず2つの関数を差分比較してください。ast スコアは、どれだけ共通しているかを示します(0.9 は1箇所だけ編集されたコピー、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 を下回る場合、通常、2つの関数に共通するのはイディオムであり、実装ではありません。サマリーからファイルがホットスポットだと分かる場合を除き、そのままにしてください。
必ず以下を確認してください:
- jscpd が報告した2箇所だけでなく、すべての呼び出し箇所を更新する
- リファクタリング後もテストが合格する
- 抽出した抽象化に、明確で説明的な名前を付ける
- 再実行時に、クローンを検出した実行と同じ検出フラグを使用する
ヒント
- 行数が最も多いクローンから着手してください。影響が最も大きくなります
- サマリーの
dup%列を使って作業の順序を決めてください。重複行の割合が高い大きなファイルほど、先に対処する価値があります - テストファイル間のクローンは、テストヘルパーが不足していることを示している場合があります
- 関連のないモジュール間のクローンは、共有ユーティリティが不足していることを示している場合があります
- フォーマットをまたぐクローン(
.jsファイルと.tsファイルで同じロジックが見つかる場合。--cross-formatsで検出)は、コードを移植した後に元のコードを削除し忘れたことを示している場合がよくあります。3つ目の共有コピーを抽出するのではなく、1つの実装(通常は TypeScript の実装)に統合して、インポートを更新してください - 1つのファイルに
(renamed)クローンが多数ある場合、多数の抽象化ではなく、1つの抽象化が不足している可能性が高いです。ペアごとに抽出する前に、共通の構造を探してください。テストファイル間に(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を保持してください。名前変更と類似コードの実行では、完全一致の実行とは異なる方法でクローンのフィンガープリントが作成されます