The post What's new in jscpd 5.4.2 shows the new features with commands and real output.
New Features
-
Boundaries keep the code of a module inside it. The new
boundarieskey of the config file lists rules. A rule names the code it protects (code: a folder, a file, a glob, or a method aspath#name), where copies of it must not show up (notIn, or every file outsidecodewhen it is left out) and where they may after all (allow). Every clone run checks the rules, and a copy of protected code outside its boundary fails the run with exit code 1. Import linters miss such copies, because a copied function imports nothing.- jscpd looks for copies three ways, and a rule can set its own bars in
thresholds. Token clones find copy-paste, the structure comparison of--similarityfinds an edited copy, and the--semanticmodel finds the same logic written again, often in another language.disableturns kinds off by their--kindnames. Without a downloaded model, jscpd prints a warning and checks the rules without the semantic search. - A mistake in a rule, such as an unknown key or a glob that doesn't compile, stops the run with an error that names the rule. A rule that checks nothing in a run, after a typo in a path or a method name for example, gets a warning.
- The console, console-full, ai and json reporters show the violations, the json one under
boundaries. Baselines and--changeddon't apply to violations yet, and SARIF, the language server and the MCP server don't report them. fixtures/boundaries-demois a small shop whose web app copies the backend's pricing and loan code. (#1225, #1227)
- jscpd looks for copies three ways, and a rule can set its own bars in
-
--ignore-identifierspairs a name with one name. Every name used to match every other name, sox = x + x * xwas a clone ofd = a + b * c, although its data flows another way. Now a name on one side has to stand for the same name on the other side every time, and two names can't stand for one, as in Baker's parameterized matching and NiCad's consistent renaming. On 24 projects in 11 languages, 6,942 of the 14,501 renamed clones that the old matching reported had no such mapping.- jscpd cuts a clone where a name starts to stand for two and still reports every part of
--min-tokenstokens or more. - A name reached as a member, after
.or?., and after->in C, C++, PHP and Perl, is another name than a local of the same text. A constructor that setsthis.url = urlstill matches its copy that setsthis.url = link(#1220). --ignore-identifiersand--ignore-identifiers=consistentgive the new matching,--ignore-identifiers=blindthe old one. The config takes"ignoreIdentifiers": true,"consistent"or"blind", and the Action input takesconsistentorblind. The value goes after=only, so--ignore-identifiers src/still scanssrc/.fixtures/type2-demohas theconsistentandmembersexamples. (#1217, #1218)
- jscpd cuts a clone where a name starts to stand for two and still reports every part of
-
--similaritypairs a name with one name too. The fingerprint of a subtree now records how its names repeat, sox = x + x * xno longer scores 1.0 againstd = a + b * c, and a renamed copy still does. Members, labels such as keyword arguments and object keys, and types each have names of their own, soself.url = urlstill matchesself.url = link. A function's own name matches any name. Kotlin'strue,falseandnulland Clojure'strue,falseandnilare literals now. On the same 24 projects, 15 of 23,530 pairs fell below 0.8, and runs take 2 to 9% longer.--ignore-identifiers=blindbrings back the old scores and also turns on renamed token clones.fixtures/similarity-demo/namesscores a renamed copy at 1.00 and a mirrored function at 0.74. (#1219) -
--historytracks the health score, and a bare--historywalks the last 30 commits. Every commit of the series gets the score thatjscpd --healthgives its tree. The console draws a health chart under the duplication chart and adds HEALTH and CHANGE columns, and every point of the JSON report hashealth.--healthand--dashboardtake--historynow: the badge or the dashboard comes first, then the series, in the markdown and html reports too.jscpd --historyused to fail with "a value is required". Without a value the flag takes the config'shistory, or else the newest 30 commits of HEAD (--history-limit).--history-every Nspaces them N commits apart, and the Action'shistoryinput takestrue.- jscpd scans a commit the way it scans the working tree:
--ignoreand--patternsee its files at their working-tree paths, and the checkout runs none of the repository's hooks. A walk that stops at the edge of a shallow clone prints a warning that names the commit. - The last 30 commits of axios take about 5.4 seconds, against 4.9 without the health score. (#1199)
-
jscpd knows 73 more formats by extension, and files by name. 94 of 224 formats had no built-in extension, a gap that came from v4, so
jscpd .skipped Dockerfiles, Makefiles,go.mod, Elixir and Nix files unless you mapped them. The new extensions come from GitHub Linguist, and a table of file names mapsDockerfile,Containerfile,Makefile,go.mod,CMakeLists.txt,Gemfile,Jenkinsfileand others. Existing formats got more suffixes, such as.zsh,.jsonc,.tfvarsand.sbt. A Markdown fence such as```dockerfiletakes its format from the same table, and--formats-extsand--formats-namesstill win over every built-in mapping.FORMATS.mdlists the file names too. (#1174, #1192)
Changes
-
jscpd .scans more files. On 22 repositories the new file associations picked up 1 to 5 more files each, mostly Dockerfiles,.editorconfig,.jsonc, Makefiles andgo.mod, and jscpd scans the.rstfiles of Sphinx docs like Markdown. The duplication percentage can move, and a--thresholdgate can fail. (#1192) -
--ignore-identifiersreports fewer renamed clones, and some of them are shorter. A baseline that 5.4 made with--ignore-identifiersis a blind one. Keep--ignore-identifiers=blindwith it or update the baseline, or--fail-on-new-clonesreports the cut clones as new. (#1218) -
--historywalks first parents. A merged branch counts as its merge commit, so a range across merged branches gives fewer points than before. jscpd builds a series only when a reporter shows it (console, console-full, ai or json, and markdown or html with--healthor--dashboard), so a badge-only--healthjob no longer walks the range. The ai lines read37be780 2026-08-08 2/1/47.6 +47.6 health 72 -2now, and a wrong health or dead-code option no longer fails a clone run. (#1199)
Bug Fixes
-
jscpd aborted on a Markdown page whose front matter ran into a code fence. A page that opens with a horizontal rule and later shows a Python stub had front matter from the rule down to the stub's
..., across the fence, and the scan panicked:jscpdexited with 134, andjscpd --mcpandjscpd --lspdied. A---on the first line now opens front matter only when the next line isn't blank, as Pandoc reads it, and only a---or...line before the first code fence closes it. The frontmatter of an Astro component follows the same rule. A page of code fences that never close used to take quadratic time, 3.9 seconds for 100 KB, and takes 0.04 seconds now. (#1223, #1226) -
A Razor view was read out of order. jscpd tokenized the HTML of a whole view and then the C# of its blocks, so a clone across that seam ended before it started: release builds printed ranges such as
[48:48 - 5:35], and debug builds panicked. A view is now one stream in the order of its text. Two identical views count as duplicated from the first line to the last, and markup in an@ifor@foreachblock matches the same markup outside one. (#1221, #1222) -
The MCP server's
check_duplicationnever matched a Vue, Svelte or Astro snippet. It tokenized a snippet as one stream, while a scan splits a component into its blocks and a Markdown file into its prose and fences. A component snippet met only the components inside Markdown fences, by meaning it found nothing, and the fences of a Markdown snippet matched nothing either. The server now splits a snippet the way a scan splits a file of its format. Each match hasformatandlines, and the note names every block too short to match. #1228 also fixed how jscpd counts the duplicated lines of a clone whose two fragments swap places, so the statistics of a--cross-formatsrun can change a little. (#1224, #1228, #1229) -
--historyfailed when git'slog.showSignaturewas on. git printed the signature check of each signed commit before the commit, and jscpd took that line for a commit hash. jscpd now passes--no-show-signatureand reads only lines that start with a commit hash. Reported with an exact reproduction by @vitorcsbrito. (#1195, #1196) -
--comparepaired a function with every near-best lookalike. It took the rule of--semantic, where a group of near-equal functions all pair with each other, but in a port a function has one counterpart. A pair now stays only when it is the best match of one of its two functions, and a near tie goes to the partner in a file of the same name. On the analysis modules of Lucene and Lucene.NET the pairs went from 2,219 to 1,392, and the share of pairs between files of the same name from 46% to 73%. Analysis and numbers by @MysterionRise. (#1162, #1194) -
--semanticreported a page and the component it renders as a clone. It skips a pair when one function calls the other, but it didn't count a JSX element such as<EditUserForm />as a call. It does now, and so does the call graph of--compare. Reported by @Rosenblad, fixed by @cristianchiriac. (#1210, #1211) -
Reports named a clone in a Markdown block or a component like
guide.md:markup. The VS Code extension opened that name as a file and failed. The JSONname, the language server'sjscpd/clonesandjscpd/semantic, the XMLpathand the xcode location now name the file, and the JSON keeps the block's language informat. The console, ai and HTML reports still name the block. (#1200) -
Every repository counted the hook samples in
.git/hooks. The walk takes hidden files, so it went into.gittoo, and the 9 bash files and 1 perl file thatgit initwrites counted in the file count, the line totals and the health score. jscpd now skips.gitfolders. (#1193)
Other
- jscpd depends on basta 0.3.2. basta reads YAML with serde-saphyr now, since its old parser, serde_yaml_ng, sits on the unmaintained unsafe-libyaml (RUSTSEC-2024-0449) (#1198), and it gives
--historythe dead-code report without the compiler's Rust findings (#1199). - The README shows the duplication of jscpd's own Rust code and a badge to follow the author on X (#1197).
Dependencies
- rustls moved to 0.23.46 for RUSTSEC-2026-0346 to 0349 (#1201). cargo deny and cargo audit ignore three advisories about extracting zip archives, RUSTSEC-2026-0367 to 0369: jscpd extracts no archive, and candle-core 0.11 still needs zip 8 (#1215, #1216).
- ruff moved to 0.0.17 (#1202), tree-sitter-php to 0.25.1 (#1204), tree-sitter-language to 0.1.9 (#1205), sha2 to 0.11.1 (#1207) and serde_json to 1.0.154 (#1208).
- In CI, actions/setup-node moved to 7.1.0 (#1203), actions/download-artifact to 8.0.2 (#1206) and taiki-e/install-action to 2.87.28 (#1209).
Thank You ❤️
- @soodoku for the issue behind the new
--ignore-identifiers, with numbers from Django and scikit-learn (#1217) - @MysterionRise for the analysis of
--compareon Lucene and Lucene.NET (#1162) - @cristianchiriac for the JSX fix in
--semantic(#1211), and @Rosenblad for the report (#1210) - @vitorcsbrito for the
log.showSignaturereport and its reproduction (#1195) - @pygarap for the file associations issue (#1174)
Published Packages
basta@0.3.2on crates.iocpd-core@0.1.22on crates.iocpd-finder@0.1.22on crates.iocpd-reporter@0.1.23on crates.iocpd-semantic@0.1.3on crates.iocpd-similarity@0.1.1on crates.iocpd-tokenizer@0.1.21on crates.iojscpd@5.4.2on crates.iocpd@5.4.2on npmjscpd@5.4.2on npmjscpd-darwin-arm64@5.4.2on npmjscpd-darwin-x64@5.4.2on npmjscpd-linux-x64-gnu@5.4.2on npmjscpd-linux-arm64-gnu@5.4.2on npmjscpd-linux-x64-musl@5.4.2on npmjscpd-linux-arm64-musl@5.4.2on npmjscpd-windows-x64-msvc@5.4.2on npmjscpd-windows-arm64-msvc@5.4.2on npmjscpd==5.4.2on 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