Commit Graph

1969 Commits

Author SHA1 Message Date
apk fc8e803a1a fix(ci): unblock the PR — Unicode data licence, and separator-free path tests
Two failures on the branch, neither on macOS, which is why they surfaced
only in CI.

`cargo deny` rejected `unicode_names2`, which `ruff_python_parser` pulls in
and which carries `Unicode-DFS-2016` alongside its MIT/Apache dual licence.
That licence covers the character tables and is the older wording of
`Unicode-3.0`, already on the allow list; added beside it.

The two new config-discovery tests asserted on `../project/.jscpd.json`,
and Windows prints the separator it joined with, so both failed there.
They now normalise backslashes before comparing — the assertion is about
which file was read, not about how the path is spelled.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-16 13:18:10 +02:00
apk 4c3f007d0b fix(ci): let the basta release publish the dependencies it needs
`cargo publish` verifies by building the packaged crate against the
*published* versions of its dependencies, not the local paths. basta uses
`cpd_core::deadcode`, `cpd_reporter::deadcode` and a public `LineIndex` —
none of which exist in the versions on crates.io — so every attempt failed
with "failed to verify package tarball", and basta could not reach crates.io
until a jscpd release happened to carry those three crates along.

That is the coupling this workflow exists to avoid. It now walks basta's own
dependency graph, publishes in topological order whatever is missing, and
waits for the index between crates. jscpd is excluded by construction: it is
a sibling, not a dependency, and releasing it is not this workflow's job.

Bumps the three crates that actually changed: cpd-core 0.1.13 -> 0.1.14
(the `deadcode` module), cpd-tokenizer 0.1.15 -> 0.1.16 (`LineIndex` made
public, `.pyi`), cpd-reporter 0.1.14 -> 0.1.15 (the dead-code reporters).
cpd-finder is untouched and stays where it is.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
basta-v0.1.1
2026-09-16 13:03:37 +02:00
apk 6229a1f21d docs(basta): ship a README with the npm package, and bump to 0.1.1
`basta@0.1.0` published with `files: ["run-basta.js", "platform-map.js"]`,
so npmjs.com/package/basta shows an empty page — the one place most people
will look before installing anything. npm takes the README from the
published tarball, so a version bump is the only way to put one there.

The README is npm-facing rather than a copy of the crate's: it leads with
`npm install -g basta` and `npx basta src`, and it names the known gaps —
no Vue/Svelte/Astro, class members matched by name, PEP 420 namespace
packages — so a reader can judge whether the tool fits before running it.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-16 13:00:25 +02:00
apk 687ed8906a ci: add a workflow to deprecate npm packages
Deprecating needs npm credentials, and this project's live in repository
secrets rather than on a laptop. Takes a spec and a message; an empty
message un-deprecates, which is npm's own convention and what makes a
wrong message recoverable.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-16 12:58:36 +02:00
apk fa1fdeaa0e fix(ci): poll for the basta dist-tag instead of reading it once
The publish step's own output says it: "Your package is being processed and
may take a few minutes to become available." Reading `dist-tags.latest`
seconds later returns the previous tag, so the guard failed a release that
had in fact succeeded — basta@0.1.0 was published, signed and tagged
correctly, and the job went red anyway.

Polls to a five-minute deadline, the same shape as the platform-package gate
directly above it.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-16 12:19:54 +02:00
apk 78401b3921 fix(ci): let the basta release fall back to a crates.io token
Trusted Publishing is configured per crate and per workflow filename, and
it can only be configured on a crate that already exists — so the first
publish of a new crate cannot use it. The job already had
`secrets.CARGO_REGISTRY_TOKEN` as a fallback, but the auth step failed the
job before anything could reach it. It is now non-fatal, with an explicit
check that at least one credential is present.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
basta-v0.1.0
2026-09-16 11:18:15 +02:00
apk b72abf0981 fix(cli): look for a config beside the scanned path, not only the cwd
`cd rust && jscpd --basta ..` silently ignored the repository's own
`.jscpd.json`: config discovery only ever tried `.jscpd.json`,
`.config/jscpd.json` and `package.json` relative to the working
directory. The configuration is a property of the code being scanned,
not of where the shell happens to be, so a scan aimed at a project read
none of its ignores, formats or thresholds.

The cwd is still searched first and still wins, so nothing about an
existing invocation changes; each scanned path's directory is searched
after it, and a path that is also the cwd is not searched twice. A file
argument contributes the directory holding it. `ConfigSource` now
carries the path it was read from, so the "Using config from" line names
the file that was actually used rather than assuming it was the local one.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-16 11:10:19 +02:00
apk b718753839 chore(basta): ship basta on its own trigger, separate from the jscpd release
basta is a separate product from jscpd — a different question about the
same tree — so it gets its own version, its own prebuilt packages and its
own trigger. jscpd goes out on every push to master that leaves a version
unpublished; basta goes out only when a `basta-v*` tag is pushed, or by
hand through workflow_dispatch. Neither release can drag the other along
or hold it up.

`basta-release.yml` owns the whole thing: it checks that the crate, the npm
manifest and the tag all name the same version and that the working tree is
already synced, publishes the crate, builds the eight targets and publishes
`basta-<platform>` plus the `basta` wrapper. It ends by asserting that npm's
`latest` really points at this release — the package carries 1.x versions
from an unrelated tool of the same name, and a dist-tag that quietly stayed
on those would hand every `npm i basta` the wrong program.

The one place the two cannot be separated is crates.io: `jscpd` depends on
`basta`, so cargo refuses to publish jscpd until a matching basta exists.
The jscpd crates job therefore still publishes basta when it is missing, and
that is now commented as the dependency requirement it is. Both paths skip a
version already on the registry, so whichever runs first wins.

npm needed the wrapper pattern `jscpd` and `cpd` already use: `rust/basta/`
holds the manifest, a platform map naming `basta-<platform>`, and a launcher
that falls back to a local `cargo build` when no platform package is
installed. `npm-prebuilt-package.mjs` grew a `--product` flag; its output for
jscpd is byte-identical to before. `prebuilt-targets.json` now carries a
platform label rather than a jscpd-specific blurb, so one table serves both
products. `sync-version.mjs` syncs and verifies the basta wrapper against the
basta crate version — the same independent version/pin update the jscpd block
does, for the same reason it does it.

Nothing is published by this commit.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-16 11:01:06 +02:00
apk d47d2dc38a fix(basta): read Python names hidden in quoted annotations and src layouts
Two defects found by checking every finding on the ten trending Python
repositories against an import graph rebuilt independently of the tool.
They accounted for 280 wrong findings out of 1,915; 4 remain, both in
cases where the checker is the generous one.

A quoted type annotation is a forward reference, not data. `def f(m:
"torch.nn.Conv1d")` reads `torch` exactly as surely as the unquoted form
does, and the `if TYPE_CHECKING:` idiom exists so that it *is* quoted —
the name may not be importable at run time. Without parsing them the
import that supplies the name looks unused, which is what made 275 of
834 unused-import findings wrong: 36% of the category, every one of them
reported at full confidence. The walk now tracks whether it is inside an
annotation and parses string literals found there with ruff's own
`parse_string_annotation`, which resolves in the source file's
coordinates so the references keep real offsets. Outside an annotation a
string stays what it was: weak evidence, never a read.

Absolute imports now also resolve against the roots the tree implies,
through a new optional `Analyzer::import_roots`. Python derives the
parent of every top-level package, which is what makes a `src/` layout
work from a file that belongs to no package: `utils/check_repo.py`
importing `transformers.testing_utils` matched no scan root and no
package of its own. Derived roots are tried last — a scan root is what
the user asked for, this is an inference.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-16 10:54:15 +02:00
apk df7b27c793 feat(dead-code): find code nothing runs, via the new basta engine
Adds a second question about the same tree: not "what is duplicated" but
"what does nothing reach". A new `basta` crate builds the import graph from
the project's entry points and walks it, reporting unused files, unused
exports, unused module-private declarations, unused imports and — opt-in —
unused class members, across JavaScript, TypeScript, JSX, TSX and Python.

It ships two ways: `jscpd --dead-code` inside the existing binary, and a
standalone `basta` command. Both reuse the walker, the filters, all fifteen
reporters and the `--threshold` / `--exit-code` gates, so nothing new has to
be learned to run it.

Because it is a graph traversal rather than a reference count, dead code
cascades: a helper whose only caller is itself dead is reported too. Every
finding carries a confidence score and, below 100, the reasons it might be
wrong — `eval`, an unrecognised decorator, a wildcard re-export, a name seen
only in a string, a file in the scan that did not parse. `--min-confidence`
sets the floor at 60.

Entry points come from `package.json`, `pyproject.toml`, framework and
file-name conventions, and anything a shell script, CI workflow, Makefile or
Dockerfile names by path. CommonJS is read alongside ESM. A project that
renames its own import paths is read on its own terms: `paths` and `baseUrl`
from `tsconfig.json` and `jsconfig.json` are resolved, through a relative
`extends` chain and per package in a monorepo. A file named only by a path
string — a worker spawned through `new URL`, a build entry, a vitest setup
file — counts as used.

Languages plug in through one trait, `basta::lang::Analyzer`, which owns
parsing, specifier resolution, entry-point conventions, manifests, alias
configs and path traits. Nothing outside `lang/` is language-specific;
`docs/basta-extending.md` walks through adding one.

Validated against knip, fallow, vulture, skylos, fossil-mcp and repowise on
trending repositories. On a corpus of ten of them — 11k files, 3.3M lines —
every one of basta's 1,806 findings was checked against an import graph
rebuilt independently of it: no unused-file, unused-import or unused-symbol
claim survived as wrong.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-16 09:23:13 +02:00
apk 7d6b4050e2 release: cpd-v5.2.1
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
v5.2.1
2026-09-15 08:24:41 +02:00
Andrey Kucherenko 3b90eeed37 Merge pull request #1060 from kucherenko/fix/1059-follow-symlinks-paths
fix(walk): report symlinked files by their walked path and scan each file once
2026-09-15 08:19:39 +02:00
apk 52a995d516 fix(walk): path filters see the walked path, like jscpd v4
`--skip-local folder1 folder2` shows cross-folder clones only, so a
file found through a symlink inside folder1 belongs to folder1 and its
clones with other folder1 files are dropped. `--skip-isolated` also
compares walked ids; the canonical path stays as a fallback so a group
folder that is itself a symlink still matches the files found through
it.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-14 21:28:08 +02:00
apk 8200cc452e fix(walk): report symlinked files by their walked path and scan each file once
With `--follow-symlinks` the source id was the canonical path, so a file
reached through a link showed up as an absolute path outside the scan
root, `--ignore` (matched on the walked path) and the report disagreed
about its name, and a file reachable through two paths was two sources
(a file symlink next to its target even became a clone of itself).

The walked path, anchored at the canonical scan root, is now the id:
what reports show, what `--ignore` matches, what `--absolute` prints.
The canonical path travels alongside (`DiscoveredFile::real_path`,
`PreparedSource::real_path`) for reading the file and for the
`--skip-local` / `--skip-isolated` prefix checks. When links are
followed, files whose canonical path was already seen are dropped; the
entry that is the real file wins, else the first walked path.

Docs: `--follow-symlinks` row in the option table, the v4 default in the
migration table and README. Fixture `fixtures/follow-symlinks-demo`.

Closes #1059

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-14 20:35:29 +02:00
Andrey Kucherenko 6627c87d49 Merge pull request #1057 from kucherenko/docs/skills-noise
docs(skills): Type-2/Type-3 passes are noisy, triage before refactoring
2026-09-13 09:02:15 +02:00
apk d2d3c2d089 docs(skills): Type-2/Type-3 passes are noisy, triage before refactoring
Both skills now say what the normalized passes cost: they ignore names,
values or a few statements on purpose, so they also report models,
DTOs, config tables, test setup, generated code and shared idioms. The
jscpd skill lists the usual false positives, conservative settings
(--ignore-identifiers alone with --min-tokens 70, --max-gap-lines 1-2,
--similarity 0.85+), one family per run, and no CI gating on them
until reviewed. The dry-refactoring flow gains an explicit triage step
for renamed/similar candidates (skip when the sides differ in meaning,
the sameness is intentional, the code is generated, the extraction has
no precise name, or the pair is tiny) and asks the agent to report
skipped candidates separately from refactored ones.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-13 08:59:08 +02:00
Andrey Kucherenko 0baeb3f314 Merge pull request #1056 from kucherenko/docs/skills-clone-kinds
docs(skills): teach the agents about clone kinds and the summary
2026-09-13 08:56:24 +02:00
apk 5c3b7d1d32 docs(skills): teach the agents about clone kinds and the summary
jscpd skill: the Type-2 flags (--ignore-identifiers, --ignore-literals,
--ignore-annotations) and Type-3 flags (--max-gap-lines, --similarity)
in the options table, a "Clone Kinds" section with real ai-reporter
lines for (renamed), [~0.91 gap] and [~0.75 ast], where each kind shows
up in the other reporters, and the summary's dup% tied to the current
run's flags. Config example lists the new keys.

dry-refactoring skill: the two extra detection passes as prerequisites,
kind-aware workflow steps (identify the kind, re-run with the same
flags), and three strategies keyed to the kinds: parameterize for
renamed clones, unify near-miss copies for gap merges, merge similar
functions for ast pairs, with calibration for the scores.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-13 08:53:15 +02:00
Andrey Kucherenko f0f29e2db6 Merge pull request #1055 from kucherenko/fix/375-xml
fix(xml): replace characters XML cannot carry, escape paths once
2026-09-13 07:51:31 +02:00
apk 4bf22478cd test(xml): keep the escaped-path test's file name Windows-safe
The path with < > " only needs to be in the recorded source_id; the
file on disk gets a plain name.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-13 07:48:17 +02:00
apk b6dc11fce0 fix(xml): replace characters XML cannot carry, escape paths once
Closes #375.

A snippet containing a NUL, an ANSI escape, a form feed or any other
control byte made the xml report unparseable: XML 1.0 has no way to
represent those characters, not even inside CDATA, so xmllint stopped at
"PCDATA invalid Char value 27" and every parser refused the file. The
reporter now replaces every character outside XML's Char production
(tab, LF, CR and U+0020 and up, minus U+FFFE/U+FFFF) with U+FFFD in
snippets and in path attributes. The existing "]]>" split is kept.

Path attributes were also escaped twice: quick-xml escapes attribute
values on its own, so the reporter's escape_xml() turned "r&d" into
"r&amp;amp;d". The manual escape is gone and the sanitized path goes in
raw.

Tests parse the generated report to the end with quick-xml and check:
control characters become U+FFFD and none survive, a "]]>" round-trips
through the split sections, a path with & < " > is escaped exactly once
and reads back unchanged, and legal text is left borrowed.

Fixtures: fixtures/xml-report-demo with a clone whose body holds real
escape bytes, a form feed and a "]]>" literal; the README shows the
xmllint and Python checks. Verified the published 5.2.0 fails xmllint
on it and the fixed binary passes.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-13 07:44:22 +02:00
Andrey Kucherenko 572716b542 Merge pull request #1054 from kucherenko/docs/next-version-5.2.1
docs(pypi): the next release is 5.2.1, not 5.3.0
2026-09-12 10:34:15 +02:00
apk 56f58d6ea5 docs(pypi): the next release is 5.2.1, not 5.3.0
The PyPI long description pointed the repo-hosted pre-commit hook at
"v5.3.0 or later"; the python hook ships with the next release, which
is 5.2.1. The PEP 440 examples use the same version.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-12 10:31:00 +02:00
Andrey Kucherenko f454eb7fb8 Merge pull request #1053 from kucherenko/refactor/dry
feat(history): dedupe the test scaffolding grown by --history and --fail-on-empty
2026-09-12 09:08:41 +02:00
apk 6d94fd3279 docs+test: console-full prints the --history block like console
print_appendices gave console-full the history block it lacked; the
History section now names both console reporters and an integration
test checks console-full renders it.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-12 09:04:37 +02:00
apk e635cd1b73 refactor(tests): fold the last reporter, cli and finder test duplicates
Phase 3 of the DRY pass: report_result / report_to_file_timed fixtures
for the stdout and time-printing reporters, rule_ids and
first_result_properties in the sarif tests, cross_formats_with in the
cli tests, run_isolating in the skip-isolated finder tests, and a
tests/common module for the cpd crate holding the invoice/credit-note
snippets the CLI and MCP tests both used.

rust/crates: 12 clones / 0.4% left, all deliberate (fixture corpora,
use blocks, a TS file and its JS twin, impl headers, AST visitors).
971 workspace tests pass, clippy clean.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-12 08:46:56 +02:00
apk 66419cc4a7 refactor(tests): shared scaffolding for the CLI, MCP and finder integration tests
Phase 2 of the DRY pass. cpd/tests/integration.rs was 23% duplicated:
every reporter test rebuilt the same temp root, wrote the same greet
pair, ran cpd with the same assertion block and parsed the report by
hand. Now: GREET_DUP / greet_root / write_greet_pair, run_ok_in,
read_json, scan_json (json+silent scans returning the parsed report and
stderr), run_scratch (exit code + stderr for a scratch dir), config_dir
/ stderr_in_dir for config discovery, commit_dates for --history and
baseline_root_with_pair for the baseline suites. mcp_stdio.rs gains
spawn_mcp / drive and INITIALIZE / INITIALIZED constants; cpd-finder's
five integration tests share tests/common (temp_dir, duplicate_js,
fixtures); mcp.rs unit tests share scratch_dir.

rust/crates: 69 clones / 2.4% before the pass, 21 / 0.7% after.
971 workspace tests pass, clippy clean.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-12 08:43:57 +02:00
apk 16766df02e refactor: share clone constructors, report context and tokenizer block helpers
Phase 1 of the jscpd-guided DRY pass (69 clones, 2.4% in rust/crates):

- cpd-core: Location::new, Fragment::new/with_blame and CpdClone::exact
  replace the 20-line struct literals repeated across the reporter tests
  and fixtures.
- cpd-reporter: ReportContext::new instead of the copied struct literal
  in every test; shared::fixtures::report_to_file replaces seven
  near-identical run_<reporter>_report helpers; shared::print_appendices
  prints the --summary and --history blocks for both console reporters.
- cpd-tokenizer: embedded::html_only_maps and tokenize_blocks_shifted
  hold the code razor.rs and sfc.rs had copied from each other.

971 workspace tests pass, clippy clean.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-12 08:39:10 +02:00
Andrey Kucherenko 433e231444 Merge pull request #1052 from kucherenko/feat/history-chart
feat(reporter): bar chart for --history instead of a sparkline
2026-09-12 08:29:42 +02:00
apk cfac085cf1 feat(reporter): bar chart for --history instead of a sparkline
The one-line sparkline was too small to read a trend from. The console
block now draws an eight-row bar chart: a percentage axis on the left
spanning the series' own min and max (a series moving between 2.0% and
2.4% would be a flat line on a 0-based axis), one column per point built
from eighth-block characters so every point keeps at least a sliver, an
x-axis, and point numbers that match a new `#` column in the table.
Past 20 points the columns narrow to one character so 30 points still
fit an 80-column terminal. The ai reporter keeps the compact sparkline.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-12 08:26:27 +02:00
Andrey Kucherenko 9f7334b424 Merge pull request #1051 from kucherenko/docs/citation
docs: add CITATION.cff and a Citation section
2026-09-12 08:05:19 +02:00
apk 097e5bdcd9 docs: add CITATION.cff and a Citation section
CITATION.cff (CFF 1.2.0, validated with cffconvert) gives GitHub's
"Cite this repository" button BibTeX and APA output for the project.
The README gets a short Citation section with a ready-made @software
entry. sync-version.mjs keeps the file's version in step with
rust/package.json and stamps date-released whenever the version
changes, and verifies the version like the other manifests.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-12 08:02:09 +02:00
Andrey Kucherenko 2f3b7bc4ba Merge pull request #1050 from kucherenko/feat/1002-history
feat(cli): --history, duplication trend over git history
2026-09-12 07:53:34 +02:00
apk adbf5ecf3d feat(cli): --history, duplication trend over git history
Closes #1002 (the --history half; --ratchet is deliberately not
implemented, the report shows the headroom instead).

`jscpd src --history v5.0.0..HEAD` (or `--history-since 2026-01-01`)
lists the commits with `git log`, checks each one out into a temporary
detached worktree (the --baseline-from-ref machinery, now shared through
baseline_ref::map_scan_paths), scans it with the run's own configuration
and keeps the totals; the current run is the series' last point.
`--history-every N` keeps every Nth commit counted from the newest,
`--history-limit N` (default 30) samples a long series evenly keeping
both ends. Nothing runs unless one of the flags is given.

Output: the console reporter appends a sparkline of the duplication
percentage, a table (commit, date, files, lines, clones, dup lines,
dup%, change, subject) with the change column red when duplication rose
and green when it fell, the overall trend line, and, when --threshold is
set and the latest value is under it, the headroom with the value the
threshold could be tightened to. The json reporter adds a `history`
object; the ai reporter prints one compact line per point. Other
reporters are unchanged.

New cpd_core::history (model, sparkline, change/headroom helpers with
unit tests), cpd::history (git log parsing, sampling, worktree scans),
cpd_reporter::history_render. Config keys history / historySince /
historyEvery / historyLimit; action input `history`. Integration tests
build a four-commit repo with fixed dates and check the JSON series,
the console block, every/limit thinning and the error paths. 969
workspace tests pass, clippy clean.

Docs: History section and option rows in docs/rust.md, README bullet,
action input row, fixtures/history-demo/README.md with a script that
builds the demo repository and the expected output.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-12 07:50:18 +02:00
Andrey Kucherenko eebc1cd7bd Merge pull request #1049 from kucherenko/feat/1047-exit-codes
feat(cli): non-zero exit for unknown format, missing path, failed reporter; add --fail-on-empty
2026-09-11 08:48:23 +02:00
apk bc3ec21f2d feat(cli): non-zero exit for unknown format, missing path, failed reporter; add --fail-on-empty
Closes #1047.

Four situations used to write an empty report and exit 0, so a CI job
that checks only the exit code saw a green run:

- `--format` naming a format that is not supported (a warning since #964)
  is now `Error: --format: 'x' is not a supported format`, exit 1. Names
  introduced through --formats-exts or --formats-names stay accepted.
- A scan path that does not exist is `Error: path does not exist: <p>`,
  exit 1, before anything runs.
- A reporter that fails to write its output still prints the existing
  `Reporter '<name>' error: ...` line; the run now ends with
  `ERROR: a reporter failed to write its output` and exit 1, after the
  other reporters have run.
- A scan that analyzes no files (paths exist, but nothing matched the
  --format/--ignore/--pattern filters or every file was below
  --min-tokens) prints `Warning: jscpd analyzed no files` and still
  exits 0, so an intentionally empty tree passes. The new
  `--fail-on-empty` flag (config key `failOnEmpty`, action input
  `fail-on-empty`) makes it exit 1. Reports are written before the check.

Docs: an "Exit codes" section in docs/rust.md, option rows in
docs/rust.md and docs/ci-and-hooks.md, a README feature bullet.
Fixtures: fixtures/fail-on-empty-demo with a `code/` clone pair, a
`nothing/` comment-only file, and a README quoting every command's
output and exit code. Tests: the #964 warning test becomes an error
test, plus integration tests for each new exit path and for the config
key; 531 tests pass, clippy clean.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-11 08:45:09 +02:00
Andrey Kucherenko f0cb1e4a8b Merge pull request #1048 from kucherenko/docs/language-aware
docs: say that jscpd tokenizes by language syntax, not text
2026-09-11 08:38:26 +02:00
apk 5f09d96c69 docs: say that jscpd tokenizes by language syntax, not text
Reviews compare jscpd with detectors that "use language-specific
normalization" as if jscpd matched raw text. It does not: every format
has its own comment and string rules, JS/TS/JSX/TSX go through the oxc
parser (with TypeScript erasure for cross-format pools), Vue/Svelte/
Astro/Markdown/Razor are split into embedded languages, and tokens are
classified so the Type-2 pass keeps keywords while renaming identifiers.

The README intro and a new first Features bullet say so, and docs/rust.md
gains a "How detection works" section walking through format detection,
tokenization, modes and ignores, normalization, Rabin-Karp matching and
the near-miss passes, plus what stays out of scope (Type-4). Every claim
is taken from cpd-tokenizer (generic.rs comment styles, javascript.rs,
ts_strip.rs, sfc.rs, markdown.rs, razor.rs, tokenizer.rs mode filter,
strips_annotations, is_common_keyword).

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-11 08:35:08 +02:00
Andrey Kucherenko 6c5178d63f Merge pull request #1046 from kucherenko/dependabot/github_actions/taiki-e/install-action-2.87.8
chore(deps): bump taiki-e/install-action from 2.87.3 to 2.87.8
2026-09-11 08:09:28 +02:00
Andrey Kucherenko e557068f83 Merge pull request #1045 from kucherenko/dependabot/cargo/rust/askama-0.16.1
chore(deps): bump askama from 0.16.0 to 0.16.1 in /rust
2026-09-11 08:09:08 +02:00
dependabot[bot] 8de72db3ac chore(deps): bump taiki-e/install-action from 2.87.3 to 2.87.8
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.87.3 to 2.87.8.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/0758d235715de2f3551eacc980d9ae8fce9342c3...d438492cf8a250514fa2d34b30bc3c0dc37c65ff)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.87.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-11 03:07:10 +00:00
dependabot[bot] 3824ea9ac0 chore(deps): bump askama from 0.16.0 to 0.16.1 in /rust
Bumps [askama](https://github.com/askama-rs/askama) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/askama-rs/askama/releases)
- [Commits](https://github.com/askama-rs/askama/compare/v0.16.0...v0.16.1)

---
updated-dependencies:
- dependency-name: askama
  dependency-version: 0.16.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-11 03:05:51 +00:00
Andrey Kucherenko 0f8785f423 Merge pull request #1038 from mnahkies/patch-1
docs: correct ignore options in readme
2026-09-10 15:06:18 +02:00
apk 0a52d25feb Merge remote-tracking branch 'origin/master' into patch-1 2026-09-10 15:02:37 +02:00
apk e27a6c1957 docs(npm): ignore examples that actually match
`--ignore-pattern` has no `-i` short flag (only `--ignore` does), and
ignore globs are matched against the whole file path, so bare directory
names such as `node_modules` in the config example did not exclude
anything (verified with 5.2.0: 3 of 4 files still scanned). Use the
`**/dir/**` form the other docs already use and say so in the table.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-10 15:02:37 +02:00
Andrey Kucherenko a72652bf9e Merge pull request #1040 from kucherenko/docs/pypi-description
docs(pypi): describe what jscpd finds and how it installs
2026-09-10 14:58:03 +02:00
apk 95cb9d7e99 docs(pypi): describe what jscpd finds and how it installs
The PyPI long description said 150+ formats and little else. It now
matches the README: 224 formats, 15 reporters, exact / Type-2 / Type-3
clones, baseline and threshold gating, the pip/pipx/uv install paths,
the Node-free pre-commit hook and the repo-hosted hook, plus links.
Metadata of a published release cannot change, so this lands with the
next upload.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-10 14:54:44 +02:00
Andrey Kucherenko b495c0e362 Merge pull request #1039 from kucherenko/feat/pypi-wheels
feat(release): publish jscpd platform wheels to PyPI
2026-09-10 14:38:08 +02:00
apk 17c4b6db1c docs(pypi): show wheel script usage from the repository root
Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-10 14:35:11 +02:00
apk a2da469f5e feat(pre-commit): install the repo-hosted hook from PyPI, not npm
The `jscpd` hook in .pre-commit-hooks.yaml switches from `language: node`
to `language: python`: pre-commit now `pip install`s this repository,
whose new root pyproject.toml is an empty project that depends on the
`jscpd` wheel of the same version, so the hook needs no Node.js on the
machine. pre-commit is itself a Python tool, so Python is always there.

The root package.json existed only for the node hook and is removed;
sync-version.mjs now keeps pyproject.toml's version and `jscpd==` pin
in step with rust/package.json instead (mapped to PEP 440 the same way
build-pypi-wheels.py does) and verifies both before a release.

Verified with `pre-commit try-repo . jscpd` against locally built
5.2.0 wheels. Tags up to v5.2.0 keep the node hook, since a hook is
read from the pinned rev.

Claude-Session: https://claude.ai/code/session_01WztdFKZAwW9b51iJe9zhtV
2026-09-10 14:26:37 +02:00