/** * Dead code and islands — one derivation of "nothing in this repository * reaches here". * * The graph can answer that question exactly, and that is the problem: the * exact answer is *no incoming edge*, and a symbol with no incoming edge is not * the same thing as a symbol nobody uses. Reflection calls it. A framework * registers it by name. A test file that was never indexed imports it. The * resolver saw the name and could not follow it. So the honest product of this * module is two things at once — a list, and everything the list could not see. * * ## The shape of the claim * * `unreferenced` is a fact: no edge in the index, other than the `contains` * edge from whatever holds it, points at this symbol. `dead` is an inference on * top of that fact, and every step of the inference is subtractive — a * candidate is dropped from the list the moment there is any reason to believe * something outside the graph reaches it: * * - it is **exported** (something outside this repository may import it); * - it lives in a **test** or a **generated** file (not code anyone deletes by * hand); * - it is **abstract** or declared on an interface (a declaration is dispatched * to, never called); * - it is **decorated** (`@app.route`, `@Component`, `@EventHandler`) — a * decorator is a registration, and the framework that reads it is not in the * graph. Seen as the symbol's own outgoing `decorates` edge, which is where * the engine records it; `node.decorators` is only populated by a couple of * languages and is checked as well rather than instead; * - it **overrides** a member an ancestor declares (calls land on the ancestor; * see {@link overrideCandidates} for why an ancestor we cannot read counts * the same way); * - it has a name the language calls by itself (`constructor`, `__enter__`, * `main`); * - it sits in a **vendored** directory (`vendor/`, `third_party/`, * `node_modules/` — code the repository carries but does not own); * - it is in a **test scope** the path does not reveal — a Rust * `#[cfg(test)] mod tests`, a nested `Tests` namespace; * - it is in a **component file** whose markup the index reads for calls but * not for references, so a handler passed as `{onkeydown}` is invisible; * - it is in a file **nothing in the index reaches**. Then "nothing references * this symbol" is a restatement of "we cannot see how this file is wired", * not a finding about the symbol — and it is the map, not this list, that * says so: a file no one reaches is an island, and islands are drawn there; * - the index holds an **unresolved reference** to its name. A `failed` row in * `unresolved_refs` is the resolver's own record of a reference it could not * follow, and a symbol whose name we failed to follow cannot be called * unreferenced. * - **another symbol of the same name IS referenced.** This is the one that * matters most and the one nothing else would catch. `CodeGraph.getTopRouteFile` * calls `this.queries.getTopRouteFile()`; the resolver prefers a same-name * definition in the call site's own file, so the edge lands on the caller * itself and the real target is left with nothing. From the edge table, * "nobody calls this" and "the resolver picked the twin" are the same * picture — so the claim is not made about either; * - it is declared in a **header** (`.h`, `.hpp`, `.d.ts`, `.pyi`): a header IS * the export surface, and the reference to it is an `#include` the resolver * does not follow to the declaration; * - it is in a language this index records **no export marker** for. The * exported filter is the strongest one here, and for Rust (`pub` is not * recorded) or Python and C (no such concept at all) it silently does * nothing — so the index is asked, per language, whether it ran; * - **its own file writes the name more than once.** The last rule, and the * only one that is not a graph query. Everything above assumes the edge * table is complete; it is not, and the gaps do not announce themselves — * `this.handleMessage.bind(this)` is a value reference the extractor does not * record, and a call inside an object-literal initialiser is another. Both * leave the name written twice in one file and no edge at all. So before the * claim is made, the identifier is counted in the file itself AND in every * file the index says depends on it — written once, in its own declaration, * nothing that can reach it writes it down; written twice, we simply did not * see the second one. * * Every subtraction is counted. {@link DeadCodeReport.excluded} is not * diagnostics — it is the sentence under the list ("47 exported, 12 overriding * an ancestor…"), because a list of eight rows drawn from four thousand * candidates means something different from a list of eight drawn from nine. * * ## Islands * * The other half of the task, a *module* nothing depends on, is not computed * here: it falls straight out of the map's own link set (a module with no * incoming link), and the map's layout is already a pure function in the * viewer. Computing it a second time on this side would be a second answer to * a question the map has already answered. See `ui/src/lib/map-model.ts`. * * Everything here is query-time and read-only. */ import fs from 'fs'; import path from 'path'; import type CodeGraph from '../index'; import type { Node, NodeKind } from '../types'; import { isTestFile } from '../search/query-utils'; // ============================================================================= // Caps and defaults // ============================================================================= /** * Kinds asked about by default. * * Callables and types, and nothing else. `variable`/`constant`/`field` are out * deliberately: a value's uses are recorded as `references` edges, and that * coverage is the most language-dependent thing in the resolver — a default * that included them would produce a list whose truthfulness varied by which * language the reader happened to be looking at. */ export const DEAD_CODE_KINDS: readonly NodeKind[] = [ 'function', 'method', 'class', 'component', 'interface', 'struct', 'trait', 'protocol', 'enum', 'union', 'type_alias', ]; /** Kinds a `kinds=` request may ask for. Anything else is a caller bug. */ export const DEAD_CODE_ALLOWED_KINDS: ReadonlySet = new Set([ ...DEAD_CODE_KINDS, 'variable', 'constant', 'property', 'field', 'enum_member', 'namespace', 'module', ]); /** * Candidates pulled out of SQL before any exclusion runs. * * High enough that no real repository reaches it with the default kinds (this * index produces ~1 400), and bounded so that a half-indexed monorepo cannot * turn one screen into a scan of a million rows. When it bites, * {@link DeadCodeReport.bounded} says so. */ export const MAX_DEAD_CODE_CANDIDATES = 20000; /** Levels walked up looking for an ancestor that declares the same member. */ export const MAX_OVERRIDE_ANCESTOR_DEPTH = 8; /** * Files read for the corroboration pass, and the biggest one read. * * The pass runs over the survivors only — everything cheap has already fired — * so on this index it reads a few dozen files. The caps are a backstop against * a repository whose survivors span a thousand files or include a generated * megabyte. A file skipped for either reason counts as NOT corroborated, which * drops the row: the safe direction is always the one that says less. */ export const MAX_CORROBORATION_FILES = 600; export const MAX_CORROBORATION_BYTES = 2_000_000; /** Kinds that can carry members, i.e. whose ancestors are worth walking. */ const CONTAINER_KINDS: ReadonlySet = new Set([ 'class', 'interface', 'struct', 'trait', 'protocol', 'enum', 'union', 'type_alias', ]); /** Container kinds whose members are declarations, never call targets. */ const DECLARATION_CONTAINER_KINDS: ReadonlySet = new Set([ 'interface', 'trait', 'protocol', ]); /** Member kinds an override can be declared on. */ const OVERRIDABLE_KINDS: ReadonlySet = new Set([ 'method', 'function', 'property', 'field', ]); /** * Names a language or a runtime calls without anything in the source naming * them. * * Kept short on purpose. The temptation is a per-language table of every * lifecycle hook ever written, which would be wrong twice over — it would go * stale, and it would hide real dead code behind a name coincidence. The * entries below are the ones where the *language itself* does the calling, so * no source file could name them even in principle. Framework hooks are caught * by the decorator and override rules instead, which are structural. */ const IMPLICIT_ENTRY_NAMES: ReadonlySet = new Set([ 'constructor', 'main', 'init', 'deinit', 'finalize', 'destructor', 'dispose', 'drop', 'default', 'tostring', 'equals', 'gethashcode', 'hashcode', ]); /** * Qualified-name segments that mean "inside a test scope the file path does not * reveal" — a Rust `#[cfg(test)] mod tests`, a nested `Tests` class in C#, a * Go `TestMain` helper block. `isTestFile` only reads paths, and an in-file test * module is invisible to it. */ const TEST_SCOPE_SEGMENTS: ReadonlySet = new Set([ 'test', 'tests', '__tests__', 'spec', 'specs', 'testing', ]); /** * Languages whose files are markup with a script block inside them. * * The extractors for these read the `