Files
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.
2026-09-18 15:28:02 +02:00
..
2026-09-18 15:28:02 +02:00

basta

Find dead code: unused files, exports, declarations and imports across JavaScript, TypeScript, Vue, Svelte, Astro and Python.

basta builds the import graph from a project's entry points, walks it, and reports what it never reaches. It ships as its own basta command and inside jscpd as jscpd --dead-code.

basta src
Unused files (1)
 - src/legacy-export.ts  certain 95%  11 lines

Unused exports (1)
 - function src/invoice.ts:21:17 renderReceipt  high 85%  3 lines

Unused symbols (1)
 - function src/invoice.ts:29:10 describeTotal  certain 90%  3 lines

Unused imports (1)
 - import src/invoice.ts:2:10 roundToCents  certain 100%  1 lines

Found 4 dead code findings in 4 files (31.6% of 57 lines).
Done in 12ms

What makes it different

It is a traversal, not a reference count. A helper whose only caller is itself dead is reported too. That cascade is most of what a reference-counting linter misses.

It tells you how sure it is. Static analysis of JavaScript and Python cannot be certain, so every finding carries a score from 0 to 100 and the reasons it is not higher — a file that calls eval or getattr, an unrecognised decorator, a wildcard re-export, a name that shows up in a string literal. --min-confidence sets the floor; the default is 60.

It reads components, not just scripts. A .vue, .svelte or .astro file is a module wrapped in markup, and the markup is where a component's imports are actually used. basta reads both halves, so <Foo /> counts as the use of import Foo from './Foo.vue' — kebab-case spellings, {{ … }} interpolations, Svelte actions and attribute expressions included. A tool that stops at </script> does not merely miss findings here; it reports every component import in the project as dead.

It knows the conventions. A package's __init__.py re-exports are its API, not unused imports. from __future__ import annotations is a directive, not a binding. # noqa: F401 and PEP 484's import x as x mean a deliberate re-export. A package.json pointing at dist/index.js means src/index.ts. CommonJS counts: require('./x'), module.exports = { a } and a literal import('./x') are edges like any import. A shell script or CI workflow that names a source file keeps it alive.

Usage

basta [PATHS]...

  --categories <LIST>        unused-file, unused-export, unused-symbol,
                             unused-import, unused-member, or `all`
  --min-confidence <N>       drop findings below this score (default: 60)
  --entry <GLOB>             treat matching files as entry points (repeatable)
  --ignore <GLOB>            skip matching files (repeatable)
  --include-tests            report dead code inside test files
  --include-entry-exports    report exports of entry points
  --min-lines <N>            only report declarations this many lines or longer
  -r, --reporters <LIST>     console, json, sarif, html, markdown, csv, xml,
                             codeclimate, openmetrics, badge, xcode, ai, silent
  -o, --output <DIR>         where file reporters write (default: report)
  --threshold <PERCENT>      fail when dead code exceeds this share
  --exit-code <CODE>         exit with this code when anything is found
  --format <FORMAT>          restrict the scan (repeatable)
  --list                     print the formats basta analyzes

Entry points

Every finding is the answer to nothing reaches this, so the entry points are the whole basis of the result. basta finds them from package.json (main, module, bin, exports, files, scripts), pyproject.toml ([project.scripts] and entry-point tables), scripts in the tree (*.sh, CI workflows, Makefiles, Dockerfiles) that name a source file, and conventions: src/index.ts, __main__.py, manage.py, framework routes under pages/ and app/, *.config.ts, .d.ts declarations, shebangs, if __name__ == "__main__", every __init__.py, and the file-system routes of the component frameworks — pages/, layouts/, src/routes/, app.vue, error.vue.

When a project does something none of that covers, say so once:

basta src --entry 'src/handlers/**' --entry 'scripts/*.ts'

It reads the project's own build. resolve.alias from vite.config.*, kit.alias and $lib from SvelteKit, paths from tsconfig.json, and — in a monorepo — every workspace package's name, so @acme/ui/date reaches the file that package's exports names. A specifier computed over a directory (import(`./locales/${l}.json`), import.meta.glob('./lang/**/*.ts')) is expanded to the files its pattern matches, the way a bundler expands it; a literal import('./x.svelte') in a component's markup and the imports of an Astro client <script> are edges like any other.

A framework that loads whole directories is read from its own config rather than guessed at: a nuxt.config.ts beside the tree means components/, composables/, utils/, middleware/, plugins/, modules/ and server/ are reached by the framework, with no file importing them. When no config declares them, ~/x, @/x and ~~/x fall back to the project root, which is what they mean in Nuxt — its alias table is generated into .nuxt/ and never scanned.

Adding a language

One file under src/lang/ implementing Analyzer, and one line in the ANALYZERS registry. The trait is the only place a language is allowed to be special: it parses one file into declarations, imports and references, resolves that language's specifiers against the index of scanned modules, and declares its entry-point conventions, manifests and path traits. The walker, the graph, the confidence model, the classifier, the reporters and both CLIs are language-agnostic and pick the new language up from the registry.

docs/basta-extending.md is the walk-through: how a run works, the contract an analyzer has to honour (each rule there was a real false positive once), a worked skeleton, and how to prove a new language right against real projects.

License

MIT