> ## 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.

# Dead code explained

> What each issue type in fallow's dead-code output means, how to interpret the results, and when to act. Covers unused class members with inheritance tracking, circular dependency detection with no depth limits, and CSS/SCSS import resolution.

This page covers what `fallow dead-code` reports, how to prioritize findings, and when to act.

<Tip>
  Pass `--explain` to any command with `--format json` to include issue type definitions directly in the JSON output as a `_meta` object. The MCP server always includes `_meta` automatically.
</Tip>

## Issue types

Fallow detects 16 types of dead code. Each type has a default severity that determines whether it causes a non-zero exit code with `--fail-on-issues`.

### Unused files

Files not reachable from any entry point. No module in the project imports them, and they are not matched by any framework plugin or entry point pattern.

| Severity | Default |
| :------- | :------ |
| Error    | Yes     |

**When to act:** Almost always. Unused files add to IDE indexing time and confuse search results.

**Common false positives:**

* **Scripts run directly** (e.g., `node scripts/seed.ts`): Add to `entry` in config, or ensure the script is in a `package.json` script field (fallow parses those).
* **Files loaded by tools fallow doesn't know about**: Add to `entry` or the relevant plugin config.

### Unused exports

Exported symbols never imported by any other module. The export exists but nothing references it.

| Severity | Default |
| :------- | :------ |
| Error    | Yes     |

**When to act:** Most unused exports should be removed or unexported. They inflate the public API surface and confuse consumers about what the module does.

**When it's OK to keep:**

* **Library public API**: Exports consumed by external users fallow can't see. Mark them with [`/** @public */`](/configuration/suppression#jsdoc-visibility-tags) (or `@internal`, `@beta`, `@alpha`) JSDoc tags, add the library entry point to [`entry`](/configuration/overview), or use [`ignoreExports`](/configuration/overview#ignoreexports) for broad patterns.
* **Convention-based exports**: Exports consumed by frameworks via naming conventions (e.g., Next.js `getStaticProps`). Fallow's framework plugins handle most of these. If one is missed, configure the plugin or add an ignore pattern.

### Unused types

Type aliases and interfaces never referenced by any module.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**When to act:** Remove unused types. They add noise to auto-import suggestions and inflate the public type surface.

**When it's OK to keep:**

* **Shared type packages**: Types exported for external consumers. Mark the package entry point in `entry`.

### Private type leaks

Exported values, functions, classes, and types whose public TypeScript signature references a same-file type declaration that is NOT itself exported. Consumers may see the private type's name in generated declarations or API docs but cannot import it, so they end up retyping it or using `Parameters<typeof X>[0]` / `ReturnType<typeof X>` workarounds.

This is an opt-in API hygiene check, not a default dead-code finding. It is most useful for packages that publish TypeScript declarations or maintain a public API surface. For app-internal exports, private `Props`, `Options`, and `State` helper types are often intentional and should not be exported just to satisfy this rule.

| Severity | Default |
| :------- | :------ |
| Off      | Yes     |

**Example:**

```typescript theme={null}
type Props = { label: string; }; // private to the module

export function Component(p: Props): void {} // public, but Props leaks
```

**When to act:** Enable this check for publishable packages or declaration-emitting modules. For findings on that API surface, export the backing type alongside the exported symbol, or restructure the public signature so it doesn't reference the private type:

```typescript theme={null}
export type Props = { label: string; };
export function Component(p: Props): void {}
```

**Interaction with unused types:** when you export the backing type to fix a leak, fallow does not turn around and report the new export as `unused-types` even if no other module imports it. Any type whose name is referenced from a public signature in the same file is treated as load-bearing for the public API and is excluded from the unused-types check.

**Skipped patterns:**

* **ECMAScript `#field` private members and TypeScript `private` accessibility**: these are not part of the public signature, so types they reference do not leak. `class Service { #state: InternalState; }` does not flag `InternalState`.
* **Storybook story files**: `*.stories.{ts,tsx,js,jsx,mts,cts,mjs,cjs}` and `*.story.*` variants are skipped because the canonical `type Story = StoryObj<typeof X>; export const Default: Story = ...` pattern would otherwise generate one finding per story.
* **Framework routing conventions**: route files where the framework forces multiple exports per file to share a private `Props` / `Params` / `LoaderArgs` type. Coverage:

  * **Next.js App Router**: `app/**/{page,layout,template,loading,error,not-found,route,default,global-error,forbidden,unauthorized}.{ts,tsx,js,jsx}` plus the metadata files (`opengraph-image`, `twitter-image`, `icon`, `apple-icon`, `manifest`, `sitemap`, `robots`).
  * **Next.js Pages Router**: any file under `pages/`.
  * **Gatsby**: pages and templates.
  * **Remix v2 + TanStack Router**: top-level files in `app/routes/` and folder-route entries (`route.tsx`, `_layout.tsx`, `_index.tsx`, `index.tsx`, `__root.tsx`). Subdirectories under `app/routes/<segment>/` are still checked because users co-locate non-route helpers there.
  * **Expo Router**: `_layout.tsx` and `+*.tsx` special files.

  Patterns match anywhere in the path so monorepo subpackages (`packages/web/src/app/blog/[slug]/page.tsx`) are skipped too.

**Enable:** run `fallow dead-code --private-type-leaks`, or set `private-type-leaks: "warn"` / `"error"` in [`rules`](/configuration/rules).

**Suppression:** add `// fallow-ignore-next-line private-type-leak` above the affected export, or keep `private-type-leaks: "off"` in [`rules`](/configuration/rules).

**Prior art:** Rust's `unreachable_pub` lint, TypeScript's TS4023 / TS4060 errors emitted with `--declaration`. ESLint has no equivalent rule today.

### Unused dependencies

Packages listed in `dependencies` that are never imported in source code and not used as script binaries in `package.json`.

| Severity | Default |
| :------- | :------ |
| Error    | Yes     |

**When to act:** Remove from `package.json` when nothing imports it. If the finding says the package is imported in another workspace, move the dependency to that consuming workspace instead. Unused dependencies slow down `npm install`, increase `node_modules` size, and expand the attack surface for supply chain vulnerabilities.

**Internal workspace packages count too:** in monorepos, workspace package names (e.g., `@repo/ui`) listed in another workspace's `dependencies` are checked the same way as external npm packages. If a workspace declares `@repo/ui` but never imports it, the dependency is reported as unused. A workspace package's own name does not need to be self-listed.

**Common false positives:**

* **Uninstalled peer dependencies of other packages**: Fallow credits required peers when the package is installed under `node_modules`. If a peer is required only by an environment outside the analyzed install tree, check whether removing it breaks that runtime.
* **Runtime-only packages** (polyfills, CSS frameworks loaded via `@import`): Add to `ignoreDependencies` in config.

### Unused devDependencies

Same as unused dependencies, but for packages in `devDependencies`.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

### Unused optionalDependencies

Same as unused dependencies, but for packages in `optionalDependencies`. Only reported when the `--unused-optional-deps` flag is passed.

| Severity | Default     |
| :------- | :---------- |
| Warning  | No (opt-in) |

### Unused enum members

Enum values declared but never referenced.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**When to act:** Remove the unused member. Unused enum members inflate serialized values and can cause confusion about which values are valid.

### Unused class members

Class methods and properties never referenced outside their class body.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**What it detects:** Public methods and properties that are never accessed by any module other than the class itself. Internal `this.member` accesses within the class body are tracked separately and do not count as external usage.

**Inheritance awareness:** Fallow builds an inheritance map from `extends` clauses and propagates member accesses through the hierarchy. If a parent class method calls `this.getArea()`, that access credits child class overrides (`Circle.getArea()`, `Rectangle.getArea()`). External member accesses on a parent type also propagate to child implementations. This prevents false positives on abstract-like patterns where a base class defines a contract that subclasses fulfill.

**Decorator exclusion:** Members with decorators (e.g., `@Get()`, `@Column()`, `@Input()`, `@Inject()`) are automatically excluded from detection. Decorators indicate runtime wiring that static analysis cannot trace, so decorated members are never flagged.

**Lifecycle method allowlists:** Framework lifecycle methods are never flagged. Fallow has built-in allowlists for React (`componentDidMount`, `render`, `shouldComponentUpdate`, etc.) and Angular (`ngOnInit`, `ngOnDestroy`, `canActivate`, `validate`, etc.). For other frameworks (Web Components, ag-Grid, etc.), the `usedClassMembers` config option lets you extend the allowlist.

**Whole-object use heuristics:** Patterns like `Object.values(instance)`, `Object.keys(instance)`, rest destructuring (`const { foo, ...rest } = instance`), and `for (const key in obj)` conservatively mark all members as used.

```typescript theme={null}
class UserService {
  // Used: called in app.ts
  async getUser(id: string) { ... }

  // Unused: never called outside this class
  private formatName(user: User) { ... }

  // Excluded: has decorator (runtime wiring)
  @Get('/users')
  handleGetUsers() { ... }

  // Excluded: React lifecycle method
  componentDidMount() { ... }
}
```

**When to act:** Remove or mark as `private`. An unreferenced public method is either dead functionality or wired at runtime. Both are worth investigating.

**Configuration:** The `usedClassMembers` config option marks specific member names as always-used. This is useful for convention-based methods called by DI frameworks or third-party libraries that invoke methods reflectively (e.g., `agInit`, `connectedCallback`). Plugins can also contribute to this allowlist via the `usedClassMembers` field in custom plugin definitions.

### Unresolved imports

Import specifiers that cannot be resolved to a file on disk or a `node_modules` package.

| Severity | Default |
| :------- | :------ |
| Error    | Yes     |

**When to act:** Always investigate. An unresolved import usually means the code will fail at runtime. Common causes:

* Typo in the import path
* Missing dependency (not installed or not in `package.json`)
* Path alias not configured in `tsconfig.json` paths

### Unlisted dependencies

Packages imported in source code but not declared in `package.json`.

| Severity | Default |
| :------- | :------ |
| Error    | Yes     |

**When to act:** Add the package to `dependencies` or `devDependencies`. Unlisted dependencies work by accident (hoisted from another package) and will break in environments with strict `node_modules` resolution (pnpm, Yarn Plug'n'Play).

**Internal workspace packages count too:** in monorepos, importing a workspace package (e.g., `import { x } from '@repo/utils'`) from a workspace whose own `package.json` does not list `@repo/utils` is reported as unlisted. Add the workspace package to that workspace's `dependencies` (or move the import). Self-references stay allowed without requiring a package to depend on itself.

### Duplicate exports

The same symbol name exported from multiple modules. This causes ambiguous auto-imports and makes it unclear which module is the canonical source.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**When to act:** Consolidate to a single canonical export. Re-export from one location if multiple modules need to expose it.

### Circular dependencies

Modules that import each other directly or transitively, forming a cycle.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**Detection during graph construction:** Circular dependencies are detected as a natural byproduct of building the module graph using Tarjan's strongly connected components algorithm (O(V + E)). There is no separate analysis pass, so cycle detection adds zero meaningful cost to the analysis.

**No depth limits:** Fallow detects cycles of any length. The graph algorithm processes all strongly connected components regardless of size, then enumerates individual elementary cycles within each component. There is no cap on cycle depth.

**Type-only cycle filtering:** Cycles where every edge consists exclusively of `import type` imports are filtered out automatically. Type imports are erased at compile time and cannot cause runtime initialization issues. Fallow distinguishes these from runtime cycles by tracking the `is_type_only` flag on each import symbol through the graph.

**Cycle path reporting:** Fallow reports the full path of each cycle, so you can see exactly which files are involved:

```
src/auth.ts → src/user.ts → src/permissions.ts → src/auth.ts
```

This makes it straightforward to identify the weakest edge to break.

**Why circular dependencies are risky:**

* Module evaluation order becomes unpredictable. JavaScript evaluates modules depth-first, and a cycle forces at least one module to receive an incomplete namespace object.
* Accessing an export before the module finishes evaluating returns `undefined`, causing subtle runtime errors that are hard to trace.
* Bundlers may produce incorrect output or larger bundles when they cannot determine a clean evaluation order.
* Refactoring becomes dangerous because moving code between cycle members can break initialization order in ways that only manifest at runtime.

| Cycle length | Risk                             | Action                                  |
| :----------- | :------------------------------- | :-------------------------------------- |
| 2 (A ↔ B)    | Moderate, usually a design issue | Extract shared code into a third module |
| 3-5          | Higher, harder to trace          | Break the cycle at the weakest edge     |
| 5+           | Architectural issue              | Refactor module boundaries              |

**When it's OK to keep:**

* **Type-only cycles**: If the cycle only involves `import type`, there is no runtime initialization risk. These are filtered from circular dependency reports automatically.

### Boundary violations

Imports that cross user-defined architecture zone boundaries. Zones are defined in the `boundaries` config section and map file path patterns to named architecture zones. A boundary violation occurs when a file in one zone imports from a file in another zone that the boundary rules disallow.

| Severity | Default |
| :------- | :------ |
| Error    | Yes     |

**When to act:** Always. Boundary violations indicate that code is breaking the intended architecture. Fix by restructuring imports to respect zone boundaries, or update the boundary config to allow the import if it's an accepted exception.

**Common false positives:**

* **Shared utilities**: Code in a shared/common zone may need to be accessible from multiple zones. Adjust boundary rules to allow these imports.
* **Newly created zones**: When introducing boundaries to an existing codebase, expect violations. Use `warn` severity initially.

### Type-only dependencies

Production dependencies (`dependencies`) that are only ever imported via `import type`. Since type imports are erased at compile time, these packages are never loaded at runtime and should be moved to `devDependencies`.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**When to act:** Move to `devDependencies`. This reduces the production install size and clearly communicates that the package is only needed for type checking.

### Test-only dependencies

Production dependencies (`dependencies`) that are only ever imported by test files (files matching test patterns like `*.test.ts`, `*.spec.ts`, test directories). Since they are never used in production code, they should be moved to `devDependencies`.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**When to act:** Move to `devDependencies`. This reduces the production install size and makes it clear the package is only needed for testing.

<h3 id="unused-catalog-entries">
  Unused pnpm catalog entries
</h3>

Packages declared in `pnpm-workspace.yaml` under `catalog:` or `catalogs.<name>:` that no workspace `package.json` references with the `catalog:` protocol.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**When to act:** Remove the catalog entry, or switch consumers that still pin a hardcoded version to `catalog:` first.

<h3 id="empty-catalog-groups">
  Empty pnpm catalog groups
</h3>

Named `catalogs.<name>:` groups in `pnpm-workspace.yaml` that contain no package entries. The top-level `catalog:` map is not reported when empty because it can be a harmless default-catalog placeholder.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**When to act:** Remove the empty named group header.

<h3 id="unresolved-catalog-references">
  Unresolved pnpm catalog references
</h3>

Workspace `package.json` dependencies using `catalog:` or `catalog:<name>` where the selected catalog does not declare the requested package.

| Severity | Default |
| :------- | :------ |
| Error    | Yes     |

**When to act:** Add the package to the selected catalog, switch to a catalog that declares it, or pin a direct version.

<h3 id="pnpm-dependency-overrides">
  pnpm dependency overrides
</h3>

`unused-dependency-overrides` reports override entries whose target package is not declared by any workspace `package.json` and is not present in `pnpm-lock.yaml`. Overrides that target transitive dependencies resolved in the lockfile (the common CVE-pin pattern) are treated as used. When the lockfile is missing or unreadable, fallow falls back to a package.json-only check and keeps a `hint` on each finding so transitive pins can be reviewed before removal. `misconfigured-dependency-overrides` reports malformed override keys or empty values that pnpm rejects.

<h3 id="stale-suppressions">
  Stale suppressions
</h3>

`// fallow-ignore` comments and `/** @expected-unused */` JSDoc tags that no longer match any issue. Suppression comments accumulate as codebases evolve. A suppression that once silenced a real finding may become stale when the underlying issue is fixed, the code is moved, or another module starts importing a previously unused export.

| Severity | Default |
| :------- | :------ |
| Warning  | Yes     |

**Two origins are tracked:**

* **Inline comments** (`// fallow-ignore-next-line`, `// fallow-ignore-file`): reported as stale when the target line or file no longer has a matching issue.
* **`@expected-unused` JSDoc tags**: reported as stale when the tagged export is now imported by another module. Use `/** @expected-unused */` to mark exports that are intentionally dead code but should be flagged if they become used again.

```typescript theme={null}
// STALE: this suppression no longer matches any issue
// fallow-ignore-next-line unused-export
export const helper = () => {};  // now imported in app.ts

// @expected-unused marks intentional dead code
/** @expected-unused */
export const deprecatedApi = () => {};
// ^ reported as stale if something starts importing deprecatedApi
```

**When to act:** Remove the stale suppression comment or JSDoc tag. Stale suppressions add noise and may hide future issues if the code changes again.

**Configuration:**

```jsonc theme={null}
{
  "rules": {
    "stale-suppressions": "error"  // default: "warn"
  }
}
```

Use `--stale-suppressions` as a filter flag to show only stale suppression findings.

## Prioritizing findings

Not all findings are equally important. Use this priority order:

1. **Unresolved imports**: potential runtime failures
2. **Unlisted dependencies**: breaks in strict environments
3. **Boundary violations**: architecture enforcement
4. **Unused files**: entire modules to delete
5. **Unused dependencies**: security and install time
6. **Unused exports**: API surface cleanup
7. **Circular dependencies**: initialization bugs
8. **Type-only dependencies**: install size optimization
9. **Test-only dependencies**: install size optimization
10. **Duplicate exports**: developer experience
11. **Stale suppressions**: suppression hygiene
12. **Unused types / enum members / class members**: code hygiene

<Info>
  The first two categories indicate correctness issues that may cause runtime failures. Everything else is maintenance and hygiene.
</Info>

## Reading the summary line

```text theme={null}
Found 401 issues in 0.16s
```

The total count includes all issue types. Filter with `--unused-files`, `--unused-exports`, etc. to focus on specific categories.

With `--fail-on-issues`, only issues with error severity cause a non-zero exit. Warnings are reported but don't fail the build.

With `--threshold` on specific filters, you can set numeric limits:

```text theme={null}
Found 401 issues (17 errors, 384 warnings)
```

## Incremental adoption

Most projects have existing dead code. Fallow supports gradual adoption:

### Baseline comparison

```bash theme={null}
# Save current state
fallow dead-code --save-baseline fallow-baselines/dead-code.json

# Only fail on new issues
fallow dead-code --baseline fallow-baselines/dead-code.json --fail-on-issues
```

The baseline captures issue fingerprints (file + export + type), not line numbers. Refactoring won't invalidate the baseline as long as the export names stay the same.

### Changed-since

```bash theme={null}
# Only check files in the current PR
fallow dead-code --changed-since main
```

Reports issues only in files that git shows as changed. Fast and focused for CI.

### Filter by type

```bash theme={null}
# Start with just unused files
fallow dead-code --unused-files --fail-on-issues

# Add unused exports later
fallow dead-code --unused-files --unused-exports --fail-on-issues
```

Adopt one issue type at a time. Fix the findings, then add the next type.

## Common false positive patterns

Fallow auto-detects 95 frameworks via `package.json`, so most convention-based exports (Next.js pages, Storybook stories, Jest configs, tap suites, and tsd declaration tests) are handled out of the box. Run `fallow list` to see which plugins are active and what entry points they add.

False positives that do occur are typically in these categories:

| Pattern                                       | Why fallow flags it                       | Fix                                                                              |
| :-------------------------------------------- | :---------------------------------------- | :------------------------------------------------------------------------------- |
| Scripts run directly (`node scripts/seed.ts`) | Not imported by any module                | Add to `entry` in config, or add a `package.json` script                         |
| Dependency injection / runtime wiring         | Registered via DI container, not `import` | Add to `entry` or use `/** @public */` (or `@internal`, `@beta`, `@alpha`)       |
| Dynamic `require()` with variables            | Path not statically resolvable            | Add the directory to `entry`                                                     |
| Environment-specific files                    | Only imported behind `process.env` checks | Add to `entry` if always needed                                                  |
| Unsupported framework conventions             | Plugin doesn't exist for this framework   | Add entry points manually or [write a custom plugin](/frameworks/custom-plugins) |

<Warning>
  Before acting on a finding, verify with `--trace`. A finding is only as good as fallow's module graph. If an export is used via a dynamic pattern fallow can't resolve, use `--trace` to confirm.
</Warning>

```bash theme={null}
# Verify a specific export
fallow dead-code --trace src/utils.ts:formatDate

# See all edges for a file
fallow dead-code --trace-file src/utils.ts

# Check where a dependency is used
fallow dead-code --trace-dependency lodash
```

## Limitations

* **Fully runtime-computed paths**: `import(variable)` or `require(variable)` where the argument has no static structure cannot be resolved. Template literals with partial paths (`import(\`./locales/\${lang}.json\`)`) and `import.meta.glob`*are* resolved. Only completely opaque variables are a limitation. Mark those directories in`entry\`.
* **Syntactic analysis only**: fallow doesn't invoke the TypeScript compiler. Conditional types or dead code behind generic constraints require type resolution to detect. See [known limitations](/analysis/limitations) for details.
* **Side-effect imports** (`import './polyfill'`) are tracked as file-level edges. The imported file won't be flagged as unused, but individual exports within it are still analyzed.

<Info>
  Monorepo workspaces, framework conventions (Next.js, NestJS, Angular, etc.), and decorator-driven wiring are handled automatically via [auto-detected plugins](/frameworks/built-in) and [workspace detection](/configuration/workspaces). See [known limitations](/analysis/limitations) for edge cases.
</Info>

## JSON `_meta` object

When `--explain` is passed (or via MCP), the JSON output includes a `_meta` object:

```json theme={null}
{
  "schema_version": 3,
  "_meta": {
    "docs": "https://docs.fallow.tools/explanations/dead-code",
    "issue_types": {
      "unused_file": {
        "name": "Unused file",
        "description": "File not reachable from any entry point",
        "severity": "error",
        "action": "Delete the file or add it as an entry point"
      },
      "unused_export": {
        "name": "Unused export",
        "description": "Exported symbol never imported by another module",
        "severity": "error",
        "action": "Remove the export keyword or delete the symbol"
      },
      "circular_dependency": {
        "name": "Circular dependency",
        "description": "Modules that import each other directly or transitively",
        "severity": "warning",
        "action": "Extract shared code into a separate module"
      },
      "boundary_violation": {
        "name": "Boundary violation",
        "description": "Import crosses a user-defined architecture zone boundary",
        "severity": "error",
        "action": "Restructure the import to respect zone boundaries"
      }
    }
  }
}
```

AI agents and CI systems can use this to interpret issue types without consulting external documentation.
