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.
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.
`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.
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.
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.
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.
`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@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
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
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