- Formatter: “Does this file look consistent?”
- Linter: “Does this file follow the rules?”
- Fallow: “Is this codebase structurally sound?”
Three pillars of code quality
Formatter
Prettier, Biome, dprint. Enforces consistent formatting across all files: indentation, quotes, trailing commas. Works on a single file. No understanding of imports or project structure.
Linter
ESLint, Biome, oxlint. Enforces coding rules and detects local code smells: undefined variables, missing dependencies in
useEffect, suspicious patterns. Works on a single file, with optional type context from tsc.Codebase intelligence
Fallow. Finds issues that only exist at the project level: unused files, orphaned exports, circular dependencies, dependency graph violations. Requires building and traversing the full module graph.
What each tool finds
[1] Unused exports.
eslint-plugin-import/no-unused-modules rebuilds the full import graph on every lint run. On projects with 20-30K LOC this adds ~320 seconds to total lint time (#1487). Produces incorrect results with import { name as alias } (#1339). Does not follow barrel file re-export chains. Neither Biome nor oxlint have an equivalent rule.
[2] Circular imports. ESLint: eslint-plugin-import/no-cycle takes 45-305 seconds on large projects with known OOM failures (#2348, #2076). Teams commonly set maxDepth: 1 to stay fast, missing longer cycles. Only covers ES import, not require(). Biome v2: noImportCycles is Rust-based and graph-aware, but Biome documents it as computationally expensive. oxlint: no-cycle in Rust, faster than the JS version. Fallow: builds and caches the graph once, no depth cap, covers both import and require().
[3] Unused vs unlisted deps. no-extraneous-dependencies (ESLint) and noUndeclaredDependencies (Biome) catch imports of packages missing from package.json. That is the opposite problem. No linter detects packages listed in package.json that are never imported. Fallow finds both.
[4] Boundaries. ESLint: eslint-plugin-boundaries enforces import rules between element types with capture groups and templates. no-restricted-paths enforces directory-level restrictions but requires enumerating every forbidden pair. Neither follows re-export chains, so a boundary can be bypassed via a barrel file. Biome: noRestrictedImports accepts plain string paths only, no globs or patterns. oxlint: no boundary rules. Fallow: zone presets (bulletproof, layered, hexagonal, feature-sliced) with re-export chain tracking.
Detailed comparison
Unused exports
Fallow builds a persistent module graph, resolves re-export chains iteratively, and handles TypeScript path aliases.If you’re building a library, your public API exports are intentionally consumed externally. Mark them with
/** @public */ (or @internal, @beta, @alpha) JSDoc tags or configure your entry points so fallow doesn’t flag them.Unused files
no-unused-modules detects exports with no importers, but does not detect files that are never imported at all. A file with no exports and no importers (an orphaned utility, a deleted component’s leftover test file) does not trigger any ESLint rule.
Detecting unreachable files requires starting from entry points and walking the full import graph. ESLint has no concept of entry points. It lints files; it does not traverse graphs from roots. Fallow reads entry points from package.json fields and framework conventions (Next.js pages, Vite entry, etc.), then marks every file not reachable from those roots.
Circular dependencies
// fallow-ignore-next-line circular-dep to suppress them, the same way you’d use an ESLint disable comment. No tool automatically distinguishes intentional from accidental cycles.
Architecture boundary violations
Fallow tracks re-export chains when evaluating boundaries. Ifshared/index.ts re-exports a symbol from core/, an import of that symbol is attributed to core/, not shared/, when checking boundary rules. Zone presets (bulletproof, layered, hexagonal, feature-sliced) cover common architecture patterns. Zones use glob patterns, and rules are expressed as allowlists (“this zone can only import from these zones”) rather than enumerating forbidden paths.
Recommended CI pipeline
Run all three in parallel. Some linter plugins (no-cycle, no-unused-modules) overlap with fallow on a subset of issues. If you already have those enabled, fallow covers the same ground faster and adds the issues linters cannot detect (unused files, unused deps, duplication, churn hotspots). If you don’t have them enabled, fallow covers everything in one pass:
Fallow’s GitHub Action automatically uploads SARIF results to GitHub Code Scanning, giving you inline annotations on PR diffs. Linter results typically appear via separate status checks or linter-review actions. For GitLab CI and other runners, see CI integration.
What to use when
For a comparison with knip, which operates at the same graph level as fallow, see Fallow vs Knip.
Quick Start
Add fallow to your project in under a minute.
CI Integration
Full guide for GitHub Actions, GitLab CI, and other pipelines.
Dead code explained
Fallow’s dead code and dependency findings, with examples.
Configuration
Entry points, ignore patterns, severity rules.