New Features
-
--similaritycompares the structure of functions in 15 languages. jscpd parses every function and method and normalizes its syntax tree: the names of the functions it calls and its operators stay, local names, field names and literals become markers, and comments and the parentheses around one expression drop out. Every subtree of that tree is a fingerprint, and two functions score the Jaccard index of their two fingerprint sets, so a renamed copy scores 1 and an edited statement lowers the score.- It reads JavaScript, TypeScript, Python, Java, Kotlin, Scala, C#, Go, Rust, C, C++, PHP, Ruby, Swift and Clojure, and the code blocks of Markdown files and the scripts of Vue, Svelte and Astro components. 5.4.0 compared JavaScript and TypeScript only.
- The search is exact: it finds every pair at or above the ratio. A function nested in another is part of it, and test files and Rust
mod testsare left out. Code betweenjscpd:ignore-startandjscpd:ignore-end, or matched by--ignore-pattern, adds nothing to a function (#1144). --similarityalone compares at 0.8.--min-nodes(config keyminNodes, 20 by default) sets the smallest normalized tree that takes part,--min-linesapplies as before and--min-tokensdoes not. The JSON report gives the node count of both functions asnodes.- While the method was built, a check on 100 open-source repositories against a brute-force implementation of it, which scores every pair, found the same 58,405 pairs with the same scores, and jscpd's runs took 64.8 seconds on one thread against 498.
fixtures/similarity-demohas a runnable example in every language. (#1129, #1132, #1134, #1136, #1139, #1147, #1148, #1149, #1150, #1152, #1155, #1138, #1141, #1142, #1143, #1145, #1146, #1156)
-
An
ednreporter.-r ednwritesjscpd-report.ednwith{:candidates [...] :clones [...]}. The candidates are every pair--similarityfound, most similar first, with their score, language, both locations and node counts, also the pairs a token clone already reports. The clones are the ones the other passes found, with their kind, method and score. (#1151, #1153, #1156) -
--changedreports the clones of the files you are working on. The changed files are the onesgit statuslists: staged, unstaged and untracked files, and a renamed file under its new name. jscpd still scans every file and reports a clone when one of its fragments is in a changed file, so a new file that copies an unchanged one shows up.- The first run saves the clones of HEAD to
.jscpd-baseline.jsonat the repository root, or to the--baselinefile, with the commit and a hash of the scan paths and options. Later runs read it and mark the clones HEAD did not have as[NEW], so--fail-on-new-clonesworks with it. jscpd builds the file again after a commit or for other paths or options. A baseline file without a commit, such as one--update-baselinewrote, is used as it is. --changed-onlyscans the changed files alone, and they match only one another.- Both flags are command line only.
fixtures/changed-demowalks through five steps. (#1154, #1172)
- The first run saves the clones of HEAD to
-
--report-namesets the base name of the report files. Linter aggregators such as MegaLinter run several tools into one reports folder, and two jscpd runs there overwrote each other'sjscpd-report.json.--report-name megalinter-jscpd(config keyreportName) names thejson,xml,csv,html,markdown,sarifandednreports, and the GitHub Action takes it as thereport-nameinput. The badge, OpenMetrics and CodeClimate files keep their names. The name must be a plain file name: a path, an extension or an empty name is an error. Contributed by @MannXo. (#1015, #1058) -
The MCP server finds every type of clone and compares folders. Every tool takes
kinds:exact,renamed,similarandsemantic, ortype1totype4. Without it, a tool reports whatjscpdreports with the server's options.compare_folders(left, right)returns the JSON report of--comparefor two folders. The server speaks the MCP revision 2026-07-28, and clients of the older revisions keep working.check_duplicationnow finds every copy of a snippet, where it used to match one of two, and--mcpno longer ignores--semantic. (#1135, #1137)
Changes
--similarityscores differ from 5.4.0. The method is new, so a pair that scored 0.85 before can score lower now. Refresh a--thresholdor a baseline that was set on the old results.--similarity 1now reports functions with the same structure; up to 5.4.0 a ratio of 1 turned the search off, and jscpd prints a warning that says so. The options that the method replaced never reached a release. (#1156)
Bug Fixes
-
--semanticand--comparefound functions namedifin C, C++ and C# code. Around#ifand#ifdef, the grammars readelse if (…) { }as a function namedif, orwhileorawait, and everyif (in the scan counted as a call to it, so in--compareone such branch could collect thousands of callers. jscpd now drops a function whose type is the keywordelse. A word such asif,whileorcatchbefore a parenthesis counts as a call only as a member, as inpromise.catch(f). Reported by @MysterionRise, fixed by @mvanhorn. (#1163, #1188) -
Three
--comparefixes found on Apache Lucene and Lucene.NET, all by @MysterionRise:- Test helpers now count toward the namesakes of a call. A private
assertEqualsin Lucene's benchmark code took all 6,127 calls to the name, 6,126 of them from tests. (#1165) - The HTML map cuts a long folder label in its middle, so the folders of one module no longer read alike. (#1166)
- A .NET test project counts as tests wherever
Testssits in its dotted name, as inLucene.Net.Tests.Analysis.Common. Before, 6,736 functions in 771 files of such projects counted as code. (#1167)
- Test helpers now count toward the namesakes of a call. A private
-
The
--comparereports disagreed. The console and the HTML map now round a share the same way, so 60 of 147 functions read 41% in both. A Tests block appears only when a side has a test that counts, and a declaration without a body no longer counts as a function. (#1130) -
--dead-codereported an import used only in a TSDoc link as unused.{@link X},{@linkcode X}and{@linkplain X}now count as uses ofX, as TypeScript counts them, and in JavaScript files the JSDoc types of@param,@returns,@typeand the like count too. A name in the docs credits the import only, so a function that only a link names is still reported. (#1170, #1171) -
--ignore-patterndropped the wrong tokens in code blocks. jscpd computed the ranges on the host file but tokenized Markdown fences, component scripts and Razor blocks from the block's own offsets. The ranges now move to the block. (#1144) -
git commands run from a hook acted on the hook's repository. jscpd's git subprocesses now drop the variables git exports to hooks, such as
GIT_DIRandGIT_INDEX_FILE; before,--baseline-from-refin a pre-commit hook wrote the base tree into the commit's index. A--historyrange or a--baseline-from-refref that starts with-is refused, so a scanned repository's config can no longer pass git an option such as--output=<file>. The worktree cleanup no longer prunes the user's other worktrees, and an API key that an embeddings API echoes in an error is redacted. (#1169) -
The Nix flake warned about
stdenv.isDarwin. It usesstdenv.hostPlatform.isDarwinnow. Contributed by @mlavrinenko. (#1133)
Other
- The minimum Rust version is 1.97. oxc 0.153 and ruff 0.0.16 need it. (#1187)
- jscpd depends on basta 0.3.1, which has the dead-code fixes above.
- The tests check behavior instead of implementation details, and CI measures line coverage (#1168). The release retries
cargo publishwhile the crates.io index catches up (#1123), and thecompare-codebasesskill keeps vendored and build folders out of a comparison (#1124).
Dependencies
- oxc moved to 0.153 (#1125, #1184), ruff to 0.0.16 (#1185) and tree-sitter-swift to 0.7.4 (#1186), together in #1187. The new Swift grammar reads
nilas a node of its own, and--similaritytreats it as a literal. - xxhash-rust moved to 0.8.19 (#1126, #1140), and yoke-derive to 0.8.4 because 0.8.3 was yanked.
- In CI, taiki-e/install-action moved to 2.87.26 (#1127, #1183) and dorny/test-reporter to 3.2.0 (#1182).
Thank You ❤️
- @pygarap for the issues that shaped the new
--similarity, theednreporter and--changed: #1129, #1131, #1132, #1134, #1136, #1139, #1147, #1148, #1149, #1150, #1151, #1152, #1153, #1154 - @MannXo for
--report-name(#1058) - @mvanhorn for the fix of phantom C# functions (#1188)
- @MysterionRise for three
--comparefixes (#1165, #1166, #1167) and the report of phantom functions (#1163) - @mlavrinenko for the Nix flake fix (#1133)
- The readers of the DOU.ua discussion who reported the TSDoc false positive (#1170)
- @emanuelb (#357), @jonim8or (#411, #412), @JiangWeixian (#449) and @umair2602 (#519), whose older reports this cycle's triage closed
Published Packages
basta@0.3.1on crates.iocpd-core@0.1.21on crates.iocpd-finder@0.1.21on crates.iocpd-reporter@0.1.22on crates.iocpd-semantic@0.1.2on crates.iocpd-similarity@0.1.0on crates.iocpd-tokenizer@0.1.20on crates.iojscpd@5.4.1on crates.iocpd@5.4.1on npmjscpd@5.4.1on npmjscpd-darwin-arm64@5.4.1on npmjscpd-darwin-x64@5.4.1on npmjscpd-linux-x64-gnu@5.4.1on npmjscpd-linux-arm64-gnu@5.4.1on npmjscpd-linux-x64-musl@5.4.1on npmjscpd-linux-arm64-musl@5.4.1on npmjscpd-windows-x64-msvc@5.4.1on npmjscpd-windows-arm64-msvc@5.4.1on npmjscpd==5.4.1on PyPI
Verify
Archives are signed with Sigstore (keyless, <asset>.sigstore.json)
and carry SLSA build provenance. Replace jscpd-linux-x64-gnu.tar.gz with your asset:
cosign verify-blob \
--bundle jscpd-linux-x64-gnu.tar.gz.sigstore.json \
--certificate-identity-regexp '^https://github\.com/kucherenko/jscpd/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
jscpd-linux-x64-gnu.tar.gz
gh attestation verify jscpd-linux-x64-gnu.tar.gz --repo kucherenko/jscpd
sha256sum --check --ignore-missing checksums.txt