dry-consolidation について
重複コードを検出し、テスト済みの共有抽象へ切り出す手順を示す開発スキルです。複数ファイルにまたがるユーティリティ、コンポーネント、フックの整理に向いています。クローン検出と構造検索を使います。
主な機能
- 01jscpdでコードクローンを検出
- 02ast-grepで構造の類似性を確認
- 03ユーティリティ、コンポーネント、フックに分類
- 04抽象のインターフェース、配置、利用箇所を計画
- 05テストを追加してプロジェクトの検証を実行
活用シーン
- 01複数ファイルの重複ユーティリティを抽出
- 02重複するUI断片や状態処理を統合
- 03編集前にdry-runで重複を確認
- 04指定範囲に絞って重複を整理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ laurigates/ claude-plugins --skill dry-consolidationインストール後、Claude Code で/dry-consolidationを入力して読み込まれたか確認します。
同梱ファイルscripts/tests/test-dry-consolidation.sh
詳細
DRY の統合
重複したコードを、共有されテスト済みの抽象化に体系的に切り出します。
このスキルを使用する場面
| 次の場合にこのスキルを使用します… | 次の場合はこちらを使用します… |
|---|---|
| 複数のファイルに同一またはほぼ同一のコードブロックがある | 1つのファイルを整理する場合 → /code:refactor |
| コンポーネント間でユーティリティ関数がコピー&ペーストされている | 修正せずにアンチパターンを探す場合 → /code:antipatterns |
| 繰り返し使われる UI パターン(ダイアログ、ページネーション、エラー状態)がある | ファイルまたはディレクトリの機能的なリファクタリングを行う場合 → /code:refactor |
| フックや状態管理の定型コードが重複している | 構造的なコード検索だけを行う場合 → ast-grep-search |
| 変数名を変更しただけの、ほぼ重複するコピー&ペーストを列挙する必要がある(jscpd はここでクラスターを検出します) | 既知の構造パターンを1つ検索する場合 → ast-grep-search |
| インラインパターンの繰り返しによりインポートブロックが肥大化している | リントやフォーマットの問題に対処する場合 → /lint:check |
コンテキスト
- 対象パス:!
echo "$1" - プロジェクトの種類:!
find . -maxdepth 1 \( -name "package.json" -o -name "Cargo.toml" -o -name "pyproject.toml" -o -name "go.mod" \) - ソースディレクトリ:!
find . -maxdepth 1 -type d \( -name "src" -o -name "lib" -o -name "app" -o -name "components" -o -name "packages" \) - テストフレームワーク:!
find . -maxdepth 2 \( -name "vitest.config.*" -o -name "jest.config.*" -o -name "pytest.ini" -o -name "conftest.py" \) - 既存の共有ユーティリティ:!
find . \( -path "*/lib/*" -o -path "*/utils/*" -o -path "*/shared/*" -o -path "*/common/*" -o -path "*/hooks/*" \) -type f -print -quit
パラメーター
$1:スキャンするパスまたはディレクトリ(デフォルトはsrc/)--scope:特定の切り出しタイプに絞り込みます。utilities、components、hooks、allのいずれかを指定します(デフォルト:all)--dry-run:変更を加えずに重複を分析して報告します
実行
この7段階の統合ワークフローを実行します。セッションでタスクツールを使用できる場合は、TodoWrite を使って切り出しごとに別々のタスクとして管理します(.claude/rules/agentic-permissions.md § タスクツールの利用可否を参照)。使用できない場合は、回答内のチェックリストで管理します。
ステップ1:重複クラスターを検出する(決定論的なクローン検出)
決定論的なクローン検出器で重複範囲を列挙し、その後、報告された範囲のみを読み取ります。候補ファイル全体は読み取りません。これにより、検出の再現性が保たれ、コストも抑えられます。トークンベースの検出(jscpd)は、空白やフォーマット、囲んでいるシンボル名に左右されず、コピー&ペーストを検出します。ラッパー関数の名前が変更されると、名前ベースの Grep では見つからないクローンのペアも検出できます。次に ast-grep(1b)で、ブロック内の変数名の変更も許容して検出します。
1a. jscpd によるトークンベースの類似コード検出
jscpd は、名前に「js」が含まれていますが、150以上の言語に対応するトークンベースのコピー&ペースト検出器です。npx を使えば、グローバルインストールなしで実行できます。対象パスに対して実行します。
npx jscpd --reporters json --min-tokens 50 --output /tmp/jscpd-dry --silent <path>
/tmp/jscpd-dry/jscpd-report.json にレポートが出力されます。このレポートを読み取り、duplicates 配列を解析します。各エントリには、クローンのペアの正確なファイル範囲と行範囲、およびトークン数と行数が記載されています。
{
"duplicates": [
{
"format": "tsx",
"lines": 12,
"tokens": 84,
"firstFile": { "name": "src/UserList.tsx", "start": 20, "end": 32 },
"secondFile": { "name": "src/OrderList.tsx", "start": 15, "end": 27 }
}
],
"statistics": { "total": { "clones": 3, "duplicatedLines": 40, "duplicatedTokens": 252, "percentage": 5.1 } }
}
報告されたクローンごとに、重複を確認して分類するため、行範囲のみを読み取ります(start/end の前後を offset/limit で指定して Read します)。候補ファイル全体を Read してはいけません。報告されたクローンは --min-tokens の一致により、jscpd の類似度が構造上高くなっています。抽出計画にトークン数と行数を記録してください。
1b. ast-grep による構造の確認
jscpd でクラスターを検出したら、ast-grep のメタ変数を使い、同じ形状、つまり同じ呼び出し形状またはキャプチャされた変数だけが異なる同じブロックであることを確認します。$VAR/$INIT は任意の識別子または式に一致するため、名前変更されたキャプチャだけが異なるブロックも一致します。
ast-grep -p 'const $VAR = useState($INIT)' --lang tsx <path>
抽出を計画する前に、これを使って、実際に抽出可能な重複と偶然トークンが重なっただけのものを区別します。抽出を行わずに構造検索だけを行う場合は、ast-grep-search スキルを使用してください。
1c. 検出ツールを利用できない場合の Grep によるフォールバック
npx/jscpdを利用できない場合、またはエコシステムにおいて PATH 上にnpxがない場合は、エージェント主導のテキスト検索に切り替えます。
- Grep を使用して、繰り返し使われている関数名、変数のパターン、インポートのまとまりを検索します
- Glob を使用して、類似した構造のファイル(例:すべての
*List.tsx、すべての*Detail.tsx)を特定します - 候補ファイルを読み込み、重複を確認して範囲を測定します
このフォールバックでは、変数名の変更や引数の並べ替えなどを伴う類似コードの検出率が低くなります。利用可能な場合は jscpd の方法を優先し、Grep は jscpd を利用できない場合にのみ使用してください。
分類対象となる重複の兆候(jscpd と Grep のどちらの方法で検出しても、Step 2 では同じカテゴリに分類します):
- 複数のファイルで同一のユーティリティ関数が定義されている(文字列の切り詰め、日付の書式設定、バリデーション)
- 同一のエラー処理ブロック(try/catch のパターン、エラー状態の JSX)
- コピー&ペーストされた UI フラグメント(ページネーションコントロール、確認ダイアログ、読み込み状態)
- 繰り返されるフック/状態管理のパターン(削除確認+ミューテーション+ハンドラー)
- インライン実装の繰り返しを示す、重複したインポートブロック
Step 2: 重複を分類する
検出した重複を、抽出カテゴリごとにまとめます。
| カテゴリ | 抽出先 | 配置規則 |
|---|---|---|
| ユーティリティ | 純粋関数 | src/lib/utils/ または src/utils/ |
| コンポーネント | 共有 UI コンポーネント | src/components/ui/ または src/components/shared/ |
| フック | カスタム React/Vue フック | src/hooks/ または src/composables/ |
| 型 | 共有型定義 | src/types/ または抽象化対象と同じ場所 |
共有コードの配置には、プロジェクトで既に採用されている規則に従ってください。規則がない場合は、フレームワークに基づいて提案してください。
Step 3: 抽出を計画する
重複のクラスターごとに、抽出を計画します。
- 抽象化に名前を付ける — 共有される動作を表す、明確で説明的な名前を使用します
- インターフェースを定義する — 使用箇所ごとの違いに対応するために必要なパラメーターを決定します
- 配置場所を選ぶ — 共有コードの配置に関するプロジェクトの規則に従います
- すべての利用箇所を列挙する — 更新するすべてのファイルを特定します
- リスクを評価する — パラメーター化が必要な、重複箇所間の微妙な違いを記録します
(--dry-runが指定されておらず、対象範囲が明確な場合を除き)作業を進める前に計画をユーザーに提示してください。
計画の形式:
## Extraction Plan
### 1. [Abstraction Name] → [target file path]
- Type: utility | component | hook
- Replaces: [N] identical blocks across [M] files
- Consumers: [list of files]
- Parameters: [any variations that need to be parameterized]
- Duplicated: [N] tokens / [N] lines (from jscpd; blank when the Grep fallback was used)
- Similarity: [N]% (from jscpd; "exact" when ast-grep-confirmed as the same shape)
- Estimated lines saved: [N]
DuplicatedフィールドとSimilarityフィールドは、jscpd のレポート(クローンごとのトークン数/行数と、クラスターの割合)から取得します。これにより、最善を尽くした説明ではなく、定量化された--dry-runレポートになります。Grep によるフォールバック(1c)でクラスターが検出された場合は、空欄にするか、「grep-estimated」と記載してください。
Step 4: 共有抽象化を抽出する
計画した抽出をそれぞれ実行します。
- 適切な型付けとドキュメントを備えた共有抽象化を作成します
- 利用側ファイルの各インスタンスを、新しい抽象化のインポートと使用に置き換えます
- 違いに対応する — 複数の抽象化を作成するのではなく、インスタンス間の違いをパラメーター化します
- インポートを更新する — 新しいインポートを追加し、インライン版でのみ必要だったインポートを削除します
抽出の順序: 依存関係のないユーティリティから始め、次にコンポーネント、最後に(ユーティリティ/コンポーネントに依存する場合がある)フックの順に進めます。
次に進む前に、トラッカーで各抽出を完了済みにします。
Step 5: テストを作成する
抽出した各抽象化のテストを作成します。
| 抽象化の種類 | テスト方法 |
|---|---|
| ユーティリティ関数 | あらゆる入力のバリエーションとエッジケースを網羅する単体テスト |
| UI コンポーネント | レンダリングテスト、プロパティのバリエーション、アクセシビリティ |
| カスタムフック | モック依存関係を使用したフックのテスト、状態遷移 |
| 型定義 | 該当する場合は型レベルのテスト(tsd、expect-type) |
既存の規則に従い、テストファイルを抽象化と同じ場所、またはプロジェクトのテストディレクトリに配置してください。
ステップ 6:不要なコードを整理する
すべての抽出が完了したら、次の作業を行います。
- 更新したすべての利用側ファイルから未使用のインポートを削除します
- 不要なコードを削除します。置き換えられたインラインのヘルパー関数が対象です
- 参照が残っていないことを確認します。削除したコードへの参照が残っていないか検索します
ステップ 7:すべてのチェックに合格することを確認する
すべての検証を実行します。
TypeScript/JavaScript プロジェクト:
npx tsc --noEmit # Type checking
npm run lint # Linting (or biome/eslint directly)
npm run test # Full test suite
Python プロジェクト:
ty check . # Type checking
ruff check . # Linting
pytest # Test suite
Rust プロジェクト:
cargo check # Type checking
cargo clippy # Linting
cargo test # Test suite
3つすべてに合格する必要があります。失敗した項目がある場合は、完了を報告する前に問題を修正してください。
出力の概要
すべてのフェーズが完了したら、次の内容を報告します。
## DRY Consolidation Summary
### Extractions
- [Abstraction Name] (type) — replaced N blocks in M files
- ...
### New Files Created
- path/to/new/file.ts — [description]
- ...
### Tests Added
- N tests across M test files
### Net Effect
- ~N lines of duplicated code consolidated
- N reusable abstractions created
- All verified: typecheck + lint + N passing tests
エージェント向けの最適化
| コンテキスト | アプローチ |
|---|---|
| 決定論的なクローンスキャン | npx jscpd --reporters json --min-tokens 50 --output /tmp/jscpd-dry --silent <path> を実行し、その後 duplicates[] を解析して正確な範囲を特定します |
| 構造形状の確認 | ast-grep -p '<pattern with $METAVARS>' --lang <lang> <path> |
| クイックスキャン | --dry-run を使って、変更せずに重複レポートを確認します |
| 対象を絞った抽出 | --scope utilities を使って、ユーティリティ関数のみを抽出します |
| 大規模なコードベース | 対象を特定のディレクトリに絞ります:/code:dry-consolidation src/components/ |
| 抽出後の検証 | `npx tsc --noEmit 2>&1 |
| テストの実行(高速) | npm test -- --bail=1 --reporter=dot を実行し、合否をすばやく確認します |
関連項目
/code:refactor— ファイルやディレクトリの機能的なリファクタリング(純粋関数、不変性、合成)/code:antipatterns— コードの不適切なパターンを検出のみで分析しますast-grep-search— パターンを検索するための構造的なコード検索
関連スキル
- 統合中に不要なコードが検出された場合 →
/code:dead-code - 統合後に複雑さが高い場合 →
/code:complexity