# Dead code analysis

> Find the code, exports, and dependencies you can delete from a TypeScript or JavaScript project. Fallow also reports class members, circular dependencies, and boundary violations from the same module graph.

> Page index of all docs: https://fallow.tools/llms.txt
> Docs MCP server (search and read the docs): https://fallow.tools/docs/mcp
> HTML version of this page: https://fallow.tools/docs/analysis/dead-code/

`fallow dead-code` tells you which files, exports, and dependencies you can delete. It builds a module graph from your entry points and reports everything that the graph does not reach.

The graph build is deterministic, so a developer, a CI pipeline, and a coding assistant get the same result for the same code.

```bash
fallow dead-code
```

## Issue types

Fallow reports these core issue types:

| Issue type | Description |
|:-----------|:------------|
| **Unused files** | Files not reachable from any entry point |
| **Unused exports** | Exported symbols never imported elsewhere |
| **Unused types** | Type aliases and interfaces never referenced |
| **Unused dependencies** | Packages in `dependencies` never imported or used as script binaries |
| **Unused devDependencies** | Packages in `devDependencies` never imported or used as script binaries |
| **Unused optionalDependencies** | Packages in `optionalDependencies` never imported or used as script binaries |
| **Unused enum members** | Enum values never referenced |
| **Unused class members** | Class methods and properties never referenced outside their class. Tracks inheritance, skips framework lifecycle methods, and lets you exclude decorators with `ignoreDecorators` |
| **Unused catalog entries** | Entries in the `catalog:` or `catalogs:` maps of `pnpm-workspace.yaml` that no workspace package references with the `catalog:` protocol |
| **Unresolved imports** | Import specifiers that cannot be resolved |
| **Unlisted dependencies** | Imported packages missing from `package.json` |
| **Duplicate exports** | Same symbol exported from multiple modules |
| **Circular dependencies** | Modules that import each other directly or transitively |
| **Boundary violations** | Imports that cross user-defined architecture zone boundaries |
| **Type-only dependencies** | Production dependencies that only `import type` statements use (move them to devDependencies) |
| **Test-only dependencies** | Production dependencies that only test files import (move them to devDependencies) |
| **Stale suppressions** | `fallow-ignore` comments or `@expected-unused` JSDoc tags that no longer match any issue |

Fallow also has framework-specific rules, such as unused component props and route collisions, and opt-in rules, such as private type leaks. To set the severity of a rule, see [Rules & severity](https://fallow.tools/docs/configuration/rules/). To see the filter flag of each issue type, run `fallow dead-code --help`. For circular dependencies, boundary violations, and the other architecture findings, use [`fallow architecture`](https://fallow.tools/docs/cli/architecture/).

Typical output with more than one issue type:

**$ fallow dead-code**

```bash
── Unused Code ─────────────────────────────────────

● Unused files (3)
  scripts/check-db.ts
  src/features/forecasting/hooks/useCashFlowForecast.ts
  src/features/forecasting/hooks/useIncomeForecast.ts
  Files not reachable from any entry point
  https://fallow.tools/docs/explanations/dead-code/#unused-files
  To suppress: // fallow-ignore-file unused-file

● Unused exports (8)
  test/component-helpers.tsx (5)
    :1 act (re-export)
    :1 waitFor (re-export)
    :1 within (re-export)
    :33 ThemeContext
    :58 ToastContext
  src/server/jobs/queue.ts (3)
    :61 enqueueJobDelayed
    :206 sweepStuckProcessingJobs
    :276 getDeadLetterJobs
  Exported symbols with no known consumers
  https://fallow.tools/docs/explanations/dead-code/#unused-exports
  To auto-fix: fallow fix --dry-run
  To suppress: // fallow-ignore-next-line unused-export
  3 in src, 5 in test files

✗ 3 files · 8 exports (0.16s)
```

## Filtering by issue type

To report only some issue types, pass their flags:

```bash
fallow dead-code --unused-files
fallow dead-code --unused-exports --unused-types
fallow dead-code --unresolved-imports --unlisted-deps
```

## Findings in unused files

When fallow reports a file as an unused file, it does not list the unused exports, unused types, unused class members, and unused enum members in that file. When you delete the file, you also remove these findings. Thus they add no information to the report. Other finding kinds in an unused file stay in the report, for example an unresolved import.

The report counts the hidden findings:

| Output | Where the count is |
|:-------|:-------------------|
| `json` | The `cascade_hidden` integer at the root of the output. With `--group-by`, the grouped output has it at its root, and no group has it. |
| `sarif` | `runs[0].properties.cascadeHidden` |
| `markdown` | One note line that gives the count and names `--show-cascade` |

When the count is 0, all three outputs omit it. An example of the markdown note:

```markdown
_3 findings in unused files are hidden; use `--show-cascade` to list them._
```

The `codeclimate`, `compact`, and `github-annotations` formats do not show the count. The markdown output with `--group-by` does not show it either. The `summary` counts and `total_issues` count only the findings in the report.

To list the hidden findings, use one of these options:

- The `--show-cascade` flag, for one run of `fallow`, `fallow dead-code`, `fallow audit`, or `fallow fix`.
- The [`showCascade`](https://fallow.tools/docs/configuration/overview/#showcascade) config key, set to `true`.
- The `show_cascade` parameter of the [MCP tools](https://fallow.tools/docs/integrations/mcp/#notable-tool-parameters) `analyze`, `check_changed`, `audit`, `fix_preview`, and `fix_apply`, and of the `combined` call in Code Mode.
- The `showCascade` option of the [Node bindings](https://fallow.tools/docs/integrations/node-bindings/#dead-code-options).

```bash
fallow dead-code --show-cascade
```

Know these rules:

- The [health](https://fallow.tools/docs/explanations/health/) vital signs and the health score still count the hidden findings. Bare `fallow` and `fallow health` give the same values with and without `--show-cascade`.
- When an issue-type selection does not include unused files, fallow hides nothing. For example, `fallow dead-code --unused-exports` lists the unused exports of unused files.
- A `// fallow-ignore-file unused-file` comment removes the file from the report. Fallow also hides the export and member findings of that file.
- `fallow fix` does not fix a hidden finding. Delete the unused file, or pass `--show-cascade` to fix these findings too.
- With `--changed-since` or `--workspace`, `cascade_hidden` counts only the findings in the scoped files.
- A [baseline](https://fallow.tools/docs/analysis/dead-code/#baseline-comparison) that you saved with an older version still matches. A baseline that you save now does not contain the hidden findings.
- A [regression baseline](https://fallow.tools/docs/cli/dead-code/#regression-detection) that you saved with an older version counts the hidden findings. After the upgrade it allows more new findings than before. Save it again with `--save-regression-baseline`.

## Optional props that callers never supply

[Absent component props](https://fallow.tools/docs/explanations/dead-code/#absent-component-props) are optional props that a component reads but no inspected reachable caller supplies. Enable `rules.absent-component-props` with `warn` or `error`. The rule starts off because a default or a stable API can make an omitted prop intentional.

Fallow reports candidates only when it knows at least one reachable caller and can account for the component's callers statically. Unknown spreads, forwarding, dynamic use, and external package users prevent that conclusion. A finding includes the declaration, inspected callers, and whether a default exists. Review that evidence before changing the component. Fallow does not remove the prop or its conditional UI automatically.

## Output formats

#### Human (default)

Colored terminal output for people to read.

```bash
fallow dead-code --format human
```

```ansi
── Unused Code ─────────────────────────────────────

● Unused files (1)
  src/server/jobs/worker.ts
  Files not reachable from any entry point
  https://fallow.tools/docs/explanations/dead-code/#unused-files

● Unused exports (3)
  src/server/jobs/queue.ts (2)
    :61 enqueueJobDelayed
    :206 sweepStuckProcessingJobs
  src/components/Card/index.ts
    :1 CardFooter
  Exported symbols with no known consumers
  https://fallow.tools/docs/explanations/dead-code/#unused-exports
  To auto-fix: fallow fix --dry-run
  To suppress: // fallow-ignore-next-line unused-export

✗ 1 file · 3 exports (0.16s)
```

#### JSON

JSON for scripts and tools.

```bash
fallow dead-code --format json
```

```json
{
  "kind": "dead-code",
  "schema_version": 10,
  "version": "3.33.1",
  "elapsed_ms": 160,
  "total_issues": 401,
  "unused_files": [
    {
      "path": "src/server/jobs/worker.ts",
      "finding_id": "dc1:unused-file:5d2e8c41a07b93f6"
    }
  ],
  "unused_exports": [
    {
      "path": "src/server/jobs/queue.ts",
      "export_name": "enqueueJobDelayed",
      "is_type_only": false,
      "line": 61,
      "col": 0,
      "span_start": 1420,
      "is_re_export": false,
      "finding_id": "dc1:unused-export:81a349a3b9ea3b15"
    }
  ],
  "unused_dependencies": [
    {
      "package_name": "@trpc/react-query",
      "location": "dependencies",
      "path": "package.json",
      "line": 18,
      "used_in_workspaces": ["apps/web"],
      "finding_id": "dc1:unused-dependency:3f9a0c7e12b4d865"
    }
  ]
}
```

Each finding has a stable `finding_id`. The id does not change when lines move, so you can track one finding across runs. See [`finding_id`](https://fallow.tools/docs/cli/dead-code/#finding-id).

#### SARIF

SARIF output for GitHub Code Scanning and other static analysis tools.

```bash
fallow dead-code --format sarif
```

```json
{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [{
    "tool": { "driver": { "name": "fallow", "version": "3.33.1" } },
    "results": [
      {
        "ruleId": "fallow/unused-export",
        "level": "error",
        "message": { "text": "Export 'enqueueJobDelayed' is never imported by other modules" },
        "locations": [{
          "physicalLocation": {
            "artifactLocation": { "uri": "src/server/jobs/queue.ts" },
            "region": { "startLine": 61, "startColumn": 1 }
          }
        }],
        "partialFingerprints": {
          "tools.fallow.fingerprint/v1": "c4e1a9b07f2d3865",
          "primaryLocationLineHash/v1": "c4e1a9b07f2d3865",
          "fallowFinding/v1": "dc1:unused-export:81a349a3b9ea3b15"
        }
      }
    ]
  }]
}
```

A dead-code result has the key `fallowFinding/v1` in `partialFingerprints`. Its value is the [`finding_id`](https://fallow.tools/docs/cli/dead-code/#finding-id) of the finding in JSON output, so you can join a SARIF result to its JSON finding. The keys `tools.fallow.fingerprint/v1` and `primaryLocationLineHash/v1` did not change, so GitHub code scanning keeps each open alert after an upgrade.

Some findings give one result for each location: an unlisted dependency gives one result for each import site, and a duplicate export gives one result for each location. These results do not have the `fallowFinding/v1` key, because one id would identify more than one result. `fallow report --from` with a saved report from an older version also gives no key. Security results keep their own `fallowSecurity/v2` key.

#### Compact

One line per issue, for `grep` and editor integrations.

```bash
fallow dead-code --format compact
```

```text
unused-file:src/server/jobs/worker.ts
unused-file:src/server/jobs/cron.ts
unused-export:src/server/jobs/queue.ts:61:enqueueJobDelayed
unused-export:src/components/Card/index.ts:1:CardFooter
```

#### Markdown

Markdown for PR comments and documentation.

```bash
fallow dead-code --format markdown
```

```markdown
## Fallow: 4 issues found

### Unused files (2)

- `src/server/jobs/worker.ts`
- `src/server/jobs/cron.ts`

### Unused exports (2)

- `src/server/jobs/queue.ts`
  - :61 `enqueueJobDelayed`
  - :206 `sweepStuckProcessingJobs`
```

To post it as a PR comment, pipe it to `gh pr comment`:

```bash
fallow dead-code --format markdown | gh pr comment --body-file -
```

#### CodeClimate

CodeClimate JSON, which GitLab Code Quality shows as inline MR annotations.

```bash
fallow dead-code --format codeclimate
```

```json
[
  {
    "type": "issue",
    "check_name": "fallow/unused-file",
    "description": "File is not reachable from any entry point",
    "categories": ["Bug Risk"],
    "severity": "major",
    "fingerprint": "a1b2c3d4e5f67890",
    "location": {
      "path": "src/server/jobs/worker.ts",
      "lines": { "begin": 1 }
    }
  },
  {
    "type": "issue",
    "check_name": "fallow/unused-export",
    "description": "Export 'enqueueJobDelayed' is never imported by other modules",
    "categories": ["Bug Risk"],
    "severity": "major",
    "fingerprint": "b2c3d4e5f6789012",
    "location": {
      "path": "src/server/jobs/queue.ts",
      "lines": { "begin": 61 }
    }
  }
]
```

The `fingerprint` of a dead-code issue comes from its [`finding_id`](https://fallow.tools/docs/cli/dead-code/#finding-id), so it does not change when lines move. GitLab Code Quality therefore keeps an issue open when you add lines above it. The same package unused in two workspaces gives two issues with two fingerprints. Duplication, health and security fingerprints did not change.

> **Warning:** On the first run after the upgrade to this fingerprint form, GitLab Code Quality and other tools that key on the fingerprint show each dead-code finding as resolved and as new one time. After that run, a line shift keeps the issue. A saved report without finding ids, rendered with `fallow report --from`, keeps the old line-based fingerprint.

Add this job to GitLab CI:

```yaml
fallow:
  script: npx fallow dead-code --format codeclimate > gl-code-quality-report.json
  artifacts:
    reports:
      codequality: gl-code-quality-report.json
```

## Incremental analysis

To check only the files that changed since a git ref, pass `--changed-since`:

```bash
fallow dead-code --changed-since main
fallow dead-code --changed-since HEAD~5
```

In CI, this reports only the issues that a pull request adds.

**$ fallow dead-code --changed-since main**

```bash
── Unused Code ─────────────────────────────────────

● Unused exports (2)
  src/features/savings/hooks/usePotGroups.ts
    :8 usePotGroupTotals
  src/server/jobs/queue.ts
    :276 getDeadLetterJobs
  Exported symbols with no known consumers
  https://fallow.tools/docs/explanations/dead-code/#unused-exports

✗ 2 exports (0.04s)
```

## Baseline comparison

To adopt fallow on an existing codebase, save the current issues as a baseline. Fallow then fails only on new issues:

```bash
# Save current issues as baseline
fallow dead-code --save-baseline fallow-baselines/dead-code.json

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

A dead-code baseline stores one line-free key for each finding: the rule, the path and the names. When you add lines above a finding, the baseline still hides it. Each saved entry hides one finding, so a second finding with the same key is new. A baseline from an earlier release still loads. Save it again once to get the line-free form. See [Incremental](https://fallow.tools/docs/cli/dead-code/#incremental).

## Debugging

To see why fallow counts an export, a file, or a dependency as used or unused, trace it:

```bash
fallow dead-code --trace src/utils.ts:formatDate
fallow dead-code --trace-file src/utils.ts
fallow dead-code --trace-dependency lodash
```

## How it works

Fallow parses your code with Oxc and resolves bindings with scope analysis. It does not run the TypeScript compiler and does not use type information, which keeps the analysis fast.

```mermaid
flowchart TB
  A["Discovery<br/>Entry points from package.json + plugins"] --> B["Parsing<br/>Parallel with Oxc + oxc_semantic + rayon"]
  B --> C["Resolution<br/>Import specifiers to file paths"]
  C --> D["Graph<br/>Module graph with re-export chains"]
  D --> E["Analysis<br/>Walk from entry points, report unreachable code"]
```

> **Note:** Fallow works best with `isolatedModules: true`, which esbuild, swc, and Vite require. `oxc_semantic` scope analysis finds import bindings that the file never reads. Older tsc-only projects without `isolatedModules` can still get edge cases with type-only imports.

## Script binary analysis

A package that you only use from a `package.json` script is not an unused dependency. Fallow reads your scripts to find these packages. For `"lint": "oxlint src/"`, fallow sees that the `oxlint` package supplies the `oxlint` binary and marks the package as used.

Fallow reads these parts of a script:

- **Binary names.** Fallow maps commands such as `tsc`, `vitest`, or `next` to their packages (`typescript`, `vitest`, `next`). It does not report these packages as unused, even when no source file imports them.
- **`--config` arguments.** When a script names a config file (for example `jest --config jest.e2e.config.ts`), fallow makes that file an entry point, so it does not report the file as unused.
- **File path arguments.** Files that a script names directly (for example `node scripts/seed.js`) also become entry points.
- **Env wrappers and package manager runners.** Fallow removes the `cross-env`, `dotenv`, `env`, `npx`, `pnpx`, `yarn dlx`, `pnpm exec`, `npm exec --`, `yarn run`, `bun run`, or `varlock run --` prefix to find the real binary. The flags of these prefixes are also removed, for example `dotenv -e .env.ci --`, `pnpm --filter web exec`, and `pnpm -r exec`. `yarn <bin>`, `yarn run <bin>`, `pnpm <bin>`, and `bun run <bin>` count as the binary when no script has that name.
- **Script calls.** A call such as `npm run lint -- src/a.ts`, `npm run lint src/a.ts`, `yarn lint src/a.ts`, or `pnpm lint src/a.ts` runs the `lint` script with the extra arguments. Fallow reads it as the script body plus those arguments, as the package manager does. As in npm 7 and later, npm forwards the positional arguments without `--` and reads the `-`-prefixed arguments before `--` as npm config. npm config flags that take a value, such as `--tag`, `--otp`, or `--node-options`, also consume the next argument, so `npm run lint --tag next src/a.ts` forwards only `src/a.ts`. A declared script with the name of a tool runs instead of the tool: with `"oxlint": "node tools/check.js"`, `yarn oxlint src/a.ts` keeps `src/a.ts` as an entry point.
- **Other workspace packages and directories.** A command in a workspace package that it selects resolves its file arguments against the directory of that package. With the `web` package in `packages/web`, `yarn workspace web node scripts/a.ts`, `pnpm --filter web exec tsx scripts/a.ts`, `npm exec -w web -- tsx scripts/a.ts`, and a call of a `web` script such as `npm run -w web gen -- scripts/a.ts` make `packages/web/scripts/a.ts` an entry point. A pnpm filter can be a name, a name glob (`'@acme/*'`), a directory (`./packages/*`, `{packages/web}`), or an exclusion (`'!web'`). A selection of several packages resolves the file in each package where the file exists. This includes a command in every package: `pnpm -r exec tsx scripts/a.ts`, `yarn workspaces foreach -A exec tsx scripts/a.ts` (narrowed by `--include` and `--exclude`), `yarn workspaces run gen scripts/a.ts`, and `npm --workspaces run gen -- scripts/a.ts`. `yarn workspaces foreach -A` also runs in the root package, as yarn does, and its `--include` and `--exclude` match a workspace name or directory (`.` is the root). `pnpm -w`, a pnpm filter with the name or directory of the root package, and `yarn workspace` with the name of the root package also select the root package. An npm workspace name or directory does not select it, as in npm. `--include-workspace-root` adds the root package: in pnpm to `-r` and to a filter that only excludes packages (`--filter '!web'`), and in npm to every workspace selection (`-w web`, `--workspaces`), also with the short form `-iwr`. Without it, `pnpm -r`, `yarn workspaces run`, and `npm --workspaces` leave out the root package. From a workspace package, `npm --workspaces` selects only that package. A script call in the directory of a workspace package, such as `pnpm -C packages/web run gen scripts/a.ts`, `npm --prefix packages/web run gen -- scripts/a.ts`, or `yarn --cwd packages/web gen scripts/a.ts`, runs the script of that package with the arguments. The scripts of the root package and of each workspace package resolve these forms in the same way. A `start` script that calls a script in the selected packages, such as `pnpm -r run serve` or `pnpm -C packages/web run serve`, makes the files of that script runtime entry points in each package, as for the files of `start` itself. A linter target in a selected package, such as `yarn workspace web oxlint src/a.ts` or `pnpm -r run lint -- src/a.ts`, still makes no entry point. A pnpm dependency or changed-package filter (`web...`, `[origin/main]`), another `yarn workspaces foreach` selection (`--since`, `--recursive`, `--from`, `--worktree`, `--no-private`), and the task runners `turbo`, `nx`, and `lerna` (`turbo run lint -- src/a.ts`) make no entry points. The binary still counts as a used dependency. A command in another directory (`pnpm -C docs exec tsx scripts/a.ts`, `npm --prefix`, `yarn --cwd`) resolves its file arguments against that directory. `yarn node <file>` runs the file with Node.js, also after `yarn --cwd <dir>` or `yarn workspace <name>`.

```json
{
  "scripts": {
    "build": "tsc && vite build",
    "test": "vitest --config vitest.config.ts",
    "lint": "cross-env NODE_ENV=production oxlint src/"
  }
}
```

In this example, fallow detects `typescript`, `vite`, `vitest`, and `oxlint` as used dependencies, and `vitest.config.ts` as an entry point.

### Formatter and linter targets

A formatter or a linter reads its file arguments, but it does not run them. So the file arguments of these tools do not become entry points: `oxlint src/`, `oxfmt --check .`, `eslint src/a.ts`, and `prettier --check "**/*.ts"` keep every file that no import reaches reportable as unused. This applies to all the command forms in the list above: a direct call, a package manager runner, an env wrapper, and a script call. For example, with the script `"lint": "oxlint"`, the call `npm run lint -- src/a.ts` does not make `src/a.ts` an entry point.

Fallow knows these tools: alex, Biome, CSpell, dependency-cruiser, dprint, EditorConfig checkers, ember-template-lint, ESLint (and `eslint_d`), HTMLHint, jscpd, JSHint, madge, markdownlint, markuplint, Oxfmt, Oxlint, Prettier, remark, Secretlint, Standard, Stylelint, textlint, TSLint, and XO.

The tool itself is still a used dependency, and its `--config` file is still tracked. A module that the tool loads through a flag also stays reachable:

- A custom formatter, parser, or reporter: `eslint -f ./tools/formatter.js`, `stylelint --custom-formatter ./fmt.js`.
- A local plugin: `prettier --plugin=./tools/plugin.mjs`, `remark --use ./plugins/lint.mjs`.
- A rule directory: `eslint --rulesdir ./rules`, `textlint --rulesdir ./rules`. Fallow keeps the source files directly in that directory reachable.

### Turn off entry points for a command

Some other commands also read files as data. For example, a code generator that reads `src/**/*.ts` does not run those files. List such a command in [`ignoreCommandEntries`](https://fallow.tools/docs/configuration/overview/#ignorecommandentries), and its file arguments do not become entry points:

```jsonc
{
  "ignoreCommandEntries": ["my-codegen"]
}
```

The command is still a used dependency, and its `--config` file is still tracked. The option applies to `package.json` scripts, CI files, and infrastructure files. It also applies when a script call runs the command, such as `npm run gen -- src/input.ts` with the script `"gen": "my-codegen"`. To turn off entry points from all commands, use `["*"]` and declare the real entry points in `entry`. `["*"]` also drops the modules that a linter loads through a flag, such as `eslint -f ./tools/fmt.js`, so list those in `entry` too.

## Infrastructure entry points

Worker processes and migration scripts often start from a Dockerfile or a CI job, not from an import. Fallow reads these infrastructure files and makes the source files they name entry points, so it does not report those files as unused.

Fallow reads these files:

| File type | What fallow extracts |
|:----------|:--------------------|
| **Dockerfiles** (`Dockerfile`, `Dockerfile.*`, `*.Dockerfile`) | `RUN node`, `CMD`, `ENTRYPOINT`, esbuild invocations |
| **Procfiles** | Process definitions (e.g., `worker: node dist/worker.js`) |
| **fly.toml** / **fly.*.toml** | `release_command` and process definitions |
| **CI pipelines** (`.gitlab-ci.yml`, `.github/workflows/*.yml`) | `npx` and binary invocations in CI steps |

Fallow looks for these files in the project root and in common subdirectories (`config/`, `docker/`, `deploy/`).

The rules for [formatter and linter targets](https://fallow.tools/docs/analysis/dead-code/#formatter-and-linter-targets) and for `ignoreCommandEntries` also apply here. A CI step such as `npx oxlint src/` does not make `src/` files entry points.

```dockerfile
# Fallow detects scripts/migrate.ts and src/worker.ts as entry points
FROM node:20
RUN node scripts/migrate.ts
CMD ["node", "src/worker.ts"]
```

## Dynamic import resolution

Files that you load with a computed path, such as locale files or icons, are not unused. For `` import(`./locales/${lang}.json`) ``, the target is not known at analysis time. Fallow converts the pattern into a glob and matches it against the project files.

Fallow supports these patterns:

| Pattern | Example | Resolved as |
|:--------|:--------|:------------|
| Template literals | `` import(`./icons/${name}.svg`) `` | `./icons/*.svg` |
| String concatenation | `import("./routes/" + path)` | `./routes/*` |
| `import.meta.glob` | `import.meta.glob("./modules/*.ts")` | `./modules/*.ts` |
| `require.context` | `require.context("./themes", true, /\.css$/)` | `./themes/**/*.css` |

Fallow marks the matched files as reachable, so it does not report them as unused. This covers locale files, icon sets, route modules, and other directories that follow a naming convention.

A pattern match uses every value export of the matched file, `default` included. A type export (a type alias or an interface) of a matched file is used only by an import that names it.

> **Note:** Fallow cannot resolve a path that is fully computed at runtime (for example `import(userInput)`). Add those directories to `entry` in your config to make them entry points.

## Re-export chain resolution

Fallow follows `export *` chains through any number of barrel files, so an export that you import through a barrel counts as used.

```typescript
// utils/math.ts
export const add = (a: number, b: number) => a + b;

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

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

// app.ts
import { add } from './src';
```

```mermaid
flowchart TB
  A["app.ts<br/>import ﹛ add ﹜ from './src'"] --> B["src/index.ts<br/>export * from './utils'"]
  B --> C["utils/index.ts<br/>export * from './math'"]
  C --> D["utils/math.ts<br/>export const add ✓"]
```

Here, fallow follows the import of `add` in `app.ts` through `src/index.ts` and `utils/index.ts` to `utils/math.ts` and marks `add` as used.

Fallow handles these cases:

- **Multi-level chains.** Fallow follows `export *` re-exports at any depth, up to the original declaration.
- **Cycles.** Fallow detects circular re-export chains (for example, `a` re-exports from `b` and `b` re-exports from `a`) and stops without an error.
- **Mixed re-exports.** Fallow tracks named re-exports (`export { foo } from './bar'`) and namespace re-exports (`export * from './bar'`).

When two `export *` sources supply the same name, ECMAScript treats the name
as ambiguous, and the barrel does not export it. Until you fix the collision,
Fallow does not report the declarations, members, components, or injections
that supply the name. Run [`fallow trace FILE:NAME`](https://fallow.tools/docs/cli/trace/) to see the
source files and whether the collision is in the type namespace, the value
namespace, or both.

## Namespace import narrowing

A namespace import does not mark every export as used. For `import * as ns from './module'`, fallow looks for member accesses (`ns.foo`, `ns.bar`) and destructuring (`const { foo, bar } = ns`) in the importing file. Only those exports count as used.

```ts
import * as utils from './utils';

// Only foo and bar are marked as used. baz remains unused
const { foo } = utils;
utils.bar();
```

This works for static imports, dynamic imports (`const mod = await import('./x')`), and `require` (`const mod = require('./x')`).

Fallow also uses `oxc_semantic` scope analysis to find imports whose binding the file never reads. If a file has `import { foo } from './utils'` and never uses `foo`, that import does not count as a reference to the `foo` export.

> **Note:** Some patterns use the whole object: `Object.values(ns)`, `{ ...ns }`, `for (const k in ns)`, and rest destructuring (`const { a, ...rest } = ns`). Fallow cannot tell which members these patterns use, so it marks all exports as used.

## Class member detection

Fallow reports public class methods and properties that no code outside the class references. It takes class inheritance, decorators, and framework conventions into account.

```typescript
class OrderService {
  // Used: called in checkout.ts
  async createOrder(items: Item[]) { /* ... */ }
  
  // Unused: never called outside this class
  private validateItems(items: Item[]) { /* ... */ }
  
  // Excluded: decorator indicates runtime wiring
  @Post('/orders')
  handleCreateOrder() { /* ... */ }
}
```

Fallow handles these cases without configuration:

- **Inheritance.** When code calls a parent class method through the parent type, fallow marks the child override as used.
- **Decorators.** By default, fallow skips decorated members. Decorators such as `@Get()`, `@Column()`, and `@Injectable()` mean that a framework calls the member at runtime.
- **Framework lifecycle methods.** Fallow never reports `componentDidMount`, `ngOnInit`, `connectedCallback`, and other framework lifecycle methods.
- **Whole-object patterns.** `Object.values(instance)`, `Object.keys()`, spread operators, and `for..in` loops mark all members as used.

### Opting decorators out via `ignoreDecorators`

Some decorators do not mean that a framework calls the method, for example Playwright's `@step("label")` or your own `@measure`, `@log`, or `@retry`. List these names in `ignoreDecorators`. Fallow then checks a method that has only these decorators like an undecorated method.

```jsonc
// .fallowrc.json
{
  "ignoreDecorators": ["@step"]
}
```

Fallow still skips a method that has any decorator that is not in the list. A method with `@step` and `@Inject` stays framework-managed.

See [`ignoreDecorators`](https://fallow.tools/docs/configuration/overview/#ignoredecorators) for the matching rules and the unmatched-entry warning.

> **Note:** Class member detection uses syntactic analysis and does not run the TypeScript compiler. Fallow tracks member access through the import graph and does not resolve types.

## CSS and SCSS tracking

Fallow tracks CSS and SCSS imports, so stylesheets that your code uses are not unused files. It resolves SCSS `@use`, `@forward`, and partials (`_prefix` files). It treats CSS Module class names as named exports and tracks them through `styles.className` accesses.

For details, see [CSS, SCSS, and Tailwind analysis](https://fallow.tools/docs/analysis/css-analysis/).

## Entry-point partial unused exports

An entry point can also export helpers that nothing uses. Before v2.15.0, fallow marked all exports of an entry point as used. Since v2.15.0, fallow reports exports of an entry file that no other module in the project imports. A plugin or the `entry` config makes a file an entry point.

This helps most with framework convention files, such as Next.js pages and SvelteKit routes. The framework uses specific named exports (such as `default`, `loader`, or `getStaticProps`), and the file can also export helpers that nothing uses.

> **Note:** Fallow never reports an entry-point file as an unused file. It reports only the exports in that file that have zero references.

## Cross-reference with duplication

Code that is duplicated and unused is a good place to start the cleanup. `fallow dead-code --include-dupes` compares dead code findings with duplication analysis. Fallow reports clone instances in unused files, or that overlap unused exports, as **combined high-priority findings**.

```bash
fallow dead-code --include-dupes
```

When you delete a block that is duplicated *and* unused, you remove dead code and duplication in one change.

The comparison finds:

- **Clone instances in unused files.** When no entry point reaches a file and the file has duplicated code, fallow raises the priority of the duplication finding.
- **Clone instances that overlap unused exports.** When an unused export has code that is duplicated elsewhere, fallow reports both findings together.

## Circular dependency performance

`fallow dead-code --circular-deps` runs the full analysis (dead code, dependencies, boundaries, and cycles). All runs are cold, with fallow 2.100.0 on an Apple M5. The flag is now a deprecated alias for `fallow architecture --cycles`, which runs the same analysis.

| Project | Files | Time | Cycles |
|:--------|------:|-----:|-------:|
| [zod](https://github.com/colinhacks/zod) | 174 | 43ms | 0 |
| [preact](https://github.com/preactjs/preact) | 244 | 75ms | 5 |
| [fastify](https://github.com/fastify/fastify) | 286 | 97ms | 20 |
| [vue/core](https://github.com/vuejs/core) | 522 | 137ms | 58 |
| [TypeScript](https://github.com/microsoft/TypeScript) | 38,146 | 2.18s | 114 |
| [next.js](https://github.com/vercel/next.js) | 20,552 | 3.00s | 178 |
| [astro](https://github.com/withastro/astro) | 2,859 | 3.81s | 42 |

> **Info:** Fallow finds cycles in the module graph that dead code analysis already builds, so cycle detection does not build a second graph.

## See also

- [CLI: dead-code](https://fallow.tools/docs/cli/dead-code/): Full reference for the `fallow dead-code` command and its flags.
- [Rules & Severity](https://fallow.tools/docs/configuration/rules/): Control which issue types are errors, warnings, or disabled.
- [Auto-fix](https://fallow.tools/docs/analysis/auto-fix/): Remove the dead code that fallow finds.
