Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.fallow.tools/llms.txt

Use this file to discover all available pages before exploring further.

fallow dead-code performs dead code analysis on your TypeScript project. It builds a module graph from your and reports anything that isn’t reachable. Finding dead code requires building a complete module graph. Fallow does this deterministically in milliseconds, giving agents, developers, and CI pipelines the same reliable results.
fallow dead-code

Issue types

Fallow detects 16 types of dead code:
Issue typeDescription
Unused filesFiles not reachable from any entry point
Unused exportsExported symbols never imported elsewhere
Unused typesType aliases and interfaces never referenced
Unused dependenciesPackages in dependencies never imported or used as script binaries
Unused devDependenciesPackages in devDependencies never imported or used as script binaries
Unused optionalDependenciesPackages in optionalDependencies never imported or used as script binaries
Unused enum membersEnum values never referenced
Unused class membersClass methods and properties never referenced outside their class, with inheritance tracking, decorator exclusion, and framework lifecycle allowlists
Unresolved importsImport specifiers that cannot be resolved
Unlisted dependenciesImported packages missing from package.json
Duplicate exportsSame symbol exported from multiple modules
Circular dependenciesModules that import each other directly or transitively
Boundary violationsImports that cross user-defined architecture zone boundaries
Type-only dependenciesProduction dependencies only imported via import type (should be devDependencies)
Test-only dependenciesProduction dependencies only imported by test files (should be devDependencies)
Stale suppressionsfallow-ignore comments or @expected-unused JSDoc tags that no longer match any issue
Most projects find 50+ unused exports on first run. Start with --unused-exports for the most impactful cleanup.
Here’s what typical output looks like when fallow finds multiple issue types:
$ fallow dead-code
 Unused files (9)
  scripts/check-db.ts
  src/features/forecasting/hooks/useCashFlowForecast.ts
  src/features/forecasting/hooks/useIncomeForecast.ts
  src/features/forecasting/hooks/useTargetProgress.ts
  src/features/forecasting/hooks/useYearComparison.ts
  ... and 4 more
  Files not imported or referenced by any entry point https://docs.fallow.tools/explanations/dead-code#unused-files

 Unused exports (24)
  test/component-helpers.tsx (5)
    :33 ThemeContext
    :58 ToastContext
    :1 within (re-export)
    :1 waitFor (re-export)
    :1 act (re-export)
  src/server/jobs/queue.ts (3)
    :61 enqueueJobDelayed
    :206 sweepStuckProcessingJobs
    :276 getDeadLetterJobs
  Exported symbols with zero references https://docs.fallow.tools/explanations/dead-code#unused-exports

 401 issues (0.16s)

Filtering by issue type

Report only specific issue types:
fallow dead-code --unused-files
fallow dead-code --unused-exports --unused-types
fallow dead-code --unresolved-imports --unlisted-deps

Output formats

Colored terminal output designed for readability.
fallow dead-code --format human
src/components/Card/index.ts
  unused-export  CardFooter  (line 1)

src/server/jobs/queue.ts
  unused-export  enqueueJobDelayed  (line 61)
  unused-export  sweepStuckProcessingJobs  (line 206)

src/server/jobs/worker.ts
  unused-file  Not reachable from any entry point

Found 401 issues (17 errors, 384 warnings)

Incremental analysis

Only check files changed since a git ref:
fallow dead-code --changed-since main
fallow dead-code --changed-since HEAD~5
This is useful in CI to only report new issues in a pull request.
$ fallow dead-code --changed-since main
 Unused exports (2)
  src/features/savings/hooks/usePotGroups.ts
    :8  usePotGroupTotals
  src/server/jobs/queue.ts
    :276 getDeadLetterJobs
  Exported symbols with zero references https://docs.fallow.tools/explanations/dead-code#unused-exports

 2 issues (0.04s)

Baseline comparison

Adopt fallow incrementally by saving a baseline of existing issues:
# Save current issues as baseline
fallow dead-code --save-baseline

# Only fail on new issues (compared to baseline)
fallow dead-code --baseline

Debugging

Trace why an export is or isn’t considered used:
fallow dead-code --trace src/utils.ts:formatDate
fallow dead-code --trace-file src/utils.ts
fallow dead-code --trace-dependency lodash

How it works

Fallow uses syntactic analysis with scope-aware binding resolution via Oxc. No TypeScript compiler, no type information. That’s what makes it fast. The graph-based approach guarantees completeness regardless of project size.
Fallow works best with projects using isolatedModules: true (required for esbuild, swc, and Vite). oxc_semantic scope analysis detects unused import bindings (imports where the bound name is never read), but legacy tsc-only projects without isolatedModules may still see edge cases with type-only imports.

Script binary analysis

Fallow parses package.json scripts to detect CLI tool usage. This reduces false positives in unused dependency detection. When you have a script like "lint": "eslint src/", fallow recognizes that eslint is a binary provided by the eslint package and marks it as used. How it works:
  • Binary name to package name mapping: Script commands like tsc, vitest, or next are mapped back to their parent packages (typescript, vitest, next). These packages won’t be reported as unused even when they’re never import-ed in source code.
  • --config arguments as entry points: When a script references a config file (e.g., jest --config jest.e2e.config.ts), fallow treats that config file as an entry point. Config files won’t be flagged as unused.
  • File path arguments: Direct file references in scripts (e.g., node scripts/seed.js) are also recognized as entry points.
  • Env wrappers and package manager runners: Commands prefixed with cross-env, npx, pnpx, yarn dlx, or node -r are unwrapped to find the actual tool binary.
{
  "scripts": {
    "build": "tsc && vite build",
    "test": "vitest --config vitest.config.ts",
    "lint": "cross-env NODE_ENV=production eslint src/"
  }
}
In this example, fallow detects typescript, vite, vitest, and eslint as used dependencies, and vitest.config.ts as an entry point.

Infrastructure entry points

Fallow scans infrastructure config files for source file references and treats them as entry points. Worker processes, migration scripts, and other infrastructure-defined files won’t be reported as unused. Supported files:
File typeWhat fallow extracts
Dockerfiles (Dockerfile, Dockerfile.*, *.Dockerfile)RUN node, CMD, ENTRYPOINT, esbuild invocations
ProcfilesProcess definitions (e.g., worker: node dist/worker.js)
fly.toml / fly.*.tomlrelease_command and process definitions
CI pipelines (.gitlab-ci.yml, .github/workflows/*.yml)npx and binary invocations in CI steps
Fallow searches the project root and common subdirectories (config/, docker/, deploy/) for these files.
# Fallow detects scripts/migrate.ts and src/worker.ts as entry points
FROM node:20
RUN node scripts/migrate.ts
CMD ["node", "src/worker.ts"]

Dynamic import resolution

Fallow resolves dynamic imports that use patterns rather than static strings. When you write import(\./locales/$.json`)`, the import target isn’t known at analysis time. Fallow converts these patterns into glob expressions and matches them against discovered files. Supported patterns:
PatternExampleResolved as
Template literalsimport(`./icons/${name}.svg`)./icons/*.svg
String concatenationimport("./routes/" + path)./routes/*
import.meta.globimport.meta.glob("./modules/*.ts")./modules/*.ts
require.contextrequire.context("./themes", true, /\.css$/)./themes/**/*.css
Matched files are marked as reachable in the module graph, so they won’t be reported as unused. Useful for locale files, icon sets, route modules, and other convention-based directory structures.
Dynamic imports with fully runtime-computed paths (e.g., import(userInput)) cannot be resolved statically. Use entry in your config to mark those directories as entry points.

Re-export chain resolution

Fallow resolves export * chains through multiple levels of barrel files with cycle detection.
// utils/math.ts
export const add = (a: number, b: number) => a + b;

// utils/index.ts (barrel)
export * from './math';

// src/index.ts (barrel)
export * from './utils';

// app.ts
import { add } from './src';
In this example, fallow traces the import of add in app.ts through src/index.ts and utils/index.ts back to utils/math.ts. The add export is correctly marked as used across the entire chain. Resolution handles:
  • Multi-level chains: Any depth of export * re-exports is followed until the original declaration is found.
  • Cycle detection: Circular re-export chains (e.g., a re-exports from b, b re-exports from a) are detected and handled gracefully.
  • Mixed re-exports: Named re-exports (export { foo } from './bar') and namespace re-exports (export * from './bar') are both tracked.

Namespace import narrowing

When a file uses import * as ns from './module', fallow narrows which exports are actually consumed by scanning for member accesses (ns.foo, ns.bar) and destructuring patterns (const { foo, bar } = ns) in the importing file.
import * as utils from './utils';

// Only foo and bar are marked as used. baz remains unused
const { foo } = utils;
utils.bar();
Works with static imports, dynamic imports (const mod = await import('./x')), and require (const mod = require('./x')). Fallow also uses oxc_semantic scope analysis to detect imports where the binding is never read. An import { foo } from './utils' where foo is never referenced in the file does not count as a reference to the foo export. This improves unused-export detection precision.
Whole-object consumption patterns like Object.values(ns), { ...ns }, for (const k in ns), and rest destructuring (const { a, ...rest } = ns) conservatively mark all exports as used. Fallow can’t determine which specific members are accessed in these cases.

Class member detection

Fallow detects unused public class members (methods and properties) that are never referenced outside their defining class. Unlike simple text matching, fallow understands class inheritance, decorators, and framework conventions.
class OrderService {
  // Used: called in checkout.ts
  async createOrder(items: Item[]) { /* ... */ }
  
  // Unused: never called outside this class
  private validateItems(items: Item[]) { /* ... */ }
  
  // Excluded: decorator indicates runtime wiring
  @Post('/orders')
  handleCreateOrder() { /* ... */ }
}
What fallow handles automatically:
  • Inheritance: A method defined on a parent class and called via the parent type credits the child’s override as used
  • Decorators: Any decorated member is excluded from detection. Decorators like @Get(), @Column(), @Injectable() indicate runtime wiring
  • Framework lifecycle methods: componentDidMount, ngOnInit, connectedCallback, and other framework lifecycle methods are never flagged
  • Whole-object patterns: Object.values(instance), Object.keys(), spread operators, and for..in loops conservatively mark all members as used
Class member detection works via syntactic analysis, without invoking the TypeScript compiler. This means fallow tracks member access through the import graph, not through type resolution.

CSS and SCSS tracking

Fallow tracks CSS and SCSS imports using dedicated AST-based extraction. SCSS @use, @forward, and partial imports (_prefix files) are resolved accurately. CSS Module class names are extracted as named exports and tracked through styles.className member accesses. See CSS, SCSS, and Tailwind analysis for full details.

Entry-point partial unused exports

When a file is an entry point (matched by a plugin or the entry config), fallow traditionally marks all its exports as used. Starting in v2.15.0, fallow can detect partially unused exports in entry-point files: exports that exist in an entry file but are never imported by any other module in the project. This is especially useful for framework convention files (Next.js pages, SvelteKit routes) where the framework consumes specific named exports (like default, loader, or getStaticProps) but the file may also export helper functions that nothing uses.
Entry-point files are still considered used (never reported as unused files), but individual exports within them that have zero references are now reported as unused exports.

Cross-reference with duplication

Running fallow dead-code --include-dupes cross-references dead code findings with code duplication analysis. Clone instances in unused files, or overlapping with unused exports, are flagged as combined high-priority findings.
fallow dead-code --include-dupes
Use --include-dupes to prioritize cleanup: if a block of code is both duplicated and unused, removing it eliminates dead code and reduces duplication at the same time. What the cross-reference finds:
  • Clone instances in unused files: If a file is unreachable from entry points and contains duplicated code, the duplication finding is elevated.
  • Clone instances overlapping unused exports: If an unused export contains code that is duplicated elsewhere, both findings are reported together.
Use --include-dupes in CI to surface code that is both unused and duplicated.

Circular dependency benchmarks

Fallow detects dependency cycles during module graph construction at no extra cost. Standalone tools like madge and dpdm build their own graph from scratch.
ProjectFilesfallowmadgedpdmvs madgevs dpdm
zod17417ms540ms190ms32x11x
preact24419ms298ms132ms16x7x
fastify28620ms165ms132ms8x7x
vue/core52259ms175ms143ms3x2x
TanStack/query901134ms168ms137ms1.3x1.0x
3-32x faster than madge, 2-11x faster than dpdm on small-to-medium projects. Fallow uses 5-7x less memory by reusing the module graph already built for dead code analysis.

See also

CLI: dead-code

Full reference for the fallow dead-code command and its flags.

Rules & Severity

Control which issue types are errors, warnings, or disabled.

Auto-fix

Automatically remove the dead code fallow finds.