mcpField
C
code-migration

code-migration

github.com/kucherenko開発6.5k103 回インストール今日更新
リポジトリ

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 は、一方のフォルダーにある各関数と、任意の言語の組み合わせで同じ役割を果たすもう一方のフォルダーの関数を対応付け、対応する関数がないものを一覧表示します。このスキルではこれを使って移植の進捗を測定します。移植済みのもの、残っているもの、そして書いたばかりの移植コードが認識されたかどうかを確認できます。

移植は2つのフェーズで進めます。まずテストを移植し、次にテスト対象のコードを移植します。ソースのテストカバレッジレポートから、各テストがどの関数を実行するかがわかるため、機能を証明するテストと一緒に各関数を移植できます。計画:まずテスト、次にコードを参照してください。

全体を通して、次の2つの用語を使います。

  • ソース:移植元の実装。iOS アプリ、Python ライブラリ、JavaScript パッケージなどです。
  • ターゲット:移植先の実装。Android アプリ、Rust クレート、TypeScript への書き換えなどです。

どちらが旧版または新版である必要もありません。ソースが本番環境で使われ続け、変更されることもあります。また、ターゲットにソースにはない機能がすでに含まれている場合もあります。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 はベクトルをキャッシュするため、再実行時にはコードが変更された関数のみを埋め込みます。処理は数秒で完了します。

2つのパスを選び、移植全体を通して固定してください。最初がソース、次がターゲットです。パスは2つ必要で、互いに重複してはいけません(app/ と app/android/ の組み合わせは拒否されます)。開始時にターゲットが空でもかまいません。

1回の実行で両方のフェーズを測定します。レポートには Code ブロックと Tests ブロックがあり(JSON では code と tests のセクション)、テストはテストとのみ対応付けられます。jscpd は、各言語の慣例に基づいてテストを判定します。たとえば、*_test.go、test_*.py、*.test.ts、*Test.java などのテストファイル、tests/、__tests__/、src/test/ などのフォルダー、#[cfg(test)] モジュール内の Rust テスト、it('rounds cents', () => …) のような JavaScript のテストケースです。フェーズ1では Tests ブロックを読み、フェーズ2では Code ブロックを読みます。

ベンダーコードと生成コードを対象外にする

--compare は両方のパスにあるすべての関数を数え、埋め込みます。ベンダー依存関係(cargo vendor または go mod vendor による vendor/、third_party/)、インストール済みパッケージ(node_modules/、.venv/)、ビルド出力(target/、build/、dist/)、生成コードは移植対象ではありません。それでも、ベンダー化されたクレートのツリーだけで数千もの関数が含まれます。これらがパスに含まれていると、初回実行時にすべてが埋め込まれるため、数秒ではなく数十分かかり、割合も移植対象ではなく依存関係を示すことになります。

jscpd は .gitignore で除外されたものをスキップしますが、これは Git リポジトリ内に限られます。初回実行の前に、次のことを行ってください。

  1. 両方のパスにそのようなフォルダーがないか確認します。移植先として作成したターゲットも、ビルドするか依存関係をベンダー化すると、それらのフォルダーがすぐに作られるため、初回ビルド後にもう一度確認してください。

  2. ターゲットの言語やツールが生成するフォルダーと、レポートフォルダーを記載した .gitignore をユーザーに提案します。napi-rs でビルドした Rust アドオンの場合は、次のようになります。

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

    ターゲットが Git リポジトリにない場合は、git init の実行後にのみ jscpd が .gitignore を読み取ることをユーザーに伝えてください。

  3. .gitignore が機能するまでは、同じフォルダーを --ignore に渡し、すべての実行で同じグロブを使用してください。

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

合計行を見れば、何かが除外から漏れていないか分かります。ターゲットの関数数がソースよりはるかに多い場合や、小さな変更の後に実行した際に数百の関数が埋め込まれる場合は、パスにベンダーコードまたは生成コードが含まれていないか確認してください。

測定

自分用とユーザー用に、コンソールレポートを実行します。ここでは、Python の請求処理ライブラリを TypeScript に移植しています。

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
  • 1 行目は移植の進捗です。ソースの関数のうち、ターゲットに対応するものがある割合を示します。ソースに「のみ」ある関数が残りの作業です。
  • 2 行目とターゲットの「のみ」の項目は、ターゲット独自のコードを示します。移植に必要だったヘルパー、ソースにはなかった機能、または jscpd が認識しなかった移植コードです(見落としを参照)。
  • 各ファイルの行には、対応する関数の数、対応する関数ペアの平均類似度(low のペアがある場合はその数も含む)、および対応ファイルが表示されます。対応ファイルとは、反対側で対応する関数を最も多く含むファイルです。対応ファイルを参考に、不足している関数の移植先を判断してください。
  • 「別の名前で対応するペア」には、大文字小文字とアンダースコアを無視しても名前が異なるペアが一覧表示されます。名前を変更した移植、コンストラクター、プラットフォーム名などです。各ペアには類似度とレベルが表示されます(ペアの読み方を参照)。名前で検索しても見つからない場合がありますが、これらはすでに移植済みです。
  • ターゲットが空の場合、レポートには合計を示す 1 行と 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 の説明や進捗管理用の issue に貼り付けられる表です。-r html は jscpd-compare.html を出力します。ユーザーが開いて 2 つの依存関係グラフを並べて確認できる移行マップ、または同じペアを表形式で表示したものです。

計画:まずテスト、次にコード

コードより先にテストを移植します。移植したテストは、対象のコードがまだ存在しない段階で、各関数の移植が何を意味するかを定義します。関数を実装したら、そのテストに合格するか、まだ何が異なるのかが分かります。テストより先に移植したコードは、ソースを読んだあなたの解釈と照らし合わせて確認することしかできません。

カバレッジを使ってテストと関数を対応付ける

テスト名だけでは、どの関数が実行されるかは分かりません。たとえば、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)に書き込み、ソースのテストが変更されたら再作成します。

このマップは、次の3つに使います。

  • どのテストを関数と一緒に移植するか、また移植したテストが合格するためにどの関数が必要か。
  • どのソース関数もカバーしていないテストがある場合、その関数を移植する前に、現在の動作を記録するテスト(特性テスト)をソース側に書き、他のテストと一緒に移植します。テストのない移植には、動作を確認する手段がありません。
  • 作業の順序。必要な関数が少ないテストから取り組むと、最も早く成功状態になります。

カバレッジが示すのは、テストがどのコードを実行するかであって、何を検証するかではありません。あるテストが別の関数へ進む途中でしか実行しない関数は、そのテストとの結び付きが弱いものです。その関数についてのテスト名やアサーションを優先してください。

フェーズ1:テストを移植する
  1. JSON レポートの tests セクションから、ソース側で対応するテストがないテストを取得します。コールバックとして書かれた JavaScript または TypeScript のテストケース(it('rounds cents', () => …))はタイトルで対応付けるため、pytest の test_rounds_cents や Rust の rounds_cents のような名前付きテストと対応します。
  2. テストをファイルごとに、対象のテストレイアウトへ移植し、対象に実装する予定の API に対して記述します。入力値と期待値はそのままにします。プラットフォームの制限や言語の数値型などにより対象の動作を変える必要がある場合は、コメントでテストにその違いを書き、ユーザーにも伝えます。
  3. 移植したテストはまだ存在しない関数を呼び出すため、失敗します。コンパイル言語では、テストビルド全体が停止します。そのようなテストは、関数が実装されるまでビルド対象から外しておきます。たとえば、モジュール宣言をコメントアウトしたままにする、cfg 機能を使う、ソースセットから除外するといった方法があり、フェーズ2で1つずつ有効にします。テストをコンパイルできるように空の関数を作ってはいけません。ソースと同じ名前のスタブは名前で対応付けられ、移植済みとしてカウントされる可能性があります。
  4. 対象のテストを実行します。すでに合格する移植済みテストは、対象にすでにある機能をカバーしています。失敗するテストや無効化されているテストをテストマップと照らし合わせ、フェーズ2の作業リストを作成します。
フェーズ 2:コードを移植する

テストを用意してから、関数を一つずつ移植します。

  1. JSON レポートを実行し、code セクションからソースの unmatched リストを取得します。
  2. 作業の順序を決めます。まずテストを移植済みの関数から始め、呼び出し元より先に呼び出し先の関数を移植します。呼び出し元をヘルパーより先に移植しても、呼び出せるものがありません。ソースの readyToPort リストは、その順序の先頭にあたります。呼び出し先がすべて対応済みの関数が、呼び出される頻度の高い順に並んでいます。そこから次の関数を選び、各バッチの後にレポートを再実行します。関数を移植するたびに、その呼び出し元も移植可能になることがあるためです。この順序に従い、次のファイルに着手する前に一つのファイルを仕上げ、各ターゲットファイルの移植をまとめて完了させます。
  3. 移植する前に、ターゲットにその関数がすでにないことを確認します。renamed: true のペアは、別の名前で移植済みの関数です。これらは unmatched には含まれませんが、ユーザーから関数の移動先を尋ねられた場合に確認してください。次に、files にある対応ファイル内で、別の名前の同等機能(コンストラクター、統合された関数、ヘルパーを置き換えるプラットフォームやライブラリの呼び出しなど)を jscpd が見逃していないか検索します。同等機能が見つかった場合は、その関数を再度移植せず、該当する機能を記録して次に進みます。
  4. 対応ファイル、またはターゲットの構成上適切なファイルに移植します。ターゲット言語の命名規則に従いつつ、名前から元の関数が分かるようにしてください(Rust や Python では encodeBinary は encode_binary になります)。同じファイルですでに移植済みの関数ペアのスタイルに従います。移植済みの関数ペアは 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 になったり、新しいソース関数がリストに加わったりすることがあります。古いリストを使い回さず、作業の各バッチの前にレポートを再実行してください。

2つの実装間の同等性

iOS アプリと Android アプリのように、両方の実装が存在する場合、npx jscpd --compare ios/ android/ で同等性を確認できます。「Only in」の両方のリストを確認してください。

  • 片方にしかない関数は、プラットフォーム固有のコード(Android の通知チャンネル、iOS のデリゲートコールバック)か、もう一方に欠けている機能のいずれかです。実装する前に、欠けている機能をユーザーに報告してください。
  • 欠けている機能を追加するには、その機能を備えている側をその機能の参照元として扱います。計画:まずテスト、次にコードのように、テストをカバレッジに関連付け、テストを移植してからコードを移植します。

jscpd は、すでに一致しているモジュール内で関数を対応付けます。モジュールとは、各側のファイルが共有する最も深いフォルダーの直下にあるフォルダーです。最適な対応付けのため、2つのツリーは並行した構成(ios/<plugin> と android/<plugin>)にしてください。

対応付けの確認

  • level は使用中のモデルの尺度で類似度を評価するため、どのモデルでも意味は同じです。デフォルトの CodeRankEmbed を使う場合、言語を問わず high(0.7125 以上)はほぼ常に同じ関数です。medium は通常、構造を変更した同じ関数です。low の場合は両方の関数を読む必要があります。関連するコードも対応付けられるためです(UTF-8 のバイト数を数える関数と、それらに文字列を変換する関数など)。Tauri プラグインでは、すべての low のペアで名前の異なる関数が対応付けられ、そのうち1つは異なるプラグイン同士の対応付けでした。
  • matchedBy: code は、2つの関数が互いに最も近い対応先であり、類似度が際立っていることを意味します。
  • matchedBy: name は、大文字小文字とアンダースコアを無視すると名前が一致し、コードも十分に類似していることを意味します。短い移植を検出できます。同じ名前の関数が別の処理を行うこともあるため、これらのペアを確認してください。
  • 類似度では、動作の小さな違いは検出できません。異なる方向に変化した2つのバージョンも対応付けられます。変化を検出できるのはテストとコードの確認だけです。

検出されないもの

jscpd が比較するのは関数だけです。型、定数、データを持つ列挙型、SQL、UI マークアップ(ストーリーボード、Android レイアウト)はレポートに含まれないため、自分で移植して確認してください。既知の対応付け漏れは次のとおりです。

  • 言語をまたぐコンストラクター(Java のコンストラクターと Rust の new、Kotlin の constructor、Swift の init)は、コードが十分に類似している場合にのみ対応付けられます。
  • 移植時に名前を変更した短い関数(_finder_penalty_add_history に対する add_history など)。
  • 1つの関数を複数に分割した場合、または複数の関数を1つにまとめた場合。レポートでは最も近い部分だけが対応付けられ、残りは未対応として示されることがあります。
  • 無名関数(コールバック、クロージャー)は一切対象になりません。ただし、it('rounds cents', () => …) のような JavaScript と TypeScript のテストケースは、タイトルを基準に対応付けられます。

ターゲット側にのみ表示されている関数が、参照元の関数を移植したものだと分かっている場合、上記のいずれかに該当します。数値を上げるためだけに正常に動作しているコードの名前を変更しないでください。また、参照元と同じ名前のスタブや空の関数を追加しないでください。jscpd は名前を基準にスタブを対応付けることがあり、その場合、実施していない作業が進捗として報告されてしまいます。

オプション

  • --min-tokens(--compare を指定した場合は 30)と --min-lines(5)は、合計に含める関数を決定します。より短い関数も、対応する相手として検出されます。実質的な関数に絞るには値を大きくし、割合を比較できるよう、実行ごとに値を固定してください。
  • --ignore では、ベンダー依存関係、ビルド出力、生成コード、フィクスチャなどのファイルを除外します(ベンダーコードと生成コードを除外するを参照)。--pattern では、あるモジュールのテストなど、比較対象を一部のファイルに絞ります。実行ごとに glob を固定してください。
  • --format では、たとえば参照元に移植対象のコードとビルドスクリプトが混在している場合などに、対象言語を絞り込めます。
  • --semantic-model と --semantic-url では、別の埋め込みモデルまたは OpenAI 互換 API を選択します(npx jscpd --semantic-models でしきい値が調整済みのモデルを一覧表示できます)。各モデルはそれぞれ異なる尺度でスコアを算出するため、実行ごとにモデルを固定してください。
  • 進捗にかかわらず終了コードは 0 になるため、このコマンドだけではビルドは失敗しません。

ルール

  • 2つのパス、その順序、オプション、モデルを実行ごとに同じにしてください。そうしないと、数値を比較できなくなります。
  • ベンダー依存関係、インストール済みパッケージ、ビルド出力は両方のパスから除外してください。git リポジトリでは、ユーザーに提案する .gitignore を使うか、--ignore を使ってください。
  • コードより先にテストを移植し、関数はカバレッジマップで関連付けられたテストと一緒に移植してください。
  • レポートは、何を読むべきか、何が残っているかを示す地図として扱ってください。移植が正しいことを証明するものではありません。証明するのはテストです。
  • 割合を不正に操作してはいけません。スタブを作成したり、jscpd のためだけに名前を変更したり、ユーザーの判断なしに分母を小さくするためにソース関数を削除したりしないでください。
  • レポートとコードを読んだ結果が食い違う場合は、コードを信頼し、どのペアに誤りがあるように見えたかをユーザーに伝えてください。

コメント