fallow dead-code performs dead code analysis on your TypeScript project. It builds a module graph from your entry points 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
Fallow detects these types of dead code:
| Issue type | Description |
|---|---|
| Unused files | Files not reachable from any entry point |
| Unused exports | Exported symbols never imported elsewhere |
| Unused types | Type aliases and interfaces never referenced |
| Unused dependencies | Packages in dependencies never imported or used as script binaries |
| Unused devDependencies | Packages in devDependencies never imported or used as script binaries |
| Unused optionalDependencies | Packages in optionalDependencies never imported or used as script binaries |
| Unused enum members | Enum values never referenced |
| Unused class members | Class methods and properties never referenced outside their class, with inheritance tracking, configurable decorator exclusion via ignoreDecorators, and framework lifecycle allowlists |
| Unused catalog entries | Entries in pnpm-workspace.yaml's catalog: or catalogs: maps that no workspace package references via the catalog: protocol |
| Unresolved imports | Import specifiers that cannot be resolved |
| Unlisted dependencies | Imported packages missing from package.json |
| Duplicate exports | Same symbol exported from multiple modules |
| Circular dependencies | Modules that import each other directly or transitively |
| Boundary violations | Imports that cross user-defined architecture zone boundaries |
| Type-only dependencies | Production dependencies only imported via import type (should be devDependencies) |
| Test-only dependencies | Production dependencies only imported by test files (should be devDependencies) |
| Stale suppressions | fallow-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:
● 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)
Report only specific issue types:
fallow dead-code --unused-files
fallow dead-code --unused-exports --unused-types
fallow dead-code --unresolved-imports --unlisted-deps
Colored terminal output designed for readability.
fallow dead-code --format humansrc/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)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.
● 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)
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
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
Fallow uses syntactic analysis with scope-aware binding resolution via Oxc. No TypeScript compiler, no type information. That's what makes it fast.
flowchart TB
A["Discovery<br/>Entry points from package.json + plugins"] --> B["Parsing<br/>Parallel with Oxc + oxc_semantic + rayon"]
B --> C["Resolution<br/>Import specifiers to file paths"]
C --> D["Graph<br/>Module graph with re-export chains"]
D --> E["Analysis<br/>Walk from entry points, report unreachable code"]
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.
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:
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.node scripts/seed.js) are also recognized as entry points.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.
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 type | What fallow extracts |
|---|---|
Dockerfiles (Dockerfile, Dockerfile.*, *.Dockerfile) | RUN node, CMD, ENTRYPOINT, esbuild invocations |
| Procfiles | Process definitions (e.g., worker: node dist/worker.js) |
| fly.toml / fly.*.toml | release_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"]
Fallow resolves dynamic imports that use patterns rather than static strings. When you write import(`./locales/${lang}.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:
| Pattern | Example | Resolved as |
|---|---|---|
| Template literals | import(`./icons/${name}.svg`) | ./icons/*.svg |
| String concatenation | import("./routes/" + path) | ./routes/* |
import.meta.glob | import.meta.glob("./modules/*.ts") | ./modules/*.ts |
require.context | require.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.
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';
flowchart TB
A["app.ts<br/>import ﹛ add ﹜ from './src'"] --> B["src/index.ts<br/>export * from './utils'"]
B --> C["utils/index.ts<br/>export * from './math'"]
C --> D["utils/math.ts<br/>export const add ✓"]
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:
export * re-exports is followed until the original declaration is found.a re-exports from b, b re-exports from a) are detected and handled gracefully.export { foo } from './bar') and namespace re-exports (export * from './bar') are both tracked.When two export * sources contribute the same name, ECMAScript treats that
name as ambiguous and the barrel does not export it. Fallow suppresses findings
that would otherwise blame the contributing declarations, members, components,
or injections until the barrel collision is fixed. Use
fallow trace FILE:NAME to inspect the contributing source files
and whether the collision occurs in the type namespace, value namespace, or
both.
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.
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:
@Get(), @Column(), @Injectable() indicate runtime wiringcomponentDidMount, ngOnInit, connectedCallback, and other framework lifecycle methods are never flaggedObject.values(instance), Object.keys(), spread operators, and for..in loops conservatively mark all members as usedignoreDecoratorsIf you use utility decorators that do NOT imply reflective consumption (Playwright's @step("label"), internal labeling decorators like @measure, @log, @retry), list their names in the ignoreDecorators config option so methods decorated with ONLY those names are checked for usage like undecorated methods.
// .fallowrc.json
{
"ignoreDecorators": ["@step"]
}
Conservative semantics: a method carrying any decorator NOT in the list still gets skipped, so @step combined with @Inject on the same method stays treated as framework-managed.
See ignoreDecorators for the matching rules and the unmatched-entry warning.
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.
Fallow tracks CSS and SCSS imports using dedicated 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.
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.
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:
This is not a like-for-like comparison. fallow dead-code --circular-deps runs the full analysis pipeline (dead code, dependencies, boundaries, cycles) while madge and dpdm only build an import graph. dpdm reports incomplete cycle counts on these fixtures, so its timings are not directly comparable. Cold runs, fastest tool per row in bold.
| Project | Files | fallow | madge | vs madge | dpdm | vs dpdm |
|---|---|---|---|---|---|---|
| zod | 174 | 43ms | 532ms | fallow 12.4x | 192ms | fallow 4.5x |
| preact | 244 | 75ms | 299ms | fallow 4.0x | 134ms | fallow 1.8x |
| fastify | 286 | 97ms | 224ms | fallow 2.3x | 165ms | fallow 1.7x |
| vue/core | 522 | 137ms | 173ms | fallow 1.3x | 145ms | dpdm faster |
| TypeScript | 38,146 | 2.18s | 5.16s | fallow 2.4x | 136ms | dpdm faster |
| next.js | 20,552 | 3.00s | 485ms | madge faster | 463ms | dpdm faster |
Fallow is faster on small and mid-size projects and on the large TypeScript repo versus madge, while madge wins on large monorepos like next.js and astro. dpdm is fast but reports incomplete cycle counts on these fixtures. Fallow reuses the module graph already built for dead code analysis, so cycle detection adds no extra graph build.