2015 Commits

Author SHA1 Message Date
apk ff0cd0196f fix(skills): codebase-refactoring — don't use --reporters ai with --dashboard
--dashboard's ai reporter prints the same one-line health summary as
--health --reporters ai, not the per-file breakdown (that only renders
on console, the default) — so the skill's opening example was
technically valid but misleading about what it actually returns.

Use --health --reporters ai for the compact prioritization signal, and
point at plain --dashboard (or --dashboard --reporters json) for the
full picture.
2026-09-18 19:48:49 +02:00
Andrey Kucherenko 2e732622e5 Merge pull request #1078 from kucherenko/feat/codebase-refactoring-skill
feat(skills): codebase-refactoring — the health-score cleanup workflow
2026-09-18 19:06:34 +02:00
apk 45b6d8ce53 feat(skills): codebase-refactoring — the health-score cleanup workflow
A third installable agent skill, broader than dry-refactoring's
duplication-only scope: find and fix duplicated code (delegates to
dry-refactoring), then find and remove or refactor dead code
(--dead-code), then find and simplify the largest/most complex files
(--complexity) — prioritized from --health/--dashboard's three
sub-scores and re-measured at the end to confirm the pass helped.

Also fills a gap the new skill's own cross-reference exposed: the
jscpd skill's option table never mentioned --dead-code at all despite
listing --complexity and --dashboard/--health. Added it and its
related flags, and cross-linked both new-workflow directions from the
jscpd skill's closing section. README.md and docs/ai-ready.md's skill
registries updated to list all three.
2026-09-18 18:57:13 +02:00
apk 9ce7b6b39c chore(ci): retire basta-release.yml, basta releases from its own repo
basta now has a proper release: github.com/kucherenko/basta got its
own signed GitHub release with binaries, checksums, provenance and
sigstore signatures (mirroring what this repo's own release does),
built on the release surface its repo was already designed for (a
submodule pin to this workspace + its own v*-tag-triggered workflow)
but had never actually used.

That makes this repo's basta-release.yml redundant — it duplicated
that same publish logic and never created a release with artifacts
anywhere. Remove it; jscpd's own crates-publish.yml is unaffected,
since its basta-backfill logic (publishing basta as a dependency when
a fresh jscpd release needs it on crates.io first) is self-contained
and doesn't depend on basta-release.yml existing.
2026-09-18 17:48:24 +02:00
apk 94fb6c3ef0 fix(ci): don't recreate the jscpd GitHub release over a basta-only bump
basta is a workspace member but releases on its own basta-v* tag; a
version bump to it alone still made the crates_needs_publish loop
over crates/*/Cargo.toml return true, which made create-rust-github-
release try to recreate the still-current jscpd release and fail with
"Release.tag_name already exists" (seen pushing the basta 0.1.2 bump
to master before tagging it).

Track a narrower cpd_needs_publish signal (true only when the jscpd
crate itself is unpublished) and gate the GitHub release on that
instead of the broad crates_needs_publish, which stays as-is for
gating the crates.io publish job — that one is supposed to backfill
basta too.
2026-09-18 16:07:56 +02:00
apk de66dff9a9 chore(basta): bump to 0.1.2
Single-file component support (Vue/Svelte/Astro), bundler alias
resolution (vite.config, svelte.config, tsconfig paths) and monorepo
workspace package-name resolution landed on master after basta 0.1.1
was tagged (#1062); the published crate and npm packages don't have
them yet.
basta-v0.1.2
2026-09-18 15:28:02 +02:00
apk 92576864e2 release: cpd-v5.3.0 v5 v5.3.0 2026-09-18 14:39:05 +02:00
Andrey Kucherenko a8219b3cb3 Merge pull request #1076 from kucherenko/fix/health-dashboard-review-findings
fix(health,cli): pre-release review findings for --dashboard/--health
2026-09-18 14:31:45 +02:00
apk 5fd9c6967e fix: address Copilot review of #1076
- Console tables (project/duplication-by-format/complexity/dead-code) and
  the "largest:" summary line interpolated file paths and format names
  raw: the Markdown escaping fix in the previous commit only covered
  Markdown, not console. A newline in a path or a custom --formats-exts
  name forged an extra table row with none of the other columns' data.
  Sanitized at table()'s one choke point and at the "largest:" line.
- format_list() (the --health console/ai duplication line) had the same
  gap for the formats it lists, independent of the console table fix.
- --complexity --fail-on-empty returned before the reporter loop ran, so
  -r json never wrote a report; the clone-detection and dead-code paths
  write reports first and apply the gate to the exit code afterward.
  --complexity now does the same.
- dead_code::config()'s early Ok(None) for an unsupported --format
  returned before parsing --dead-code-categories or clamping
  --min-confidence, so --dashboard --format css --dead-code-categories
  bogus silently succeeded despite the comment above it saying that must
  stay a refusal. Moved both checks before the format check.

8 new tests.
2026-09-18 14:21:55 +02:00
apk 0d9a85176d fix(health,cli): pre-release review findings for --dashboard/--health
Two independent reviews of the health/dashboard work landing since v5.2.1,
before cutting 5.3.0.

Scoring:
- Markup lines stayed in the duplication denominator while excluded from
  the numerator, so adding one unique, un-duplicated HTML file could swing
  a project from grade D to grade B. The denominator now excludes markup
  too, and an all-markup project skips the dimension entirely instead of
  scoring ~75 from the small-project prior alone.
- is_markup() matched file extensions (plantuml, puml, tpl) instead of the
  tokenizer's actual format names (plant-uml, smarty), so those formats
  were never really excluded.
- A health-input weight of 1e308 overflowed the weighted geometric mean to
  NaN/100 with exit 0; validate() now rejects non-finite or unreasonably
  large weights.
- External metric ids could duplicate each other or collide with a
  built-in dimension id.

Output safety:
- Markdown output (dimension ids, format names, file paths, dead-code
  finding names) was interpolated unescaped: a hostile path or
  --health-input id could break a table or inject raw HTML/a heading
  (GitHub renders HTML embedded in Markdown), or split console/ai output
  via an embedded newline.

CLI:
- A config file with a known field of the wrong JSON type (e.g. "entry":
  "src/index.js" where an array is expected) discarded the entire config,
  --threshold included, instead of just that field — on any
  auto-discovered .jscpd.json, no new flags required.
- --dashboard --format css hard-errored instead of dropping the dead-code
  section.
- --dead-code/--complexity didn't conflict with --mcp.
- --complexity silently ignored --threshold, --exit-code,
  --fail-on-empty, the baseline family, --history and --kind.
- --dead-code --min-confidence 200 silently reported nothing instead of
  clamping to 100, unlike the standalone basta binary.
- --dashboard --max-lines let the dead-code section silently cover files
  the other sections skipped (basta needs whole files for its import
  graph).
- duplication.percentage/deadCode.percentage in dashboard JSON carried raw
  float division noise instead of the two decimals the console already
  shows.

26 new tests. Full default-path parity re-verified against the released
5.2.1 binary (13 reporters, option matrix) — byte-identical except timing.
2026-09-18 14:03:27 +02:00
Andrey Kucherenko 0e4e58da7d Merge pull request #1075 from kucherenko/chore/oxc-0.150
chore(deps): bump oxc crates to 0.150 in basta
2026-09-18 12:58:40 +02:00
apk d8ecec4f90 chore(deps): bump oxc crates to 0.150 in basta
Dependabot opened five separate PRs bumping oxc_syntax, oxc_parser,
oxc_allocator, oxc_semantic and oxc_ast individually (#1066-#1070), but the
oxc crates share AST/Visitor trait definitions across the family and only
compile against matching versions of each other — bumping one alone breaks
the build (Visit trait signature mismatches, missing .span() methods) on
every CI job. oxc_ast_visit and oxc_span also needed the same bump, but
dependabot had not opened PRs for those two.

Bumps all seven together instead, and fixes the one real API change
between 0.147 and 0.150: ParserReturn::panicked was renamed to
::fatal_error (same meaning, confirmed against oxc_parser's source).

cpd-tokenizer's own oxc_parser/oxc_ast pins stay at 0.147, coexisting
fine as a separate resolution in the same Cargo.lock (no basta/
cpd-tokenizer API overlap), so this stays scoped to basta.
2026-09-18 12:54:08 +02:00
Andrey Kucherenko 8462b2042f Merge pull request #1064 from kucherenko/feat/complexity-kind-dashboard
feat(cli): --complexity, --kind, --dashboard and --health
2026-09-18 12:15:51 +02:00
apk fe966f6ab0 fix(cli): normalize summary-entry paths to forward slashes on Windows
display_source_path() returned OS-native separators via to_string_lossy(),
so a summary path (dashboard's complexity/duplication sections) read
"src\route.js" on Windows instead of "src/route.js" everywhere else — the
convention this codebase already follows in mcp.rs, ai.rs and
summary.rs's parent_dir(). Fixes the Windows CI failure in
dashboard_has_the_health_badge_and_a_json_report, pre-existing before
this branch's dashboard/health work.
2026-09-18 12:01:06 +02:00
apk 45d70f49b9 feat(cli): beautify health badge, weigh duplication by kind, format reports
- Console health badge: reverse-video grade chip, eighth-block gauges per
  dimension, one row per sub-score with what it was measured from.
- markdown/html reporters for --dashboard and --health, alongside the
  existing console/json/badge; the health SVG badge moves into badge.rs.
- --dashboard's Duplication section lists a breakdown by format instead of
  by file, sourced from the same jscpd statistics as everywhere else.
- Duplication no longer counts markup, stylesheet and template clones
  (HTML, CSS, Handlebars, ...): a repeating style rule is not the
  maintenance problem repeating programming logic is. The exclusion
  follows the clone's own format, not the file it lives in, so a .svelte
  or .vue file's style/template block is left out even though the
  component itself is full weight.
- The duplication line and format-breakdown table now name what they
  actually measured and, only for categories the project has files in,
  what was left out (e.g. "5.4% in typescript (no text)") instead of a
  fixed disclaimer.
2026-09-18 11:48:45 +02:00
apk 88ece028b6 feat(cli): project health score, --health, dashboard JSON
One 0-100 score with an A-E grade, shown on top of --dashboard and alone
with --health.

- Dimensions are shares of the code lines, so size cancels out:
  duplication (jscpd's percentage over code files), dead code, and the
  share of code in complex files (complexity 50+). Prose and data files
  are not counted.
- Each share is scored on a half-life curve (100 at zero, 50 at one
  half-life), calibrated on 42 open-source projects so the median project
  scores 75 per dimension; shares are mixed with a 2000-line prior so one
  finding does not sink a small project.
- Sub-scores combine as a weighted geometric mean. A dimension that
  cannot be measured is left out and named, and dead code weighs as much
  as the share of the code it could read.
- --health-input FILE and the health config object add metrics from
  other tools (a ready score, or a value with a half-life) and tune the
  built-in half-lives and weights.
- Reporters: console badge, ai one-liner, json (jscpd-health.json,
  jscpd-dashboard.json with every dashboard section) and badge
  (jscpd-health-badge.svg). Exit gates apply as in the dashboard.

CpdClone::matched_lines moves to cpd-core so statistics and health count
duplicated lines the same way.
2026-09-18 09:23:58 +02:00
Andrey Kucherenko 31cb7c4e4a Merge pull request #1071 from kucherenko/dependabot/github_actions/docker/build-push-action-7.4.0
chore(deps): bump docker/build-push-action from 7.3.0 to 7.4.0
2026-09-18 08:14:49 +02:00
Andrey Kucherenko 2676303257 Merge pull request #1072 from kucherenko/dependabot/github_actions/taiki-e/install-action-2.87.13
chore(deps): bump taiki-e/install-action from 2.87.8 to 2.87.13
2026-09-18 08:14:31 +02:00
Andrey Kucherenko 68af9e7eef Merge pull request #1073 from kucherenko/dependabot/github_actions/docker/setup-buildx-action-4.4.0
chore(deps): bump docker/setup-buildx-action from 4.3.0 to 4.4.0
2026-09-18 08:13:46 +02:00
Andrey Kucherenko 2138416ac5 Merge pull request #1074 from kucherenko/dependabot/github_actions/docker/setup-qemu-action-4.4.0
chore(deps): bump docker/setup-qemu-action from 4.3.0 to 4.4.0
2026-09-18 08:13:25 +02:00
apk 527b0bca30 fix: address Copilot review of #1064
- --dashboard applies the exit gates of a clone run (--threshold,
  --exit-code, --fail-on-empty) instead of always returning 0; the
  baseline family warns and --fail-on-new-clones is refused, since no
  clone report is written.
- A refused dead-code configuration (bad --dead-code-categories, a
  --format no analyzer supports) now exits 1 as in --dead-code, rather
  than printing a dashboard with an empty dead-code section.
- --workers is a budget for the whole dashboard run: the two concurrent
  scans get half each, so neither the thread count nor the 64 MiB
  tokenizer stacks double. --workers 1 runs them sequentially.
- --complexity scans with similarity disabled, so --similarity no longer
  pays for function extraction it cannot use.
- --complexity honours a summaryBy from the config file, not only the
  --summary-by flag (Options::summary_by_set).
- reStructuredText joins the prose formats at complexity 0.
2026-09-18 08:11:44 +02:00
Andrey Kucherenko a7d98e69c1 Merge pull request #1065 from Dev-next-gen/fix/plain-text-no-comments
fix(tokenizer): plain text, logs and CSV have no comments
2026-09-18 08:01:54 +02:00
dependabot[bot] 5ebc9b274d chore(deps): bump docker/setup-qemu-action from 4.3.0 to 4.4.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/1f40c72289eff860ee54a304f1438e3cff362e0a...99012661954931238ded8c8b007157a8430204e1)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-18 03:07:43 +00:00
dependabot[bot] f65e1fca58 chore(deps): bump docker/setup-buildx-action from 4.3.0 to 4.4.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/37fe631027851001ddb9b187196cc803df7f5f0e...594f3bf4285d9ea8dc53c9a0c9c4092420091003)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-18 03:06:19 +00:00
dependabot[bot] aa45555388 chore(deps): bump taiki-e/install-action from 2.87.8 to 2.87.13
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.87.8 to 2.87.13.
- [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/d438492cf8a250514fa2d34b30bc3c0dc37c65ff...26e9283f268b880168bdbd2c545dfcd60ec2c6ab)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-18 03:06:13 +00:00
dependabot[bot] 847b2890f0 chore(deps): bump docker/build-push-action from 7.3.0 to 7.4.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.3.0 to 7.4.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/53b7df96c91f9c12dcc8a07bcb9ccacbed38856a...c3c9e263c25d99ce0380d002d59b67737d91b0dc)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-18 03:06:04 +00:00
leoca dfd4c8a94a fix(tokenizer): plain text, logs and CSV have no comments
`comment_style` names no arm for txt, log or csv, so they fall to the
`_ => CommentStyle::CStyle` fallback, the same way markdown did before
11553327. A glob such as `docs/**` in a text file opens a block comment
that never closes, and every later clone in that file goes unreported;
`https://` turns the rest of its line into a comment.

With jscpd 5.2.1, two .txt files sharing a paragraph report no clone once
one of them holds `--ignore docs/**` above it (412 tokens, 0 clones);
with this change the clone is reported (130 tokens, 1 clone).
2026-09-17 19:36:39 +02:00
apk 57c911f072 docs: use jscpd, not cpd, in command examples 2026-09-17 18:37:20 +02:00
apk 528cc70f0c docs: --complexity, --kind and --dashboard in the READMEs and AI guide 2026-09-17 18:36:48 +02:00
apk ee873ac555 feat(cli): --dashboard with duplication, complexity and dead code
--dashboard prints one screen: project size and largest formats,
duplication with clones per kind and the most duplicated files, total and
mean complexity with the most complex files, and dead code by category
with the largest findings (JavaScript, TypeScript, Python). The dead-code
scan runs beside clone detection; --summary-top sets the rows per list.

Data formats (JSON, YAML, TOML, CSV, ...) join markdown at complexity 0,
so a lock file's version ranges no longer top the complexity ranking.

Docs for --kind, --complexity and --dashboard; fixtures/dashboard-demo,
plus --kind and --complexity sections in the type3 and summary demos.
2026-09-17 18:15:45 +02:00
apk 7a8a0fa98a feat(cli): --kind filter and --complexity mode
--kind exact,renamed,similar,gap,ast keeps only the clones of those kinds
(config key "kind"). The filter runs before statistics, so percentages
describe what is reported. An unknown kind is an error; a kind whose
detector is off (ast without --similarity, ...) is a warning, and
detection is never switched on implicitly.

--complexity walks and tokenizes like a normal run, skips clone
detection, and prints the --summary tables ranked by complexity without
the DUP% column (console, ai; json writes jscpd-complexity.json).

Markdown prose now has complexity 0: "if" in a README is a word.
2026-09-17 17:57:43 +02:00
Andrey Kucherenko 5e341ff906 Merge pull request #1063 from kucherenko/refactor/rust-dry-complexity-dead-code
refactor(rust): remove dead code, clones and complexity; read config from cwd only
2026-09-17 16:30:21 +02:00
apk 274d5ae2ca test: keep symlink_layout unix-only
Folding the symlink test assertions into a helper put it between
#[cfg(unix)] and symlink_layout, so the attribute moved to the helper and
std::os::unix::fs::symlink no longer compiled on Windows.
2026-09-17 16:16:21 +02:00
apk 48bb2c69e8 fix(cli): read config from the working directory only, as jscpd 5.2.1 does
b72abf09 also searched the directory of each scanned path. fixtures/ has
a .jscpd.json with "silent": true, so `cd rust && jscpd ../fixtures -r ai`
started printing only the summary line instead of the clones that
`npx jscpd` lists. Config discovery is back to the working directory;
output now matches jscpd 5.2.1 across all reporters.
2026-09-17 15:57:53 +02:00
apk 8712c5e9df refactor(cli): strip a leading ./ with strip_prefix 2026-09-17 15:42:17 +02:00
apk 06481d3cb7 refactor: delete wrappers that only forward
- FinderError was never constructed; run() cannot fail, so its error type
  is Infallible and the documented run(&config).unwrap() keeps compiling.
- tokenize_sfc_block_inner and tokenize_to_detection_inner forwarded to
  tokenize_format_to_detection; call it directly.
- extract_line_number was Some(error.line()).
- Timer wrapped Instant; use Instant.
2026-09-17 15:40:19 +02:00
apk 3a68736578 chore: drop unused dependencies
serde, anyhow, thiserror and log from cpd-finder; thiserror from
cpd-reporter; xxhash-rust and dev serde_json from cpd-tokenizer; anyhow,
log and env_logger from cpd. None of them is referenced in code.
2026-09-17 15:37:40 +02:00
apk 73f9c8a09d refactor(tokenizer): read a line through a cursor
`tokenize_line_content`, which cuts every line of every non-JavaScript file
into tokens, was a 218-line loop with a cyclomatic complexity of 49. Each of
its nine token classes repeated the same index, column and byte-offset
bookkeeping and its own ignore-region check.

A `LineCursor` now owns the position, the column and the ignore-region kind,
and each class is one call on it: `take`, `take_while`, `take_string`,
`take_word` or `take_rest`. The loop that dispatches between them is 37 lines
with a complexity of 15. The separate Lua `--[[` branch goes: the line comment
check right after it already matched Lua's `--` and produced the same token.
`comment_kind` is folded into `LineCursor::kind`.

Checked against the previous implementation, copied beside the new one for a
one-off differential test: every text file in `fixtures/` and `rust/` — 839
of them — under all seven comment styles, plus edge cases for unterminated
and escaped strings, block comments across lines, ignore regions, and
multi-byte and non-breaking-space input, produced 5,039,506 tokens identical
in kind, text, line, column and offset. On ten trending projects (7,181 files,
11.2 million tokens) the JSON report is byte-identical, and fifteen
alternating single-worker runs put old and new within noise of each other
(median 4.04 s old, 4.01 s new).
2026-09-17 15:27:33 +02:00
apk 77df7acf37 refactor(cli): split main into phases
`main` was the most complex function in the workspace: one 437-line body
with a cyclomatic complexity of 84 and thirteen `process::exit` calls,
running config loading, flag validation, detection, baselines, blame,
history, reporter planning, output and exit codes in sequence.

It is now a short dispatch over functions that each do one of those
things: `load_options`, `check_format_names`, `validate_baseline_flags`,
`scan_paths`, `run_config`, `detect_and_report`, `display_paths`,
`apply_baseline`, `blame`, `history`, `plan_reporters`, `run_reporters`,
`print_time_and_tips` and `exit_status`. A run that ends early returns
`Err(Exit(code))`, and only `main` exits the process. The largest function
left has a complexity of 15.

Reporter planning is a pure function now, and has tests. One of them pins
the rule that took care to carry over: a `--reporters silent` run prints no
timing line, but adding `--threshold` adds a reporter that prints.

Behaviour is unchanged. The previous and the new binary agree on stdout,
stderr, exit code and written report across 28 invocations covering --list,
--debug, every warning and validation error, a missing path and config,
--silent and --threshold in combination, unknown reporters, --exit-code
with and without clones, empty scans with and without --fail-on-empty,
--absolute, --summary, --baseline with --update-baseline, and --blame; and
the fixtures corpus reports in every mode are byte-identical.
2026-09-17 15:19:32 +02:00
apk eec83f853e refactor: fold accidental code clones in the Rust workspace
`jscpd rust/` reported 48 clones. This folds every one that was repetition
rather than a deliberate copy, leaving 34 across `rust/` and 14 in
`rust/crates`, all of the kinds the #1053 pass kept on purpose: fixture
corpora, `use` blocks, the TypeScript file and its JavaScript twin,
`impl Reporter` headers and the `ts_strip` visitor pair.

- basta's JSONC reader copied a string-skipping loop into both `strip_jsonc`
  and `drop_trailing_commas`. It is now `strip_comments` followed by a comma
  pass over `lang::outside_strings`, the quote-aware scanning the bundler
  config readers already share.
- The JavaScript and Python analyzer tests each built their own module
  index, import lookup, binding-reference lookup and member-read list. They
  live once in `test_scan` as `index_of`, `import`, `binding_reference` and
  `member_reads`.
- `test_scan::TempTree` writes a test's files and removes them when dropped,
  so a failing assertion no longer leaves its directory behind; the script
  and `extends` tests that repeated the setup use it.
- Four tsconfig `paths` tests with one shape are one table-driven test.
- The CLI integration tests share `duplicate_shapes`, `history_stdout` and
  `assert_only_the_real_file_was_scanned`.
- `scripts/publish-npm.sh` repeated a 43-line "publish the platform package,
  then the main packages under --force-main" tail in two branches, and the
  "publish unless already live, or show the dry run" block five times. They
  are `publish_platform_target` and `publish_package`. Run against the old
  script with the build, the registry check and `npm` stubbed, all eleven
  combinations of --target, --all, --force-main, --dry-run, --provenance, a
  failed build and missing platforms print the same lines and exit the same.

Behaviour is unchanged: the fixtures corpus reports in every mode, the
summary, the console output and the dead-code demo are byte-identical.
2026-09-17 15:15:11 +02:00
apk 92649b797f refactor: remove dead code across the Rust workspace
basta reads JavaScript, TypeScript and Python, so `jscpd --dead-code` has no
view of the Rust crates, and the compiler does not warn about an unused `pub`
item. Every crate in the workspace is `publish = false`, though, so a `pub`
item nothing in the workspace uses is dead as surely as a private one. A scan
of every `pub` item's uses across the crates, their tests and the fuzz
targets, checked by hand, found:

- `CloneKind::is_exact`: used nowhere.
- `ConfigResult::has_diagnostics`, `basta::lang::language_of`,
  `TokenizeOptions::normalizes` and `TokenizeOptions::with_code_ignore_patterns`:
  called only by tests written for them. The language test now asks
  `analyzer_for` directly.
- `stats_with_pct` is a reporter test fixture that was compiled into the
  library; it moves inside the `#[cfg(test)]` fixtures module that re-exported it.
- Fields kept alive by `#[allow(dead_code)]`: `MarkdownFence::front_matter`
  and `SfcBlock::tag` are written and never read, and `Options::list` is never
  read because `main` reads `cli.list`.
- `scripts/regression_check.sh` and its 1528-line `tests/golden.json`: a gate
  for the Wave 2/3 migration that nothing runs, against a report schema that
  has since changed.

Behaviour is unchanged: the JSON reports for the fixtures corpus in mild, weak
and strict modes, with Type-2 normalization, and with the complexity summary,
the console output and the dead-code demo output are byte-identical before and
after.
2026-09-17 15:07:47 +02:00
Andrey Kucherenko b08c9f2219 Merge pull request #1062 from kucherenko/feat/basta-bundlers-and-complexity
feat: basta reads bundler and workspace imports; summary complexity counts per function
2026-09-17 13:54:39 +02:00
apk f516710e28 fix(dead-code): address review on globs, aliases and component markup
Copilot's review of #1062 raised seven points; each checked out and each is
fixed with a test that fails without it.

- A glob keeps its pattern. `import.meta.glob('./locales/*.js')` and
  import(`./pages/${name}.vue`) used to keep every file under the directory
  alive; they now reach what the pattern matches, `*` staying within one
  segment as it does for the bundler. On Firefly an image glob,
  '../../**/*.{png,jpg}', had been keeping all of src/ alive.
- Every pattern of an `import.meta.glob([...])` array is kept, not the first.
- A declared alias is not second-guessed: when `@` points at src/ and the file
  is missing there, the `@/` project-root fallback no longer finds a
  different file. A catch-all `*` path claims no prefix and does not count.
- Astro client <script> blocks are read as the modules Astro bundles them
  into, so what they import is used. A script with any attribute but `src` is
  left to the browser, as Astro leaves it.
- import('./x') in markup is found only inside expressions, not in comments
  or plain text.
- A quoted attribute value is an expression only on a directive (`:prop`,
  `@click`, `#slot`, `v-*`); elsewhere only its `{...}` interpolations are,
  so `class="card"` no longer credits a declaration named `card`.
- An object key in markup, `{ label: value }`, is not a read.

Re-measuring on the thirty trending projects also exposed `?raw` imports, as
in `import script from './panzoom.js?raw'`: a bundler query now is stripped
before resolving. On one snapshot of those projects the previous release
reports 1093 unused files and this branch 481, with the share some import
names falling from 83% to 62%; the CHANGELOG now quotes those numbers
instead of ones measured against a development baseline. Components used
only from .mdx content are still reported, and the docs say so.
2026-09-17 13:49:55 +02:00
apk 8b75eb7834 feat(summary): count complexity per function, the way each language branches
The CX column was one path per file plus every branch keyword the token
stream showed, and on the generic tokenizer that list was partly
unreachable: it splits punctuation into single characters, so the && and ||
entries never matched outside JavaScript and ?? counted twice. Measured
against lizard on ten GitHub-trending projects in nine languages, CX ranked
files with a median Spearman correlation of 0.83 and put their size at 0.75
of lizard's.

- Adjacent punctuation is joined before matching, so short-circuit
  operators count in every language.
- Per-language rules: Rust match arms count by their =>, less one per match
  since N arms are N paths; Swift adds guard and Go select. Where ? also
  marks an optional (TypeScript, JavaScript, Swift, Kotlin, Groovy, C#) it
  counts only as a ternary, told apart from String? and x?.y by being
  written in the open; ?: is Elvis only in Kotlin and Groovy.
- One path per function: by declaring keyword (def, fn, func, function, fun,
  and => for arrow functions), and in C, C++, Java, Objective-C and C# by the
  head(args) { shape told apart from if (...) {. Languages with neither keep
  the per-file baseline.
- The body of a """ or ''' string no longer counts. The line-based tokenizer
  leaves its quotes in adjacent literals, and an odd number of triple quotes
  in such a run opens or closes the string, which handles the PEP 257
  opener """Summary. that a pair-of-tokens check read backwards. This is
  limited to languages where """ is a delimiter: where a doubled quote is an
  escape (C#, VB.NET, SQL, Pascal) it swallowed a 413-branch C# file.
- A keyword-shaped word the file binds as a name - case = 3, x.case,
  f(case, when) - is not a branch in that file.

On the same ten projects the median correlation rose to 0.92 and the ratio
to 1.00, and every project improved. Where CX and lizard still differ they
differ on purpose: lizard counts a match once whatever its arms, and does
not count ??.

Detection, tokenization and clone output are untouched: the fixtures corpus
reports the same 248 clones. fixtures/summary-demo has one file per language
whose complexity can be counted by hand, and every CX value matches it.
2026-09-17 12:36:55 +02:00
apk 2baf2890d4 feat(dead-code): read components, bundler aliases, globs and workspaces
basta reported most of a real Vue, Svelte or Astro project as dead: on
thirty GitHub-trending projects in those three ecosystems it flagged 1572
unused files, and some import in the tree contradicted 78% of them. The
misses were almost never in the analysis itself; they were imports only the
project's own build knows how to resolve. This reads them the way the build
does.

- Single-file components are read whole. The script of a .vue, .svelte or
  .astro file is masked in place so positions stay real, and the markup is
  scanned for the names it reads: <Foo />, kebab-case spellings, {{ }}
  interpolations, Svelte actions, attribute expressions, and a literal
  import() written in markup.
- Aliases come from vite.config.* resolve.alias and svelte.config.* kit.alias
  as well as tsconfig paths, and SvelteKit's $lib is known without the
  generated .svelte-kit/tsconfig.json no repository commits. The project's
  own declaration now wins over the ~/ @/ project-root fallback.
- A computed import whose static head names a directory,
  import(`./pages/${name}.vue`), and import.meta.glob('./locales/*.js') are
  expanded into one namespace edge per file there, the way a bundler hands
  back whole module objects. Such an import no longer marks its module as
  dynamic access, so the module's own findings keep their confidence.
- In a monorepo a package name resolves through that package's package.json,
  exports subpath by subpath with source conditions preferred over ./dist,
  anywhere in the workspace pnpm-workspace.yaml, lerna.json, turbo.json or a
  workspaces field marks out.
- A path literal in a build config resolves against the project root, and
  Nitro's api/, routes/ and middleware/ are file-system routes.

On the same thirty projects reported unused files fell from 1572 to 468.

fixtures/dead-code-demo gains components/, bundler/ and monorepo/. On master
the bundler demo reports five files as certainly dead, four of them wrongly;
the monorepo demo reports badge.ts, which the app imports by package name.
Both now report only what is dead. The quote-aware byte scanning the markup
and config readers share lives in one place, lang::Quotes and
lang::outside_strings, so rust/crates keeps master's 25 clones.
2026-09-17 12:36:34 +02:00
Andrey Kucherenko b330df87c1 Merge pull request #1061 from kucherenko/feat/dead-code-basta
feat(dead-code): find code nothing runs, via the new basta engine
2026-09-16 13:32:54 +02:00
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