> ## 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 reports multiple dead-code issue types. 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. The one exception is a Lit `@state()` reactive property on a direct `LitElement` / `ReactiveElement` subclass (when `lit` is a declared dependency): `@state` is internal component state with no external surface, so a `@state` field read nowhere via `this.<name>` (in a method or the `html` template) is flagged as an unused class member. The public `@property()` decorator stays excluded (it is the element's attribute API, settable from HTML, a parent binding, `setAttribute`, or CSS).

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

### Unused store members

A Pinia store member (a `state` / `getters` / `actions` key, or a setup-store returned key) that is declared but accessed by no consumer anywhere in the project.

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

**What it detects:** The store binding is imported (so the module is reachable), yet a specific state property, getter, or action is never read. This is the cross-graph "declared but never used by any file" direction that single-file linters (eslint-plugin-vue, eslint-plugin-svelte, eslint-plugin-pinia) and type-checkers (vue-tsc, svelte-check) do not cover. Defaults to `warn`, not `error` like the closed-set class and enum member rules: a store has an open declaration surface (plugins, dynamic dispatch), so confidence is lower.

**Activation:** Runs only when `pinia` or `@pinia/nuxt` is a declared dependency, so it never fires on an unrelated `defineStore`-named helper in a non-Pinia project.

**Consumption it credits:** `const s = useStore(); s.member`, destructures (`const { count } = useStore()`, `const { count } = storeToRefs(store)`), template `store.member` in `.vue` / `.svelte` files, and intra-store `this.member` usage between getters and actions.

**Abstain heuristics (false-positive safe):** Whole-object usage (`{ ...store }`, `Object.keys(store)`, dynamic `store[key]`) and the Options-API `mapState` / `mapGetters` / `mapActions` helpers conservatively mark the whole store as used, so no member is flagged. The Pinia `$`-prefixed API (`$patch`, `$reset`, `$subscribe`, etc.) is never reported, and a setup store whose returned object spreads (`return { ...base, x }`) abstains entirely.

```typescript theme={null}
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],        // Used: read in CartView.vue
    legacyTotal: 0,   // Unused: no component or store ever reads it
  }),
  getters: {
    total: (s) => s.items.length,  // Used: rendered in the template
  },
  actions: {
    add() { /* ... */ },           // Used: called on click
    deprecatedReset() { /* ... */ }, // Unused: never dispatched
  },
})
```

**When to act:** Remove the unused state property, getter, or action. There is no auto-fix, because a store member can be accessed reflectively through a Pinia plugin or dynamic dispatch; suppress with `// fallow-ignore-next-line unused-store-member` if a member is consumed in a way fallow cannot see.

**Scope:** Pinia only. Svelte stores and Vuex are not yet covered (a Svelte `writable` is opaque and its value shape is reshaped at runtime, so member-level detection would be false-positive-prone; an entirely unused store export is already caught by the unused-export check).

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

<h3 id="re-export-cycles">
  Re-export cycles
</h3>

Two or more barrel files that re-export from each other in a loop, or a single barrel file that re-exports from itself. Distinct from circular dependencies: circular deps describe runtime import loops; re-export cycles describe **declarative** loops in the `export * from` graph where chain propagation has no terminating module to resolve names against.

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

**Why this is its own category:** Circular dependencies in the runtime import graph are sometimes intentional (mutual recursion between application modules). Re-export cycles are essentially always bugs. When `src/api/index.ts` does `export * from './internal'` and `src/api/internal/index.ts` does `export * from '../index'`, neither file actually declares anything; the chain has no terminating source. Any consumer doing `import { foo } from './api'` is asking the chain to resolve `foo` against a graph with no leaf, and the import silently comes up `undefined` at runtime.

**Two structural shapes:**

* **`multi-node`** -- a strongly connected component of two or more files in the re-export edge subgraph. Example: `barrel-a.ts` re-exports from `barrel-b.ts` and `barrel-b.ts` re-exports back from `barrel-a.ts`.
* **`self-loop`** -- a single file that re-exports from itself. Almost always a rename leftover where `barrel.ts` ended up doing `export * from './'` or `export * from './barrel'`. Surfaces under the same rule with a distinct `kind` discriminator so consumers can tell the two shapes apart.

**Detection runs in the graph layer:** detection happens during Phase 4 (re-export chain resolution) via iterative Tarjan SCC over the `(barrel, source)` edge subgraph. Same Tarjan implementation that already powers circular-dependency detection; the SCC pass runs once and surfaces both findings (and the `tracing::warn!` lines for operators running `RUST_LOG=warn` stay, additive to the typed finding).

**Type-only cycles still fire:** `export type * from './b'` paired symmetrically with `export type * from './a'` is also a re-export cycle. Chain propagation through a type-only loop is the same no-op as through a value loop; the cycle is reported regardless.

**Mock human output:**

```
Re-Export Cycles (Architecture)

  src/api/index.ts
    Cycle (2 files):
      - src/api/index.ts
      - src/api/internal/index.ts
    To fix: remove one `export * from` statement on any member file.

  src/utils/index.ts
    Self-loop (1 file):
      - src/utils/index.ts
    To fix: remove the `export * from './'` (or equivalent) inside this file.
```

**How to fix it:**

1. Pick any one file in the loop.
2. Remove the `export * from` (or `export { ... } from`) statement that points back into the cycle.
3. Any single removal breaks the cycle and restores working re-exports. You do not need to touch every member.

For a self-loop, the fix is unambiguous: open the offending file and delete the stray `export * from './'` (or `export * from './<this-file>'` after a rename).

**Suppression:** the finding is file-scoped (the cycle spans multiple files; there is no single line to anchor at). Suppress with a file-level comment on any member:

```ts theme={null}
// fallow-ignore-file re-export-cycle
export * from "./internal";
```

The four spellings `re-export-cycle`, `re-export-cycles`, `reexport-cycle`, and `reexport-cycles` are all accepted. Per-file `overrides.rules.re-export-cycle` is a **no-op** because the rule's severity is project-wide and the finding spans multiple files; configure it at the top-level `rules` block instead, or use the file-level `fallow-ignore-file` suppression above.

**When to keep it as a warning:** legacy projects with hand-written barrels predating ESM strictness may have benign-looking cycles that no consumer actually relies on. Leaving the rule at `warn` keeps the diagnostic visible without breaking CI; promote to `error` once the cycles are cleaned up. Marcus's rule of thumb on upgrade: expect `total_issues` to step up on the first run because cycles that existed before this rule shipped now appear as findings.

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

<h3 id="policy-violations">
  Policy violations
</h3>

Calls, imports, or catalogue-derived effects matching a `banned-call`, `banned-import`, or `banned-effect` rule from a configured rule pack (the `rulePacks` config key). Rule packs are standalone JSON or JSONC files of pure declarative policy data; loading a pack never executes project code. Findings are identified as `<pack>/<rule-id>` across every output format.

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

The `rules."policy-violation"` master defaults to `warn` so a new pack never hard-fails CI on its first run. A per-rule `severity` in the pack overrides the master per finding, and the exit-code gate reads the effective per-finding severity, so one `error` rule fails the run even under a warn master. `off` on the master is a kill switch for the whole evaluator. Invalid or missing packs fail config load with exit code 2 instead of silently enforcing nothing.

**When to act:** Replace the banned call or import with the alternative named in the rule's message. Scope a rule with `files` / `exclude` globs if it fires in directories where the usage is sanctioned, or suppress one rule with `// fallow-ignore-next-line policy-violation:<pack>/<rule-id>`. Use bare `policy-violation` only when you intend to suppress every rule-pack finding at that line or file scope.

**Limitations:** Matching is syntactic. `banned-call` does not follow aliased or re-bound callees (`const run = cp.exec; run()`), and `banned-import` matches the raw specifier only, so rewritten forms such as `npm:moment` are not matched; `require()` calls and dynamic `import()` are not checked.

Run `fallow rule-pack-schema` to print the pack JSON Schema for editor autocomplete. See the [`rulePacks` config key](/configuration/overview#config-fields) for the pack file format.

<h3 id="invalid-client-exports">
  Invalid client exports
</h3>

A file carrying the `"use client"` directive that also exports a Next.js server-only or route-segment-config name. Next.js rejects these at build time: a client module cannot export `metadata`, `generateMetadata`, `viewport`, `generateViewport`, `generateStaticParams`, `dynamic`, `dynamicParams`, `revalidate`, `fetchCache`, `runtime`, `preferredRegion`, `maxDuration`, the Pages Router data functions (`getServerSideProps`, `getStaticProps`, `getStaticPaths`), or a route HTTP method handler (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`). fallow catches it statically, before a build, in the same pass as the rest of dead-code analysis. The component's `default` export is never flagged.

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

The rule only runs when `next` is a declared dependency, so it cannot false-positive on non-Next projects. There is no auto-fix, because removing a legitimate client export would break the component.

**When to act:** Move the offending export out of the `"use client"` file into a server module (or a shared module without the directive). Suppress with `// fallow-ignore-next-line invalid-client-export` if the export is intentional and you handle the boundary another way.

<h3 id="mixed-client-server-barrels">
  Mixed client/server barrels
</h3>

A barrel file (one with at least one `export ... from` re-export) that re-exports both a `"use client"` module and a server-only module. Importing any single name from such a barrel can drag the other origin's directive context across the React Server Components boundary, a common Next.js App Router footgun that surfaces as confusing build or hydration errors far from the barrel.

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

A module counts as server-only when it carries a `"use server"` directive, imports the `server-only` package, `next/server`, `node:fs`, or `node:child_process` (the `node:` and bare forms both count), or imports a server-only `next/headers` API (`cookies`, `headers`, `draftMode`). The check classifies only direct re-export origins (it does not walk transitive chains), skips type-only re-exports (which carry no runtime directive context), and never flags a barrel that mixes a client module with an ordinary utility, so it stays false-positive-safe. It runs only when `next` is a declared dependency.

**When to act:** Split the barrel so client and server-only modules are re-exported from separate entry points. There is no auto-fix (splitting a barrel is a design decision); suppress with `// fallow-ignore-next-line mixed-client-server-barrel` if the grouping is intentional.

<h3 id="misplaced-directives">
  Misplaced directives
</h3>

A `"use client"` or `"use server"` directive written as a bare string-literal statement below an import (or any other statement) instead of at the very top of the file. A directive is only honored in the file's leading prologue; once any statement precedes it, the parser treats the string as an ordinary expression and the bundler silently ignores it, so a file you intended as a client component is treated as a server module. Nothing errors, which is what makes it dangerous.

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

The fix is always the same: move the directive above every `import`. The rule runs only when `next` is a declared dependency. There is no auto-fix (fallow does not reorder your source); the finding points at the misplaced directive so you can move it by hand.

**When to act:** Move the `"use client"` / `"use server"` line to the top of the file, before all imports. Suppress with `// fallow-ignore-next-line misplaced-directive` only if the bare string is intentional and not meant as a directive (rare).

<h3 id="route-collisions">
  Route collisions
</h3>

Two or more App Router route files (a `page` or a `route` handler) that resolve to the same URL within one app-root. Route groups `(name)` and parallel slots `@name` do not change the URL, so `app/(marketing)/about/page.tsx` and `app/(shop)/about/page.tsx` both own `/about`. Next.js fails the build with "You cannot have two parallel pages that resolve to the same path" because a URL can have at most one owner, whether a Page or a Route Handler. fallow catches it statically, before a build, and surfaces every colliding file at once (the build error names only one and aborts).

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

It defaults to `error` because it mirrors a `next build` failure: a project hitting it was already red, so erroring aligns fallow's exit code with the build it mirrors.

Collision buckets are scoped per app-root using your workspace package roots, so a monorepo with several independent Next apps that happen to share a path (`apps/web/app/about` and `apps/admin/app/about`) is not flagged. Files under a private `_folder` and files under an intercepting marker (`(.)`, `(..)`, `(...)`) are excluded. Two dynamic segments that differ only by name (`[id]` vs `[slug]`) are reported by [dynamic segment name conflicts](#dynamic-segment-name-conflicts), not here. The rule runs only when `next` is a declared dependency.

**When to act:** Move or merge one of the colliding files so each URL has a single owner. Suppressing a guaranteed build error does not make the build pass, so the primary action is the fix; `// fallow-ignore-file route-collision` exists only as an escape hatch.

<h3 id="dynamic-segment-name-conflicts">
  Dynamic segment name conflicts
</h3>

Two or more sibling dynamic route segments at the same App Router tree position using different param spellings (`[id]` vs `[slug]`, or a catch-all `[...x]` vs an optional catch-all `[[...x]]`). Next.js throws "You cannot use different slug names for the same dynamic path" at dev and production runtime when the position is hit, because one position must resolve to a single param name. Note that `next build` does NOT catch this (the build succeeds), so CI stays green while the route crashes the first time it is requested; fallow's static catch closes that gap. Route groups are transparent to the position and parallel slots fork it, so only genuinely-sibling segments conflict.

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

It defaults to `error` because it is a deterministic runtime crash on first request that `next build` lets through, so fallow is the only gate that fails on it. The detector is pure path arithmetic on the same primitive as `route-collision`, so there is no heuristic to misfire.

The rule runs only when `next` is a declared dependency.

**When to act:** Rename the dynamic segments at the position to one consistent slug name (pick `[id]` or `[slug]` for both). Suppress with `// fallow-ignore-file dynamic-segment-name-conflict` only as an escape hatch.

<h3 id="unused-server-actions">
  Unused server actions
</h3>

A Next.js Server Action (an export of a `"use server"` file) that no code in the project references: no import-and-call, no `action={fn}` binding, and no `<form action={fn}>`. This is the cross-graph "exported but wired to nothing" direction, exactly where dead server actions accumulate as a page is refactored, and `eslint-plugin-next` cannot see it because it is single-file. It does not mean the endpoint is unreachable: Next still registers a generated action id, so it stays POST-able; it means no project code calls it, so it is a strong delete candidate.

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

This is a more specific re-classification of `unused-export` for `"use server"` files. It reuses fallow's whole-project reference graph, so the `action={fn}` and `<form action={fn}>` bindings, plain import-and-call, and component-prop forwarding all count as real uses, and wrapped action factories (`authenticatedActionClient.action(...)`, `withAuditLogging(...)`) are credited by the wrapped const's references; only a genuinely orphaned action is flagged. The rule runs only when `next` is a declared dependency. Inline `"use server"` body directives (`export async function f() { "use server" }` in a non-`"use server"` file) are not covered yet; such dead actions still surface as `unused-export`.

**When to act:** Wire the action to a consumer or delete it. There is no auto-fix. Suppress with `// fallow-ignore-next-line unused-server-action`, or set the rule to `off`. If you gate CI on dead server actions, note the action now reports as `unused-server-action` (default `warn`) instead of `unused-export` (default `error`); set this rule to `error` to keep failing on it, and re-save any `--save-baseline` snapshot once.

<h3 id="unprovided-injects">
  Unprovided injects
</h3>

A Vue `inject(KEY)` or Svelte `getContext(KEY)` whose symbol key is `provide()` / `setContext()`'d nowhere in the project. At runtime a dead inject silently returns `undefined`, surfaced only when the affected path renders; no static tool in the Vue / Svelte / Nuxt ecosystems catches it (Vue and Nuxt emit a runtime-only warning, Svelte's eslint proposal is unimplemented).

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

The key must be a symbol with cross-file identity (an imported const or a module-local symbol), which makes the check false-positive-safe by construction: a key imported from a package abstains (the provider may live inside that package), a key bound to a string literal abstains (string identity, a literal provider matches it), a key that is part of your package's public API abstains (a "bring-your-own-provider" library exports the key for the consumer to provide), and any dynamic-keyed provide (`keys.forEach(k => provide(k))`) abstains project-wide. App-level `app.provide(KEY, value)` is credited, and a provide imported directly while the inject reaches the same key through a barrel re-export are matched. The rule runs only when `vue`, `@vue/runtime-core`, or `svelte` is a declared dependency. Nuxt's string-keyed `nuxtApp.provide` / `$x` API and the inverse provided-never-injected direction are not covered yet.

**When to act:** Provide the key (`provide(KEY, value)` / `setContext(KEY, value)` in an ancestor) or remove the dead inject. There is no auto-fix. Suppress with `// fallow-ignore-next-line unprovided-inject`, or set the rule to `off`.

<h3 id="unrendered-components">
  Unrendered components
</h3>

A single-file component (the default export of a `.vue` / `.svelte` / `.astro` file) that is kept reachable by a barrel re-export but instantiated by no file in the project: no `<Tag>`, no `:is` / `this=` binding, no `components` / `app.component` registration, no `h()` / Nuxt auto-import, and no script value-read. This is the common rot where a component is refactored out of every template but its barrel re-export keeps it alive, so `unused-file` (the file is reachable) and `unused-export` (the re-export counts as a use) both miss it; `eslint`'s `vue/no-unused-components` is single-file and cannot see a barrel-reexported component rendered by a sibling.

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

The render set is built liberally across barrel chains, Nuxt auto-imports, and dynamic / side-effect imports, so over-crediting only suppresses a finding (never creates one). A component that is itself an entry point (route page, layout, `App.vue`, Nuxt `app.vue` / `error.vue`) and a component re-exported through any chain from a non-private package entry point (a library exporting components for its consumers to render) are both abstained, so component libraries are not false-flagged. The rule runs only when `vue` / `@vue/runtime-core` / `nuxt` (for `.vue`), `svelte` / `@sveltejs/kit` (for `.svelte`), or `astro` (for `.astro`) is a declared dependency.

The same rule has two non-SFC arms. An **Angular arm** (`framework: "angular"`, gated on `@angular/core`) flags an `@Component` whose element selector appears in no template project-wide, with route / `bootstrapApplication` / lazy-`loadComponent` and dynamic-render (`ViewContainerRef.createComponent`, `*ngComponentOutlet`) abstains. A **Lit arm** (`framework: "lit"`, gated on `lit` / `lit-element` / `@lit/reactive-element`) flags a registered custom element (`@customElement('x-foo')`, or `customElements.define('x-foo', C)` on any receiver) whose tag is rendered in no `html` template, no standalone `.html` document, and via no imperative `createElement` / `customElements.get` / `whenDefined`; it abstains on published elements (public API) and on elements defined under a docs / dev / demo directory (rendered by site or dev tooling that fallow cannot parse).

**When to act:** Render the component where it belongs, or delete it and its barrel re-export. There is no auto-fix (a component can be rendered reflectively through a dynamic `<component :is>` resolved from a non-literal value, or a Lit element created from a computed tag). Suppress with `// fallow-ignore-next-line unrendered-component`, or set the rule to `off`.

<h3 id="unused-component-props">
  Unused component props
</h3>

A Vue `<script setup>` `defineProps` declared prop that is referenced by no code in its own single-file component, neither in `<script>` nor in `<template>`. This is the in-component dead-input direction that `vue-tsc` / Volar do not cover (they check caller-side prop correctness, not "this declared prop is wired to nothing"); eslint's `vue/no-unused-properties` is opt-in, off by default, and historically unreliable on `<script setup>` reactive destructure.

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

Prop names are harvested from the inline TS form (`defineProps<{ foo: T }>()`), the runtime object form (`defineProps({ foo: {...} })`), and `withDefaults(...)`; usage is credited from both script (the destructured binding or `props.foo`) and template (`{{ foo }}`, `:x="foo"`, `props.foo`, `$props.foo`). It stays false-positive-safe by abstaining on the whole component when it cannot see all consumption: a `v-bind="$attrs"` / `v-bind="props"` fallthrough, a whole-object props use (`toRefs(props)`, `{...props}`), a `defineExpose` or `defineModel` call, or a prop type from an imported alias (`defineProps<ImportedProps>()`, names not statically resolvable). The rule runs only when `vue` / `@vue/runtime-core` / `nuxt` is a declared dependency and covers Vue `<script setup>` and Options API props.

A **React / Preact arm** shares this rule key and emits the same finding for a prop that is destructured from a component's first parameter and read nowhere in the component body. It is dep-gated on `react` / `react-dom` / `next` / `preact`, covers inline-destructured literal props, and stays false-positive-safe by abstaining on rest spread (`{...rest}`), props forwarded by spread, props passed wholesale to a hook, a `forwardRef` / `memo` component whose props come from an imported interface (not statically resolvable), and exported public-API component props.

An **Astro arm** shares this rule key for a `.astro` `interface Props` (or `type Props`) field read nowhere: not through an `Astro.props` destructure (including a nested destructure such as `const { post: { id } } = Astro.props`), not via `Astro.props.<name>`, not in a `{expr}` template position, and not through a `<style>` / `<script>` `define:vars={{ prop }}` binding. It is dep-gated on `astro` and abstains on the whole component for a `{...Astro.props}` template spread, a whole-object `Astro.props` use, an `interface Props extends ...` or imported-type alias (names not statically resolvable), or an array destructure of `Astro.props`.

**When to act:** Wire the prop where it is meant to be used, or remove it from `defineProps` (Vue) / the destructure (React). There is no auto-fix. Suppress with `// fallow-ignore-next-line unused-component-prop`, or set the rule to `off`.

<h3 id="unused-component-emits">
  Unused component emits
</h3>

A Vue `<script setup>` `defineEmits` declared event that is emitted by no code in its own single-file component, neither through `emit('name')` in `<script>` nor `emit('name')` / `$emit('name')` in `<template>`. This is the emit-side sibling of unused component props: the in-component dead-output direction that `vue-tsc` / Volar do not cover (they check caller-side listener correctness, not "this declared event is never emitted").

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

Event names are harvested from the array form (`defineEmits(['save', 'close'])`), the type / object form (`defineEmits<{ save: [] }>()`), and the bound form (`const emit = defineEmits(...)`, tracking the actual local binding name so a renamed `const notify = defineEmits(...)` is still credited); usage is credited from both the script call walk and the template (`@click="$emit('save')"`). It stays false-positive-safe by abstaining on the whole component when it cannot see all emission: a dynamic `emit(eventName)` whose event name is not a literal, the emit function passed or returned or spread elsewhere, a `defineModel` call (which generates implicit `update:x` emits), or an emit type from an imported alias (`defineEmits<ImportedEmits>()`, names not statically resolvable). The rule runs only when `vue` / `@vue/runtime-core` / `nuxt` is a declared dependency and covers Vue `<script setup>` and Options API emits.

**When to act:** Emit the event where it is meant to fire, or remove it from `defineEmits`. There is no auto-fix (removing an emit can break a deliberately-stable public component API). Suppress with `// fallow-ignore-next-line unused-component-emit`, or set the rule to `off`.

<h3 id="unused-svelte-events">
  Unused Svelte events
</h3>

A Svelte component dispatches a custom event through `createEventDispatcher`, but no reachable Svelte consumer listens for that event name.

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

Event names are harvested from literal dispatcher calls such as `dispatch('save')` after tracking the local binding returned by `createEventDispatcher()`. Listeners are credited from Svelte component usage with `on:event` syntax and forwarded listener attributes. The rule stays false-positive-safe by abstaining from dynamic event names, dispatcher values that escape the component, and projects without a declared Svelte dependency.

**When to act:** Add the intended listener, forward the event intentionally, or remove the dispatch. There is no auto-fix. Suppress with `// fallow-ignore-next-line unused-svelte-event`, or set the rule to `off`.

<h3 id="unused-component-inputs">
  Unused component inputs
</h3>

An Angular component input (`@Input()`, the signal `input()` / `input.required()`, or `model()`) declared on a component that is read by no code in its own component, neither in the class body nor in its template (inline `template` or an external `templateUrl` file). This is the Angular analogue of unused component props: the in-component dead-input direction that no Angular tooling covers. There is no `@angular-eslint` rule for it, and the Angular compiler only checks caller-side binding correctness (that a parent's `[input]="..."` matches a declared input), never "this declared input is wired to nothing inside the component." An input consumed only by a parent's template binding but read nowhere in its own component IS flagged, because the parent binding is satisfied while the component never uses the value.

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

Input names are harvested from the decorator form (`@Input() foo`, including the aliased `@Input('alias')` and the metadata `inputs: ['foo']` on the `@Component` decorator) and the signal form (`foo = input<T>()`, `input.required<T>()`, `model<T>()`); `model()` is treated as input-only (its two-way write half is not an emit). Usage is credited from the class body (`this.foo`) and from the component's template (interpolation, property and attribute bindings, structural directives, and template references). It stays false-positive-safe by abstaining on the whole component whenever it cannot see all consumption: any `extends` clause (an inherited template or base class may read the input), a `{...this}` spread, a JS-reserved-word input name, an accessor input (a `get` / `set` pair, where the setter side effect is the consumption), and `host:` decorator-metadata members are credited as used. The rule runs only when `@angular/core` is a declared dependency.

**When to act:** Read the input where it is meant to be used, or remove the `@Input()` / `input()` / `model()` declaration. There is no auto-fix. Suppress with `// fallow-ignore-next-line unused-component-input`, or set the rule to `off`.

<h3 id="unused-component-outputs">
  Unused component outputs
</h3>

An Angular component output (`@Output()` or the signal `output()`) that is emitted (`.emit(...)`) by no code in its own component. This is the emit-side sibling of unused component inputs: the in-component dead-output direction that no Angular tooling covers (the compiler checks caller-side listener correctness, that a parent's `(output)="..."` matches a declared output, never "this declared output is never emitted").

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

Output names are harvested from the decorator form (`@Output() saved = new EventEmitter()`, including the aliased `@Output('alias')` and the metadata `outputs: ['saved']` on the `@Component` decorator) and the signal form (`saved = output<T>()`); only `new EventEmitter()` initializers are harvested, so an observable-stream `@Output` (one wired to a `Subject` / `Observable` whose emissions fallow cannot follow) is left alone. Usage is credited from any `this.saved.emit(...)` call in the component. It stays false-positive-safe by abstaining on the whole component whenever it cannot see all emission: any `extends` clause, a `{...this}` spread, or a JS-reserved-word output name. The rule runs only when `@angular/core` is a declared dependency.

**When to act:** Emit the output where it is meant to fire, or remove the `@Output()` / `output()` declaration. There is no auto-fix (removing an output can break a deliberately-stable public component API). Suppress with `// fallow-ignore-next-line unused-component-output`, or set the rule to `off`.

<h3 id="prop-drilling">
  Prop drilling
</h3>

A React / Preact prop that is forwarded unused through two or more intermediate components before a component finally consumes it. "Forwarded unused" means a receiving component reads the prop's identifier only as the root of a child-JSX attribute value (so `<Child userName={user.name} />` counts: the `user` prop is projected straight through), never substantively. This is a cross-file graph signal that per-file linters (eslint-plugin-react) structurally cannot see.

| Severity     | Default |
| :----------- | :------ |
| Off (opt-in) | Yes     |

This rule is off by default; enable it with `prop-drilling: "warn"` (or `"error"`) under `rules`. When enabled it reports one located chain per drilled prop, with the source component that owns the value, each pass-through hop, and the consumer, each with file, line, and component name, so you or an agent can act. It stays false-positive-safe by abstaining on the whole chain whenever it cannot prove a clean pass-through: any `{...props}` / `{...rest}` spread, a `cloneElement` call, a component passed as a prop or render prop or function-as-children, a matching context `*.Provider` in the subtree (where drilling may be a deliberate choice), or any dynamic or unresolvable hop. The rule runs only when `react` / `react-dom` / `next` / `preact` is a declared dependency. When enabled it also contributes a small, capped penalty to the health score (one point per chain, capped at five); with the rule off the health score is unchanged.

**When to act:** Colocate the consumer with the data, lift the value to a React context / provider at a mid-chain hop, or compose the components so the intermediates no longer thread the prop. There is no auto-fix (the right refactor is a design choice). Suppress an individual chain with `// fallow-ignore-next-line prop-drilling`, or set the rule to `off`.

<h3 id="thin-wrapper">
  Thin wrapper
</h3>

A React / Preact component whose entire body is `return <Child {...props} />`: it renders a single child component, forwards all of its props through a spread, and adds no markup, no hooks, and no logic of its own. It is pure structural indirection: every call site could render the child directly. This is a cross-component graph signal that per-file linters cannot see.

| Severity     | Default |
| :----------- | :------ |
| Off (opt-in) | Yes     |

This rule is off by default; enable it with `thin-wrapper: "warn"` (or `"error"`) under `rules`. It reports the wrapper component (at its definition) and the child it forwards to. It stays false-positive-safe by abstaining on wrappers that are intentional indirection: a `forwardRef` or `memo` wrapper (the sanctioned way to make a child ref-able or to set a perf boundary), an exported (public-API) component (a library re-branding a child under its own name), a context-provider wrapper, a `cloneElement` call, a render-prop / function-as-children, or any wrapper whose body is more than the single spread-forwarded child (a host element, sibling, hook, or branch). The rule runs only when `react` / `react-dom` / `next` / `preact` is a declared dependency.

**When to act:** Inline the wrapper at its call sites and delete it, or give it a real responsibility (markup, defaults, logic). There is no auto-fix. Suppress with `// fallow-ignore-next-line thin-wrapper`, or set the rule to `off`.

<h3 id="duplicate-prop-shape">
  Duplicate prop shape
</h3>

Three or more React / Preact components, across two or more files, that declare an identical set of prop names, suggesting a shared `Props` type (or a base component) should be extracted. Cross-graph, which per-file linters cannot see.

| Severity     | Default |
| :----------- | :------ |
| Off (opt-in) | Yes     |

This rule is off by default; enable it with `duplicate-prop-shape: "warn"` (or `"error"`) under `rules`. It reports one finding per member of a group, naming the other components that share the shape. It stays noise-free by requiring an exact full-set match (a superset or subset does not group), a minimum of four significant prop names after stripping ubiquitous DOM-passthrough names (`id`, `className`, `children`, `style`, ...), and at least three components in at least two files, so two buttons that merely share `{label, onClick}` are never grouped. The rule runs only when `react` / `react-dom` / `next` / `preact` is a declared dependency, and abstains on components whose props are not statically harvestable (rest spread, imported-interface generics).

**When to act:** Extract a shared `Props` type or a base component the group can reuse. There is no auto-fix (the right abstraction is a design choice). Suppress with `// fallow-ignore-next-line duplicate-prop-shape`, or set the rule to `off`.

<h3 id="unused-load-data-keys">
  Unused load data keys
</h3>

A SvelteKit `load()` return-object key (in `+page.ts` / `+page.server.ts` and the `.js` variants) that is read by no consumer: not the sibling `+page.svelte`'s `data.<key>`, and not any project-wide `page.data.<key>` (Svelte 5 `$app/state`) or `$page.data.<key>` (Svelte 4 `$app/stores`). A dead returned key still runs its real server-side fetch / DB cost on every request for data nothing renders, and no other static tool catches it: `svelte-check` types `data` through the generated `$types` but never flags an unread returned key (the unused-input direction).

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

It stays false-positive-safe by abstaining whenever it cannot see all consumption: an unharvestable `load` body (a spread return, a non-literal or multi-branch return, a computed key, a wrapped / re-exported `load`), a sibling component that passes the whole `data` object opaquely (`data={data}`, `{...data}`, `fn(data)`, `const x = data`), a `+page.server.ts` whose universal `+page.ts` sibling reads or forwards its `data` param, and any project-wide reflective whole-object read of the page-data store (`Object.values(page.data)`), which abstains every route. Page-load data reaches only the sibling `+page.svelte`, so a `data.<key>` read in a `+layout.svelte` (which receives layout-load data, not page-load data) does not credit a page key. The rule runs only when `@sveltejs/kit` is a declared dependency. Layout loads (`+layout.ts` / `+layout.server.ts`) are not covered yet.

**When to act:** Read the key where the data is meant to be used, or remove it from the `load()` return (and drop the fetch that produced it if nothing else needs it). There is no auto-fix (a load fetch can have side effects, so removing a key is a human decision). Suppress with `// fallow-ignore-next-line unused-load-data-key`, or set the rule to `off`.

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

**Unknown issue-kind tokens (typos, obsolete names):** fallow accepts the recognized tokens on a multi-kind marker even when one of the tokens does not match any known issue kind, and reports each unknown token as a stale suppression. This guards against the silent-failure case where a typo (`unused_export` instead of `unused-export`) or a kind renamed in a newer release would otherwise discard every suppression on the same line. The diagnostic's explanation surfaces the verbatim token plus a "Did you mean ...?" suggestion when a known kind is within Levenshtein distance 2.

```typescript theme={null}
// Recognized + unknown on the same marker:
// fallow-ignore-next-line unused-export, complexity-typo
export const foo = 1;
// `unused-export` still suppresses `foo`; `complexity-typo` surfaces as a stale
// suppression so you can fix the typo or drop the unknown token.
```

JSON consumers can distinguish unknown-kind tokens from stale-but-known tokens via the additive `origin.kind_known` field on `stale_suppressions[].origin` (present and `false` only in the unknown-kind case; absent when the kind is recognized).

**When to act:** Remove the stale suppression comment or JSDoc tag. For unknown-kind reports, fix the typo or drop the unknown token; the rest of the marker continues to apply. 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 114 frameworks and tools via `package.json` and config files, so most convention-based exports (Next.js pages, Storybook stories, Jest configs, tap suites, tsd declaration tests, and Contentlayer content roots) 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.
