compare-codebases について
jscpd で 2 つのコードフォルダーにある関数を比較するスキルです。異なる言語の実装も対象とし、対応関係やテスト、未対応の関数を分析する開発者向けです。
主な機能
- 01言語をまたぐ関数マッチング
- 02類似度レベル付きレポート
- 03コードとテストを分けて比較
- 04関数名によるマッチング
- 05未対応関数の確認ガイド
活用シーン
- 01Python 実装と TypeScript 移植版の比較
- 022 つのコードベース間の関数対応確認
- 03対応関数のない箇所の特定
- 04テスト間の対応関係の評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ kucherenko/ jscpd --skill compare-codebasesインストール後、Claude Code で/compare-codebasesを入力して読み込まれたか確認します。
詳細
compare-codebases
jscpd --compare A B は、フォルダー A の各関数を、同じ役割を果たすフォルダー B の関数と対応付け、どちらのフォルダーにも対応する関数がないものを一覧表示します。2 つのフォルダーは、同じ言語でも異なる言語でもかまいません(Python と TypeScript、Kotlin と Swift、Java と Rust など)。このスキルでは、比較の仕組みと適切な実行方法を説明します。比較結果を進捗の指標としてコードを移植する場合は、code-migration スキルを使用してください。jscpd のその他の機能については、jscpd スキルを参照してください。
比較の仕組み
jscpd は、両方のフォルダーに含まれる JavaScript、TypeScript、JSX、TSX、Vue、Svelte、Astro、Python、Rust、Go、Java、Kotlin、C#、C、C++、PHP、Ruby、Scala、Swift ファイルから関数を検出します。jscpd 内で動作するコード埋め込みモデル(デフォルトでは CodeRankEmbed)を使い、各関数のコードをベクトルに変換します。そのため、言語、名前、構造が異なっていても、同じ役割を果たす 2 つの関数は同じ方向を指します。同じフォルダー内の関数同士が比較されることはありません。
関数の対応付けは、次の 2 段階で行われます。
- コードによる対応付け。2 つの関数が、互いに相手側のフォルダーにある最も近い関数であり、コサイン類似度がモデルのしきい値(CodeRankEmbed の場合、異なる言語間では 0.4125、同じ言語内では 0.6375)に達し、さらにその類似度が他の候補との比較で際立っている場合に対応付けられます。最も近い関数に近い別の関数も、より高い基準を満たせば対応付けられるため、一方の側で機能が 2 回実装されている場合は、2 つのペアが作られます。ペアは、少なくとも一方の関数にとって最良の一致である場合に限り維持されます。そのため、別のクラスにあるよく似た関数が、その関数と対応する関数のペアに割り込むことはありません。同点の場合は、同じ名前のファイルが優先されます。
--min-tokens(--compareでは 30)未満、または--min-lines(5)未満の短い関数は、他の多くの関数に似てしまうため、この段階の対象外です。 - 名前による対応付け。前の段階で対応付けられなかった関数は、大文字小文字、アンダースコア、スペース、句読点を無視した名前が同じで、類似度が
mediumレベル(CodeRankEmbed で異なる言語間の場合は 0.5625)に達していれば、もう一方のフォルダーの関数と対応付けられます(encodeBinary、encode_binary、_encode_binary;テスト名のrounds centsとrounds_centsなど)。名前によるペアでは、最初の段階の最近傍チェックと際立ちチェックを行わないため、最初の段階のしきい値より高い類似度が必要です。そうしないと、2 つのコードベースのあらゆるloadやinitが対応付けられてしまいます。関数の長さは問われないため、短い移植コードも見つかります。名前によるペアは、最初の段階で関連付けられたモジュール内に限られます。モジュールとは、一方の側にあるすべてのファイルが共通して含まれる最も深いフォルダーの直下にあるフォルダーです(android/notification/…の場合はnotification)。ビルドスクリプトなど、ほかのファイルより上の階層にあるファイルがあっても、そのフォルダーが上の階層へ移動することはありません。2 つのモジュールは、コードとテストを別々に集計し、一方のモジュールのペアのうち、もう一方のモジュールと最も多く対応付けられている場合に関連付けられます。そのため、あるプラグインのcheckPermissionsが、別のプラグインにある同名の関数と対応付けられることはありません。
各ペアにはモデルの尺度に基づいたレベルが割り当てられます。モデルによって、あるコサイン類似度が高いか低いかは異なるためです。
| レベル | CodeRankEmbed での異なる言語間の値 | 意味 |
|---|---|---|
high | 0.7125 以上 | ほぼ常に同じ関数です |
medium | 0.5625~0.7125 | 通常は同じ関数ですが、構造が変更されています |
low | 0.4125~0.5625 | 両方を読んでください。関連コードもこのレベルで対応付けられます |
jscpd はテストとコードを別々に計測し、レポートの 2 つのブロックに分けて表示します。また、テストはテストとのみ対応付けられます。テストの判定には、言語ごとの慣例が使われます。
*_test.go、test_*.py、*.test.ts、*.spec.js、*Tests.swift、*_spec.rbなどのテストファイル。tests/、__tests__/、spec/、src/test/(Java、Kotlin、Scala でテストを格納する場所)、MyAppTests/、MyApp.Tests.Integration/などのテストフォルダー。比較対象フォルダー自体の名前も含みます。#[cfg(test)]モジュール内、または#[test]の下にある Rust 関数。it('rounds cents', () => …)のような JavaScript または TypeScript のテストケース。 どちらの側にもカウント対象となるテストがない場合(下記参照)、レポートは見出しなしの1つのブロックになります。
合計には、--min-tokens トークン以上かつ --min-lines 行以上の関数が含まれます。それより小さい関数は、相手としてのみ表示されます。本体のない宣言(TypeScript のオーバーロード、.d.ts ファイルの関数、インターフェースや抽象メソッド)は対象外です。匿名関数(コールバック、クロージャー)も対象外ですが、JavaScript と TypeScript のテストケースは例外です。it('rounds cents', () => …) のようなテストケースはタイトルで扱われます。test、specify、fit、xit、xtest、bench で記述されたものも同様で、その後に .only、.skip、.each(table) が付く場合も含みます。スイートとフックは匿名のままです。jscpd は型、定数、SQL、UI マークアップを比較しません。
2つのフォルダーを比較する方法
1. フォルダーを選ぶ
- リポジトリではなく、コードを指定します。
app/src/main/javaとios/Sourcesを指定し、2つのリポジトリのルートは指定しないでください。ビルド出力、ベンダーコード、インストール済みパッケージ、生成ファイルが結果を薄めます。また、ベンダーの依存関係ツリーが含まれると、すべての関数が埋め込み対象になるため、実行時間が数秒から数十分に延びることがあります。Git リポジトリ内では、jscpd は.gitignoreで除外されるものをスキップします。リポジトリ外、または.gitignoreで除外されていないフォルダーは、--ignoreで渡してください(--ignore "**/vendor/**,**/target/**,**/node_modules/**")。このようなフォルダーに対する.gitignoreのエントリーが片方にない場合は、ユーザーに追加を提案してください。 - フォルダーは2つ必要で、互いに重複していてはいけません。
app/とapp/android/の指定は拒否されます。 - 可能であれば、並行する構成を維持してください(
ios/<module>とandroid/<module>)。モジュールは名前の照合に使われるため、フォルダー名が一致していると役立ちます。 - 移植の場合は、移植元を先に、移植先を後に指定します。そうすると、レポートの最初の行で移植の進捗がわかります。両方の実装が今後も使われる場合は、順序は問いません。
- レポートではテストが独立したブロックで計測されるため、1回の実行でテストとコードの両方を対象にします。コードだけを求められた場合は、
--ignore "**/__tests__/**,**/*.test.*,**/test/**"を指定してテストを除外します。テストだけを求められた場合は、2つのテストフォルダーをパスとして渡すか、--patternでテストファイルを選択します。
2. モデルを取得する
初回実行時にモデル(548 MB)が必要です。ダウンロードする前にユーザーに確認してください。
npx jscpd --semantic-download
jscpd はフォルダーのペアごとにベクトルをキャッシュするため、以降の実行ではコードが変更された関数だけを埋め込みます。
3. 概要を読む
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
- 両方のフォルダーにテストがある場合、レポートには
CodeブロックとTestsブロックがあり、それぞれに以下のすべてが含まれます。各ブロックの先頭2行は、他方に対応する関数(またはテスト)が各フォルダーで占める割合を示します。 - ファイルテーブルには、対応する関数の数、対応する関数ペアの平均類似度(
0.62, 1 lowのようにlowのペア数も表示)、および対応する関数の大半がある反対側のファイルが表示されます。 - 「異なる名前で対応」は、大文字小文字とアンダースコアを無視しても名前が一度でも異なるペアを一覧表示します。名前による検索では、これらは見つかりません。
- 「のみ」は、対応する関数がない関数をフォルダーごとに一覧表示し、ファイル単位でまとめます。各ファイルの関数数に続いて、各関数の先頭行、名前、長さが表示されます。
- 色付き表示では、レベルは緑(
high)、黄(medium)、赤(low)で、割合は完全に対応している場合は緑、一部のみの場合は黄、まったく対応していない場合は赤です。コンソール出力を解析する場合は--no-colorsを指定するか、JSON レポートを使用してください。 - 片方のフォルダーに関数がない場合、レポートには合計と
<folder> has no functions yetの1行だけが表示されます。それ以外の結論を出す前に、パスと言語を確認してください。
4. ペアを確認する
npx jscpd --compare billing-py/ billing-ts/ -r console-full
console-full は、すべてのペアをその類似度とレベルとともに追加し、名前で見つかったペアをマークします。比較を信頼できるものとして報告する前に、サンプルを読んでください。
highのペアを 2、3 個。このコードでペアリングが機能することを確認するためです。- すべての
lowペア。関連するコードもそこにペアリングされるためです(クライアントの呼び出しと呼び出し先のエンドポイント、UTF-8 バイトを数える関数と文字列をそれに変換する関数など)。 by nameでマークされたペア。同名の関数が別のことをしている可能性があるためです。
サンプルのペアが誤っている場合は、どれが、なぜ誤っているのかを述べてください。
5. 対応するものがない関数を確認する
「Only in」リストにある関数は、もう一方の側に存在しないか、その対応するものが認識されなかったかのどちらかです。それを欠落と呼ぶ前に、もう一方のフォルダーを検索してください。対応するファイル内で、別の名前で、より大きな関数の一部として、あるいはライブラリやプラットフォームの呼び出しに置き換えられて存在するかもしれません。ペアリングにおける既知のギャップは次のとおりです。
- 言語をまたぐコンストラクター(Java のコンストラクターと Rust の
new、Kotlin のconstructor、Swift のinit)は、そのコードが十分に類似している場合にのみペアになります。 - もう一方の側で名前が変更された短い関数(
_finder_penalty_add_historyに対するadd_history)。 - 1 つの関数が複数に分割されたり、複数が 1 つに統合されたりする場合、最も近い部分だけがペアになることがあります。
設計上、一方の側にのみ存在する関数は想定内です。プラットフォームの接着コード(iOS のデリゲートコールバック、Android の通知チャネル)、ある言語が必要とし別の言語がそうでないヘルパー、一方の側が削除した機能などです。
6. 報告する
ユーザー向けに、数行で要約してください。2 つのパーセンテージ、注目すべき名前変更されたペア、確認した low ペアとその結果、そして各側の実際のギャップを、ファイルまたはモジュールごとにグループ化して示します。結果を探索したい人向けには、移行マップを書き出してください(-r html)。これは、両側を依存関係グラフとして描画し、ペアがそれらを橋渡しし、同じペアを 2 番目のタブに表として一覧表示するオフラインの 1 ページです。ドキュメントや issue 向けには、Markdown レポートを書き出して添付または貼り付けてください。
npx jscpd --compare billing-py/ billing-ts/ -r markdown -o .jscpd-compare
自身の処理用には、JSON レポート(-r json、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、renamed、matchedBy を含みます)があります。パスは各フォルダーからの相対パスです。レポートはリポジトリの外に書き出すか、そのフォルダーを .gitignore に追加してください。
結果を変えるオプション
--min-tokensと--min-linesは、どの関数を対象とするかを設定します。これらを上げると、実質的な関数に焦点を当てられます。--ignoreと--formatは、ファイルを絞り込みます。たとえば、テストやビルドスクリプトを除外できます。--semantic-modelは別のモデルを選び、--semantic-urlは OpenAI 互換の埋め込み API を選びます。npx jscpd --semantic-modelsは、しきい値が調整済みのモデルを一覧表示します。--semantic-thresholdと--semantic-same-thresholdはしきい値を動かします。新しい値の両側のペアを読んだ後にのみ変更してください。--semantic-rebuild-cacheは、すべてを再度埋め込みます。これはキャッシュが疑わしい場合にのみ必要です。
経時的に実行を比較する場合は、フォルダー、その順序、オプション、およびモデルを同じに保ってください。そうしないと、数値が比較できなくなります。終了コードは、結果に関係なく 0 です。
制限事項
- 類似性では、動作の小さな違いは検出できません。乖離した2つのバージョンも引き続きペアとして対応付けられ、多くの場合は
highになります。乖離を見つけるには、テストとコードの確認が必要です。 - 片方の構造を大きく変更するほど、その関数がコードによってペアとして対応付けられる数は少なくなります。
- 比較対象は関数のみです。
- モデルはCPUで実行されます。初回の実行では、Apple M1で1秒あたり約25個の関数を埋め込みました(261個の関数を11秒)。小規模なCIマシンでは数倍遅くなることがあります。2回目以降の実行ではキャッシュが再利用されます。