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.
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, soTS_DEAD_CODE_UNUSED_EXPORTfires on code that is plainly in use.Repro
opcore graph buildthenopcore --repo .:money,format, andcartTotal— all invoked with call syntax — are correctly not flagged.Rewrite the same logic with direct calls:
Rebuild the graph, and both warnings disappear. Only the genuinely dead export remains.
Cause
packages/validation-typescript/src/dead-code-entrypoints.ts:21This is not just a narrow edge-kind set. Grepping
crates/graph-core/src, the only edge kinds the graph emits at all areCALLS,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/filterwith named functions, or registering handlers in a table, gets false dead-export warnings in proportion to how idiomatic it is. They arewarningseverity so they do not block, but #285 and #288 both contemplate promoting dead-code toerror— 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.