New Features
- Semantic clones, experimental:
--semantic. The token passes match runs of tokens, so they miss two functions that do the same job with different code. Such a function may be renamed and restructured, or written in another language, like a validation rule that a Rust backend enforces and a Svelte frontend repeats (Type-4 clones).--semantic(config keysemantic) embeds every function with a code embedding model and reports the pairs whose vectors point the same way as clones of kindsemantic.- jscpd embeds the functions of JavaScript, TypeScript, JSX, TSX, Vue, Svelte, Astro, Python, Rust, Go, Java, Kotlin, C#, C, C++, PHP, Ruby, Scala and Swift files that clear
--min-tokensand--min-lines. It embeds each function's code without comments, starting at the name the function is declared under. - Two functions pair only if they are in different files, neither calls the other, and the clones already found do not cover both. Each must be the other's closest match among the functions of its language; a function that is only close to the best match needs a higher similarity. The cosine must reach
--semantic-thresholdfor a pair across languages, or--semantic-same-thresholdfor a pair within one language (0.4125 and 0.6375 with the default model), and it must stand at least 3 standard deviations above each function's background.--semantic-scope samekeeps the pairs within one language, and--semantic-scope crosskeeps the pairs across languages. - The default model, CodeRankEmbed (MIT), runs inside jscpd on the CPU once
jscpd --semantic-downloadhas fetched it (548 MB, pinned by revision and SHA-256), and a scan makes no network call. In a comparison of nine open models, it found more known clones than jina-embeddings-v2-base-code at the same precision, and reviewers judged more of its pairs to be duplicates. jscpd also runs jina-embeddings-v2-base-code, which is faster:jscpd --semantic-download jina-embeddings-v2-base-codefetches it (324 MB), and--semantic-model jina-embeddings-v2-base-codescans with it. --semantic-urlsends the functions to an OpenAI-compatible embeddings API instead, such as Ollama, llama.cpp, text-embeddings-inference or a hosted API. jscpd reads the API key only fromJSCPD_SEMANTIC_API_KEY, and sends it only to a URL given on the command line or to a server on this machine. A config file can neither hold a key nor send code to another host on its own.- jscpd caches the vectors, so a repeat run embeds only the functions whose code changed. Each set of scanned paths has its own cache file. Once the vectors of changed and deleted functions make up more than a quarter of that file, the next run that embeds something rewrites it without them.
--semantic-rebuild-cacheembeds everything again and replaces the file at once. - Each model scores similarity on its own scale, so each model gets its own thresholds.
jscpd --semantic-modelslists the nine models jscpd has calibrated, with their thresholds, licenses and where they run.--semantic-modeltakes any of them by the name in that list (CodeRankEmbed), by Hugging Face id or by Ollama name, and the thresholds follow the model. The rule for groups of copies has two more settings, tuned with jina-embeddings-v2-base-code: a near-best margin of 0.05 and a floor of 0.8. jscpd scales both for every other model by the model's gap between its two thresholds, which gives 0.075 and 0.7125 for CodeRankEmbed. Qwen3-Embedding-0.6B and jina-code-embeddings-0.5b expect an instruction before the text, and jscpd puts the one they were calibrated with before every function; the config keyprefixreplaces it. A model that jscpd has not calibrated gets 0.6 and 0.75, and the run warns about it. - The console prints
Clone found (rust, semantic ~0.78),-r aiprints[~0.78 semantic], JSON carries"kind": "semantic"andsimilarity, and SARIF uses the rulejscpd/semantic-code.--kind semantickeeps only these clones. The code lives in a new crate,cpd-semantic, and a run without--semanticworks as before. fixtures/semantic-demois a runnable example: a Rust API and a SvelteKit frontend with 8 rules written on both sides and 2 features written twice in one language. (#1101, #1103, #1105, #1108, #1110)
- jscpd embeds the functions of JavaScript, TypeScript, JSX, TSX, Vue, Svelte, Astro, Python, Rust, Go, Java, Kotlin, C#, C, C++, PHP, Ruby, Scala and Swift files that clear
Bug Fixes
-
A scan path inside another scanned its files twice. With
jscpd . src, or a config file that listssrcnext tosrc/generated, jscpd walked every file under the inner path once for each path and reported each of those files as a clone of itself.--semanticembedded their functions twice and paired each one with itself. Version 4 read those files once, and so does jscpd now, as it already did with--follow-symlinks. Seefixtures/nested-paths-demo. (#1106) -
Svelte components lost two kinds of use to
--dead-code. Svelte reads a store as$name, in the script and in the markup, and that is often the only use thatimport { page } from '$app/stores'gets, so--dead-codereported the import as unused with 100% confidence. It also reported a name as an unused symbol when the markup read it only inside the${…}of a template literal, as inhref={(p) => `/?${base}&page=${p}`}, because the markup scan skipped template literals the way it skips plain strings.$namenow counts as a read ofname(runes such as$stateand$props, and$$props, do not), and jscpd reads the placeholders of a template literal as code in every component format. On two SvelteKit apps, sshx and the RealWorld example, the six and three findings of basta 0.3.0 were all of these two kinds, and neither app has a finding now. Seefixtures/dead-code-demo. (#1102) -
Positions in files with Windows line endings drifted. The generic tokenizer, which reads Python, Go, Java, C# and most other formats, moved one byte forward per line where a CRLF line ends in two bytes, so every position was short by the number of lines above it. The
positionvalues in the JSON report were off, and an--ignore-patternmatch removed tokens a byte behind per line, which changed the token counts of the clones around it. Files with CRLF endings now report true byte offsets, and where a pattern applies, their clones can count different tokens than before. Files with LF endings are unaffected. Seefixtures/crlf-demo. (#1101)
Dependencies
pasteis gone from the build. The crate is unmaintained (RUSTSEC-2024-0436), and candle's matrix kernels,pulpandtokenizersstill depend on it. The workspace now patches it with a small local crate that hands its one macro topastey, its maintained successor, socargo auditandcargo denypass without an ignore for the advisory. The patch goes away once those crates release their switch topastey.sha2moved to 0.11, and every other dependency to its latest compatible release. (#1107, #1109)- oxc moved to 0.151 and ruff to 0.0.15, each as one family of crates (#1098), and clap to 4.6.7 (#1097).
Deprecations
--min-duplicated-linesnever did anything, and now says so. Since the first 5.x release, the docs described it as a minimum percentage of duplication to report, but no code ever read it: a scan with--min-duplicated-lines 100found the same clones as one without it. jscpd now hides the flag from--helpand prints a warning when it is passed, and a later release will remove it. The flag is still accepted, so a script that passes it keeps working. To fail a run on too much duplication, use--threshold; to set the smallest clone worth reporting, use--min-linesor--min-tokens. (#1100)
Published Packages
basta@0.3.0on crates.iocpd-core@0.1.19on crates.iocpd-finder@0.1.19on crates.iocpd-reporter@0.1.20on crates.iocpd-tokenizer@0.1.18on crates.iocpd@5.3.3on npmjscpd@5.3.3on npmjscpd-darwin-arm64@5.3.3on npmjscpd-darwin-x64@5.3.3on npmjscpd-linux-x64-gnu@5.3.3on npmjscpd-linux-arm64-gnu@5.3.3on npmjscpd-linux-x64-musl@5.3.3on npmjscpd-linux-arm64-musl@5.3.3on npmjscpd-windows-x64-msvc@5.3.3on npmjscpd-windows-arm64-msvc@5.3.3on npmjscpd==5.3.3on PyPI
Not Yet Published
cpd-semantic@0.1.0(published: none)jscpd@5.3.3(published: 5.3.2)
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