Guides

Comparing Two Codebases

Measure a port to another language, or check two implementations of one app for parity, with jscpd --compare: example reports in the console, JSON, Markdown and HTML, and how to read each one.

jscpd --compare A B pairs every function of folder A with the function of folder B that does the same job, in one language or across two. One report answers two questions:

  • During a port, such as a library moving to another language or an iOS app moving to Android, which functions of the source already have a version in the target, and which are still to port?
  • For two implementations of one app, such as the Android and the iOS one, what do both have, and what does only one of them have?

This page runs --compare on a small port and goes through every report it writes. To follow along in a browser, open the demo's migration map, or a larger one that compares ten Tauri plugins across Android and iOS.

Available from jscpd 5.4.0 and experimental. --compare pairs functions with the code embedding model of --semantic, which runs inside jscpd after a one-time download.

Quick start

Terminal
jscpd --semantic-download                     # once: fetches the model, 548 MB
jscpd --compare python/ typescript/           # the source first, the target second
jscpd --compare python/ typescript/ -r html   # writes the migration map to report/

--compare takes exactly two paths. In a port the first is the source, the code you port from, and the second is the target. For two implementations of one app the order only decides which side the reports list first and which side the map draws on the left.

ReporterWritesUse it for
console (default)the summary, in the terminalreading the result
console-fullthe summary plus every pairchecking each pair
jsonjscpd-compare.jsonscripts, CI and agents
markdownjscpd-compare.mda pull request, an issue or a wiki page
htmljscpd-compare.htmlthe migration map and the table of pairs

-r takes several reporters at once, as in -r console,json,html, and -o names the folder for the files, report/ by default. jscpd ignores other reporters with a warning. The exit code is 0 unless the run fails.

The example

fixtures/compare-demo in the jscpd repository holds a billing module halfway through its port from Python to TypeScript:

FileFunctionsWhat happened in the port
python/billing.pyline_total, apply_discountported under the same names, in camelCase
tax_for_regionported as salesTax
format_invoice_numberported as a one-line arrow function
due_datenot ported yet
python/shipping.pyshipping_costported
estimate_delivery_daysnot ported yet
typescript/billing.tstoCurrencynew in TypeScript, with no Python original
python/test_billing.pythree pytest testsported to typescript/billing.test.ts as it('…', () => …) cases
test_due_date_skips_the_weekendnot ported yet, like due_date itself

Every report below comes from this folder. Run the commands from inside it, since from the repository root jscpd would also load the repository's .jscpd.json:

Terminal
cd fixtures/compare-demo
jscpd --compare python typescript

The console report

Code
 71% 5 of 7 functions in python have a counterpart in typescript
 80% 4 of 5 functions in typescript have a counterpart in python

python
  file         paired  similarity  counterpart
  billing.py   4 / 5   0.89        billing.ts
  shipping.py  1 / 2   0.91        shipping.ts

typescript
  file         paired  similarity  counterpart
  billing.ts   3 / 4   0.90        billing.py
  shipping.ts  1 / 1   0.91        shipping.py

Paired under other names (1):
  python                        typescript              similarity
  billing.py:28 tax_for_region  billing.ts:27 salesTax  0.87 high

Only in python (2):
  billing.py (1)
    46  due_date                6 lines
  shipping.py (1)
    18  estimate_delivery_days  8 lines

Only in typescript (1):
  billing.ts (1)
    39  toCurrency  8 lines

Tests
 75% 3 of 4 tests in python have a counterpart in typescript
100% 3 of 3 tests in typescript have a counterpart in python

python
  file             paired  similarity  counterpart
  test_billing.py  3 / 4   0.87        billing.test.ts

typescript
  file             paired  similarity  counterpart
  billing.test.ts  3 / 3   0.87        test_billing.py

Only in python (1):
  test_billing.py (1)
    30  test_due_date_skips_the_weekend  7 lines

The report has a block for the code and a block for the tests, and both read the same way from the top. In a terminal it is in color: levels are green for high, yellow for medium and red for low, and shares and paired counts are green when every function has a counterpart, yellow when some do and red when none do. --no-colors prints the plain text shown here.

The two share lines

The first line of a block counts the functions of the source that have a counterpart in the target. Here 5 of the 7 Python functions have a TypeScript version, and during a port that line is your progress. The second line counts the other way: 4 of the 5 TypeScript functions have a Python original, and the fifth, toCurrency, is new. A parity check reads both lines, since each says how much of one side the other covers.

The file tables

Each side gets a table with a row per file:

  • paired is how many of the file's functions have a counterpart, out of the functions that count.
  • similarity is the mean similarity of those pairs. When some of them are low, the column says how many, as in 0.62, 1 low.
  • counterpart is the file on the other side that holds most of the counterparts.

So billing.py has 4 of its 5 functions in billing.ts, at a mean similarity of 0.89. A file with no pairs leaves the last two columns empty.

Paired under other names

This section lists the pairs whose names differ even once case and underscores are ignored, here tax_for_region, ported as salesTax. Nobody finds these pairs by searching for a name, so the default report shows them. Renamed ports, constructors (QrCode and __init__) and platform names (startWatch and watchPosition) end up here. Each pair has its similarity and a level.

Only in python, only in typescript

These lists hold the functions without a counterpart, grouped by file, each with its first line, name and length. In a port, "Only in python" is the work left: due_date and estimate_delivery_days. "Only in typescript" is code that exists only in the target, here toCurrency. A function in these lists is either missing on the other side or has a counterpart that jscpd did not recognize, so check it before you call it missing.

The tests block

jscpd measures tests apart from code and pairs a test only with a test, so the tests of line_total never count as line_total itself. It tells a test by the conventions of its language: test_billing.py is a pytest file and billing.test.ts a Vitest one. It also knows names such as *_test.go, *.spec.js and *Tests.swift, folders such as tests/, __tests__/ and src/test/, and Rust's #[test] functions and #[cfg(test)] modules. The full list is in the reference. When neither side has tests, the report shows the code alone, without the two headings.

The TypeScript tests are it('line total sums quantity times price', () => …) callbacks. jscpd names such a test case after its title and compares test names without case, underscores, spaces, punctuation and a leading test_. The title meets the pytest function test_line_total_sums_quantity_times_price, and the report does not list the pair as renamed.

What counts as a function

The billing.ts row says 3 / 4, though the file holds five functions. The fifth, formatInvoiceNumber, is a one-line arrow function, shorter than the bar for counting. A side's totals count the functions of at least --min-tokens tokens and --min-lines lines, the first and the last line included. With --compare, --min-tokens defaults to 30 instead of 50, because a function worth porting is often shorter than a clone worth reporting, and --min-lines stays at 5. A shorter function still shows up as the partner of one that counts, as formatInvoiceNumber does in the next section.

Anonymous functions, such as callbacks and closures, take no part. JavaScript and TypeScript test cases are the exception, since they go by their titles.

Every pair: console-full

Terminal
jscpd --compare python typescript -r console-full

console-full prints the console report and ends each block with the list of every pair:

Code
...
Pairs (5):
  python                               typescript                         similarity
  billing.py:8 line_total              billing.ts:10 lineTotal            0.89 high
  billing.py:18 apply_discount         billing.ts:19 applyDiscount        0.93 high
  billing.py:28 tax_for_region         billing.ts:27 salesTax             0.87 high
  billing.py:38 format_invoice_number  billing.ts:36 formatInvoiceNumber  0.89 high   by name
  shipping.py:6 shipping_cost          shipping.ts:5 shippingCost         0.91 high

Tests
...
Pairs (3):
  python                                                       typescript                                              similarity
  test_billing.py:8 test_line_total_sums_quantity_times_price  billing.test.ts:6 line total sums quantity times price  0.84 high
  test_billing.py:15 test_line_total_rejects_a_zero_quantity   billing.test.ts:13 line total rejects a zero quantity   0.91 high
  test_billing.py:23 test_discount_is_capped                   billing.test.ts:19 discount is capped                   0.84 high

jscpd finds pairs in two steps. The first pairs functions by their code with the rule of --semantic: each function is the other's closest match or close to it, and their similarity reaches the threshold and stands out from the function's similarity to the rest of the other side. tax_for_region and salesTax paired in this step, on their code alone.

The second step takes the functions left over and pairs two whose names match once case and underscores are ignored (encodeBinary, encode_binary, _encode_binary), when their similarity reaches the medium level. That bar keeps namesakes such as load and init from pairing whatever they do. The step takes functions of any size, since a port often makes a function shorter, so formatInvoiceNumber pairs with format_invoice_number here and the list marks the pair by name. A name pair also stays within the modules that the code pairs link, such as one plugin's Android and iOS folders.

Similarity levels

Every pair has its cosine similarity and a level. The level is on the scale of the model, since a cosine that is high for one model is low for another. For the default model, CodeRankEmbed:

LevelAcross languagesWithin one languageWhat it means
high0.7125 and up0.7125 and upalmost always the same function
medium0.5625 to 0.71250.675 to 0.7125usually the same function, restructured
low0.4125 to 0.56250.6375 to 0.675read both: related code pairs here too

low starts at the pair threshold, the lowest similarity at which jscpd pairs two functions. high starts at the group floor of the --semantic rules, or at the threshold when a model's threshold is higher, and medium starts halfway between the two. Another model has other numbers. Semantic Clones explains the thresholds, and jscpd --semantic-models lists them for each model.

The JSON report

Terminal
jscpd --compare python typescript -r json   # writes report/jscpd-compare.json

The JSON report holds everything console-full shows, plus the functions ready to port, in two sections of the same shape, code and tests. Each section has the two sides, the source first, and the pairs. Here is the code section of the demo, with two of its five pairs:

report/jscpd-compare.json
{
  "code": {
    "sides": [
      {
        "path": "python",
        "functions": 7,
        "matched": 5,
        "percentage": 71.43,
        "files": [
          { "file": "billing.py", "functions": 5, "matched": 4, "counterpart": "billing.ts", "similarity": 0.89, "lowPairs": 0 },
          { "file": "shipping.py", "functions": 2, "matched": 1, "counterpart": "shipping.ts", "similarity": 0.91, "lowPairs": 0 }
        ],
        "unmatched": [
          { "file": "billing.py", "name": "due_date", "start": 46, "end": 51 },
          { "file": "shipping.py", "name": "estimate_delivery_days", "start": 18, "end": 25 }
        ],
        "readyToPort": [
          { "file": "billing.py", "name": "due_date", "start": 46, "end": 51, "callers": 1 },
          { "file": "shipping.py", "name": "estimate_delivery_days", "start": 18, "end": 25, "callers": 0 }
        ]
      },
      {
        "path": "typescript",
        "functions": 5,
        "matched": 4,
        "percentage": 80.0,
        "files": [
          { "file": "billing.ts", "functions": 4, "matched": 3, "counterpart": "billing.py", "similarity": 0.9, "lowPairs": 0 },
          { "file": "shipping.ts", "functions": 1, "matched": 1, "counterpart": "shipping.py", "similarity": 0.91, "lowPairs": 0 }
        ],
        "unmatched": [
          { "file": "billing.ts", "name": "toCurrency", "start": 39, "end": 46 }
        ],
        "readyToPort": [
          { "file": "billing.ts", "name": "toCurrency", "start": 39, "end": 46, "callers": 0 }
        ]
      }
    ],
    "pairs": [
      {
        "a": { "file": "billing.py", "name": "tax_for_region", "start": 28, "end": 35 },
        "b": { "file": "billing.ts", "name": "salesTax", "start": 27, "end": 34 },
        "similarity": 0.868,
        "level": "high",
        "renamed": true,
        "matchedBy": "code"
      },
      {
        "a": { "file": "billing.py", "name": "format_invoice_number", "start": 38, "end": 43 },
        "b": { "file": "billing.ts", "name": "formatInvoiceNumber", "start": 36, "end": 37 },
        "similarity": 0.889,
        "level": "high",
        "renamed": false,
        "matchedBy": "name"
      }
    ]
  }
}
FieldWhat it holds
paththe path as given on the command line
functions, matched, percentagethe share line: the functions that count, those with a counterpart, and the share in percent
filesthe file table; a file with no pairs has no counterpart and no similarity
unmatchedthe "Only in" list, with the first and the last line of each function
readyToPortthe functions with no counterpart whose callees all have one, most called first
pairs[].a, pairs[].bthe function on the first side and its counterpart on the second
similarity, levelthe cosine similarity and its level
renamedwhether the names differ once case and underscores are ignored
matchedBycode for a pair from the first step, name for one from the second

Ready to port

A function is ready to port when it has no counterpart yet and everything it calls has one, so porting it waits for nothing. jscpd finds calls by name: a name followed by ( in a function's code calls the functions of the same side with that name, in a language that can call it. A function in the caller's own file wins, and jscpd does not follow a name that more than three functions carry. Two unported functions that call each other wait for each other, so neither is ready. callers counts the functions and tests that call it: due_date has one, its test.

Each side has its own list. A port works through the source's list. In a parity check, each list holds what could move to the other side next.

Reading it from a script

The JSON is what a CI job or an agent reads between two steps of a port. Three examples with jq:

Terminal
# the port's progress, in percent
jq '.code.sides[0].percentage' report/jscpd-compare.json
# what to port next, one function per line
jq -r '.code.sides[0].readyToPort[] | "\(.file):\(.start) \(.name)"' report/jscpd-compare.json
# the pairs under other names, to check by hand
jq -r '.code.pairs[] | select(.renamed) | "\(.a.name) -> \(.b.name) \(.similarity) \(.level)"' report/jscpd-compare.json
71.43
billing.py:46 due_date
shipping.py:18 estimate_delivery_days
tax_for_region -> salesTax 0.868 high

The code-migration skill runs this loop for an agent. It ports the tests first, then the code one function at a time, starting with readyToPort, and reruns --compare after each step to see the function leave the unmatched list and pair with the right counterpart.

The Markdown report

Terminal
jscpd --compare python typescript -r markdown   # writes report/jscpd-compare.md

The Markdown report has the tables of console-full for a pull request description, an issue or a wiki page that tracks a port. The demo's report starts like this:

report/jscpd-compare.md
# `python` compared with `typescript`

## Code

| Side | Functions | With a counterpart | % |
|---|--:|--:|--:|
| `python` | 7 | 5 | 71.43 |
| `typescript` | 5 | 4 | 80 |

### `python`

| File | With a counterpart | Similarity | Counterpart file |
|---|--:|--:|---|
| `billing.py` | 4 / 5 | 0.89 | `billing.ts` |
| `shipping.py` | 1 / 2 | 0.91 | `shipping.ts` |

It goes on with the table of the other side, then "Only in" tables with each function's place and length, the pairs under other names, and every pair with its similarity, level and the step that matched it. The ## Tests section repeats the same tables for the tests.

The HTML report: the migration map

Terminal
jscpd --compare python typescript -r html   # writes report/jscpd-compare.html

jscpd-compare.html is one file with its styles, script and data inside, so it opens offline and can travel as a CI artifact or a ticket attachment. It shows the comparison as a map and as a table, with one set of filters for both. Open the demo's report and click around while you read.

The migration map of the demo: the Python side on the left with ported functions lining a central channel, dotted bridges to their TypeScript counterparts on the right, due_date and estimate_delivery_days at the far left with a dark ring, and toCurrency at the far right.The migration map of the demo: the Python side on the left with ported functions lining a central channel, dotted bridges to their TypeScript counterparts on the right, due_date and estimate_delivery_days at the far left with a dark ring, and toCurrency at the far right.
Python on the left, TypeScript on the right, a dotted bridge for every pair.Open the demo's map

The header gives both shares, the tests below them and the number of pairs in the middle. Under it, "One mark per" picks folders, files or functions, and the search box finds a file or a function. When the comparison has tests, "Show" picks code, tests or both, and the page opens with both.

The map

The map draws the two sides as dependency graphs facing each other across a channel, the source in orange on the left and the target in green on the right. The labels across the top name the four bands: source functions not ported yet, ported ones lining the channel, target functions with a counterpart, and functions only in the target.

  • A mark is a function, a file or a folder. The page picks the level by the size of the comparison, and past 1,500 marks it asks for bigger marks or the table.
  • Code is a circle and a test is a square. With both shown, thin lines tie each test to the code it calls.
  • A mark fills from the bottom as its functions find counterparts. It is empty when none has one and full when all have.
  • A dark ring marks the source functions that are ready to port, here due_date and estimate_delivery_days.
  • A dotted bridge stands for the pairs between two marks. Its color is their mean similarity, light blue at 0.40 and dark blue at 1.00, and it is thicker for more pairs. The dark theme turns the blue scale around, so the closest pairs stay the easiest to see.
  • Thin gray lines are calls within a side.

Hover a mark to light up what it calls and what it pairs with. Click it to list its functions with their counterparts, calls and callers: the panel for due_date says it is ready to port and that test_due_date_skips_the_weekend calls it. Drag to move the map, and zoom with the − and + buttons or with Ctrl or ⌘ held while you scroll.

The table

The Table tab of the demo with Show set to Code: five ported pairs with their similarity, due_date and estimate_delivery_days marked Ready to port with No counterpart, and toCurrency marked Only in the target.The Table tab of the demo with Show set to Code: five ported pairs with their similarity, due_date and estimate_delivery_days marked Ready to port with No counterpart, and toCurrency marked Only in the target.
The Table tab with Show set to Code.Open the table

The Table tab lists the same bridges as rows: the source mark on the left, the similarity in the middle, the target mark on the right, and a status of ported, partly ported, ready to port, not ported or only in the target. A mark with no bridge gets a row of its own. A click on a column header sorts the table, and a click on a row opens the pairs behind it with what its source mark still lacks, or, for a function, its pairs, calls and callers. The address keeps the tab, so a link that ends in #table, like this one, opens the table.

Below the map and the table

Three more sections follow, for whatever the filters select. "Ready to port" lists those functions with the number of callers of each. "How alike the pairs are" charts the pairs by similarity in steps of 0.05, colored by level, with a switch to show the counts as a table. "Progress by folder" gives the share of each folder's functions that have a counterpart, on both sides.

A larger map: Android and iOS

tauri-apps/plugins-workspace has ten plugins written for both Android, in Kotlin, and iOS, in Swift. This report compares the two platforms as a parity check, built from commit d4835d0 of 2026-09-30 with jscpd 5.4.0:

Terminal
git clone https://github.com/tauri-apps/plugins-workspace
git -C plugins-workspace checkout d4835d0
mkdir android ios
for p in barcode-scanner biometric clipboard-manager dialog geolocation haptics nfc notification opener shell; do
  cp -R plugins-workspace/plugins/$p/android android/$p
  cp -R plugins-workspace/plugins/$p/ios ios/$p
done
jscpd --compare android ios -r html
The migration map of ten Tauri plugins with one mark per folder: each Android plugin folder on the left joined by dotted bridges to the iOS folder of the same plugin on the right, with marks half filled.The migration map of ten Tauri plugins with one mark per folder: each Android plugin folder on the left joined by dotted bridges to the iOS folder of the same plugin on the right, with marks half filled.
Ten Tauri plugins, Android against iOS, one mark per folder.Open the Tauri map

60 of the 147 Android functions and 58 of the 106 iOS functions have a counterpart, in 63 pairs: 38 high, 16 medium and 9 low. 17 pairs join functions that the two platforms name differently, such as startWatch and watchPosition in geolocation or writeTag and writeNDEFTag in nfc, and all 9 low pairs are among them. One pair crosses plugins: permissionState of notification on Android with getPermissionState of barcode-scanner on iOS, at 0.485, marked low. The "Only in" lists include the six functions of barcode-scanner's GraphicOverlay.kt on Android, the three of its CameraView.swift on iOS, and createChannel, which builds an Android notification channel for the notification plugin.

The map labels the first path "Source" and the second "Target" whatever the comparison is. In a parity check that only sets which platform the map draws on the left.

Before you trust the numbers

Point --compare at the code, not at the repository roots. It walks every file under both paths that jscpd finds functions in, so the functions of build scripts, examples and benchmarks count too. Keep vendored dependencies and build output out of both paths as well. Inside a git repository jscpd reads .gitignore. Elsewhere, or for folders that the repository keeps, pass --ignore, as in --ignore "**/vendor/**,**/target/**". A walk into node_modules or target/ also slows the run down, since jscpd embeds the functions it finds there.

Then read a sample of the pairs with -r console-full: two or three high pairs, to confirm that the pairing works on this code, every low pair, and the pairs marked by name, since a function with the same name can do something else.

Before you call a function in an "Only in" list missing, search the other side for it: in its counterpart file, under another name, as part of a larger function, or replaced by a library or platform call. jscpd misses some pairs:

  • constructors across languages, such as a Java constructor and Rust's new, Kotlin's constructor or Swift's init, pair only when their code is similar enough;
  • a short function renamed on the other side, such as add_history for _finder_penalty_add_history;
  • when one function was split into several, or several merged into one, only the closest part may pair.

Some functions exist on one side by design: platform glue, such as an iOS delegate callback or an Android notification channel, helpers that one language needs and the other does not, and features that one side dropped.

An empty side is fine. At the start of a port the target has nothing yet, and jscpd prints the source's totals with a note:

Code
  0% 0 of 7 functions in python have a counterpart in typescript
typescript has no functions yet

Tests
  0% 0 of 4 tests in python have a counterpart in typescript
typescript has no tests yet

The JSON and Markdown reports still list every function of the source as unmatched, so they track a port from its first day.

The comparison has limits. It compares functions, not types, constants or UI markup. Similarity does not see small differences in behavior, so two versions that drifted apart still pair. The more the target is restructured, the fewer of its functions pair by code. Related code pairs too, such as a function that counts UTF-8 bytes and one that converts a string to them, which is what the low level warns about.

More information

  • Semantic Clones: the model behind --compare, the other models and their thresholds. Every --semantic-* option applies to --compare except --semantic-scope.
  • Agent skills: compare-codebases runs and explains a comparison, and code-migration ports a codebase tests first with --compare as the progress measure.
  • Configuration: --compare in the table of options.
  • The reference in docs/rust.md: how the pairing works in detail, and how jscpd tells a test from code in each language.