Skip to content

The Drift Log · Software inventory you can trust

Detecting Orphaned Functions in a Monorepo

8 September 2026 · 4 min read · 833 words · established

Shelves of empty glass shells with two lit solid crystals among them

Barrel files and isolated unit tests hide dead code in monorepos. Find orphaned functions by tracing reachability from declared production roots.

Your static analysis tool reports that your workspace contains 8,400 functions. That number usually gives engineering leadership a comfortable sense of asset value. In reality, hundreds of those functions belong to an abandoned database migration, several dozen are unreferenced helpers copied between packages, and a non-trivial portion are variants of HTTP clients that should have been deleted eighteen months ago.

A raw list of exported symbols is a claim, not an inventory. As discussed in our doctrine on why a component count is not an inventory, an entry is only real if you can prove where it runs, what depends on it, and whether it executes correctly. When you cannot separate load-bearing logic from dead weight, every refactor, security patch, and dependency upgrade costs more than it should.

Finding orphaned code in a monorepo requires moving past string matching to verifiable reachability analysis.

The illusion of reference

Basic dead code detection often fails in monorepos because of how shared code is packaged.

The most common failure mode is the index aggregation pattern. Package A exports fifty utility functions through a single index.ts or mod.rs. Package B imports two of them. To a naive regular expression or text search, all fifty functions appear to be referenced because the package directory itself is actively imported across the repository.

packages/utils/src/
├── index.ts        <-- Re-exports everything
├── formatters.ts   <-- 12 functions (2 used)
└── crypto.ts       <-- 8 functions (0 used, but index.ts references it)

If your audit relies on grep, crypto.ts looks alive because index.ts mentions it. If your audit relies on git churn, it looks stable because nobody has touched it in two years. In practice, it is dead weight: unexercised, untested by real traffic, yet still compiled and carried along in your deployment artifacts.

Similarly, internal test suites often keep orphaned functions alive artificially. A unit test that imports an unused function asserts only that the function works in isolation. It does not prove that any production execution path ever calls it.

Tracing reachability from declared roots

Automated dead code detection in a monorepo must operate top-down from declared entry points rather than bottom-up from exported symbols.

The pipeline consists of three steps:

  1. Declare the real roots. An entry point is an interface that receives execution from outside the code graph: an HTTP route handler, an event consumer, a CLI command, or a scheduled job. Everything else is an internal node.
  2. Construct the directed import graph. Parse the Abstract Syntax Tree (AST) to resolve real symbol-level specifiers. If packages/billing imports { formatCurrency } from packages/utils, the graph records an edge between billing and formatCurrency, but zero edges to formatTaxId.
  3. Compute the unvisited set. Traverse the graph from the roots. Any symbol in your repository with an in-degree of zero from this traversal is an orphaned function.
Roots (APIs, CLIs, Workers)
       │
       ▼
[Active Handlers] ───► [Domain Logic] ───► [formatCurrency]
                                                 
[Unreachable Node: formatTaxId] ◄── (Zero incoming edges from roots)

When you subtract reachable symbols from declared exports, you get an actionable list of functions that can be deleted immediately without changing runtime behaviour. This turns a speculative monorepo audit into a safe cleanup task.

Where static reachability stops

Conceding the boundary limits of static analysis is essential. A purely static reachability pass has three hard edges:

  • Dynamic dispatch and reflection. If a handler invokes functions via constructed string names (e.g., handlersactionType), static AST traversal will mark those handler targets as unreferenced unless your tool understands the schema.
  • True public package boundaries. If a monorepo package is published to an external registry for third-party consumption, its public exports are entry points by definition, even if nothing inside the monorepo calls them.
  • Stub implementations. An orphaned function may be imported by an active file, but never called because the calling function itself contains early returns or dead branching. Identifying these requires combining dead code sweeps with structural audits, such as detecting stubs disguised as internal libraries.

Where dynamic dispatch is rare and boundaries are internal, static reachability gives you an uncompromising baseline. For ambiguous edges, mark the component as provisional rather than verified, and exclude it from your active catalog until an integration test exercises the path.

A repeatable audit on Monday

Do not attempt to audit an entire 500,000-line repository in a single pass. Start with one contained domain package:

  1. Identify the package's declared entry points. List the files that handle incoming traffic, consume queues, or trigger scheduled jobs.
  2. Run an AST-aware reference tracer. Use a tool that tracks named exports through barrel files (such as knip for TypeScript, deadcode for Go, or a custom tree-sitter query) rather than string grep.
  3. Exclude test files from the root set. Treat test suites as secondary verifiers, not roots of production execution. A function imported only by its own unit test is dead code.
  4. Prune the unreachable set. Delete the orphaned exports and the internal files that feed only them. Remove the re-export statements from the index barrels.
  5. Re-run the build and integration suite. If the build passes and integration tests run clean, the logic was truly unreferenced. If a unit test fails because an orphaned module was deleted, delete the test along with the dead code.

This post supports the longer argument in A Component Count Is Not an Inventory.

Keep reading

Next in the log

The Strategic Master Library · written and reviewed under the house's own epistemic rules: nothing claimed that we cannot show.