Skip to content

bug(dead-code): exported functions passed as values report as dead — graph-core emits no reference edge #299

Description

@EivMeyer

Verified on main @ af09b7c (v0.2.1), built locally, darwin-arm64.

Summary

An exported function that is only ever passed as a value — to map, reduce, filter, a callback, a handler table — is reported as dead. The graph has no edge kind for a symbol reference, so TS_DEAD_CODE_UNUSED_EXPORT fires on code that is plainly in use.

Repro

// src/domain/money.ts
export function money(amount: number, currency: Currency): Money { ... }
export function add(a: Money, b: Money): Money { ... }

// src/domain/cart.ts
export function lineTotal(item: LineItem): Money { ... }

export function cartTotal(items: readonly LineItem[]): Money {
  if (items.length === 0) return money(0, "EUR");
  return items.map(lineTotal).reduce(add);      // <-- both passed as values
}

opcore graph build then opcore --repo .:

TS_DEAD_CODE_UNUSED_EXPORT [warning] src/domain/cart.ts  :: no incoming CALLS graph evidence: lineTotal
TS_DEAD_CODE_UNUSED_EXPORT [warning] src/domain/money.ts :: no incoming CALLS graph evidence: add

money, format, and cartTotal — all invoked with call syntax — are correctly not flagged.

Rewrite the same logic with direct calls:

export function cartTotal(items: readonly LineItem[]): Money {
  let running = money(0, "EUR");
  for (const item of items) {
    running = add(running, lineTotal(item));
  }
  return running;
}

Rebuild the graph, and both warnings disappear. Only the genuinely dead export remains.

Cause

packages/validation-typescript/src/dead-code-entrypoints.ts:21

const symbolReachabilityEdgeKinds = new Set(["CALLS", "INHERITS", "IMPLEMENTS"]);

This is not just a narrow edge-kind set. Grepping crates/graph-core/src, the only edge kinds the graph emits at all are CALLS, IMPLEMENTS, IMPORTS_FROM, INHERITS, TESTED_BY. There is no reference edge, so passing a function as a value records no relationship anywhere — reachability isn't ignoring a weak edge, the edge was never extracted.

That makes this a graph-core change (emit a reference edge from the Rust extractor), not a set-membership change in TypeScript.

Why it matters

Higher-order usage is ordinary TypeScript. Any repo using map/reduce/filter with named functions, or registering handlers in a table, gets false dead-export warnings in proportion to how idiomatic it is. They are warning severity so they do not block, but #285 and #288 both contemplate promoting dead-code to error — this would need fixing first.

Distinct from #287 (entrypoint modeling), which is about package-declared public APIs whose callers live outside the repo. Here the caller is in the same file.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions