Skip to main content

Dead code & mechanical fixes

Dead code is the cheapest debt to eliminate — if you can trust the analysis. ovecc combines export usage (from real module resolution) with entry-point reachability, reports what it finds with file:line, and can apply the mechanical part of the cleanup itself, safely.

Detect

$ ovecc deadcode
Dead code: 8 unused export(s), 1 unused file(s), 1 unused dependency(ies), 1 unlisted dependency(ies)

[Medium] Unlisted dependency: dayjs
src/utils/helpers.ts:2 (bare import of 'dayjs' with no manifest declaration)

[Low] Unused dependency: left-pad
package.json:1 (dependencies)

[Low] Unused export: formatEuros in src/utils/helpers.ts
src/utils/helpers.ts:10

[Low] Unused file: src/legacy/old-report.ts
src/legacy/old-report.ts:1

[Low] Unused type export: Currency in src/utils/helpers.ts
src/utils/helpers.ts:4 (type-only)
...

Four distinct signals:

FindingRuleMeaning
Unused exportunused-exportExported, but imported by no reachable module
Unused type exportunused-typeType-only export (interface/type alias) never imported — tagged separately because removal has zero runtime effect
Unused fileunused-fileReachable from no entry point and imported by nothing
Unused dependencyunused-dependency (opt-in)Declared in the manifest, never imported (detect_unused_deps = true under [index]; off by default because config-only usages cause false positives)
Phantom dependencyunlisted-dependencyImported but missing from every package.json — works only via hoisting or a transitive install

When nothing is flagged, the report states its coverage (entry points found, JS/TS files analyzed), so a clean bill is distinguishable from an analysis that never ran.

Fix — dry-run first, always

ovecc fix plans the mechanical remediation. It is dry-run by default and prints exactly what would change:

$ ovecc fix
Fix plan: 10 change(s), 1 skipped — dry-run (pass --apply to write)

[planned] remove_unused_dependency — package.json:1 (unused-dependency)
remove "left-pad" from dependencies

[skipped] declare_dependency — package.json:2 (unlisted-dependency)
no lockfile version found for 'dayjs' — declare it manually

[planned] remove_unused_file — src/legacy/old-report.ts:1 (unused-file)
delete file (no entry point reaches it)

[planned] remove_unused_export — src/utils/helpers.ts:10 (unused-export)
- export function formatEuros(cents: number): string {
+ function formatEuros(cents: number): string {

[planned] remove_unused_export — src/core/db.ts:3 (unused-type)
- export interface Row {
+ interface Row {
...

What fix will do:

  • delete unreachable files;
  • drop the export keyword on unused exports (and prune names from re-export lists) — the symbol stays if it's used internally;
  • remove unused manifest dependencies;
  • declare phantom dependencies, pinned to the version the lockfile already resolves;
  • delete stale ovecc-ignore comments.

What it will never do: anything requiring judgement. Default exports, architectural smells, and security findings are never touched. Note the honest [skipped] above — dayjs wasn't in the lockfile, so instead of guessing a version, fix tells you to declare it manually.

Apply

$ ovecc fix --apply
Fix applied: 12 change(s), 1 skipped — applied

Safety properties of --apply:

  • Every edit re-verifies the file against the index first and skips stale entries with a reason — if the file changed since the last index, ovecc won't blind-edit it.
  • Edits are format-preserving; JSON files (like package.json) are re-validated after editing.
  • Scope it with --rule: ovecc fix --apply --rule unused-export.

Verify the payoff

Re-index and check the trend:

$ ovecc index . && ovecc history unused_exports
History: unused_exports over 3 snapshot(s) 8 -> 0 (-8)
▇█▁
2026-07-11T09:35:24… d9416a8 8
2026-07-11T09:35:25… 1a67876 10 +2
2026-07-11T09:36:59… e8fa485 0 -10

Then keep it clean: ovecc deadcode --changed-since origin/main in CI reports only dead code touching the current diff, and the baseline ratchet stops the backlog from growing.