Context
Follow-up from #1741 (identifier-argument dynamic-call gating). That issue fixed a false-positive bug where every bare identifier argument to any call was treated as a callback reference, regardless of the callee — fixed by gating on CALLBACK_ACCEPTING_CALLEES/HTTP_VERB_CALLEES (a curated name allowlist covering stdlib/framework APIs: .map, .forEach, .then, Express routes, Array.from, etc.), mirroring how member_expression args were already gated since #974.
A name allowlist can only ever cover known API names. It cannot, in principle, recognize a project's own arbitrary higher-order functions. Concretely, tests/benchmarks/resolution/fixtures/typescript/callbacks.ts:
export type UserProcessor = (user: User) => void;
export function processEach(users: User[], fn: UserProcessor): void {
for (const user of users) fn(user);
}
export function runCallbackDemo(users: User[]): void {
processEach(users, logUser); // logUser passed as fn — a genuine callback reference
}
processEach is a project-defined function, not a stdlib/framework name, so it is correctly absent from the allowlist — but that means #1741's fix now also drops the legitimate edge runCallbackDemo -> logUser. This is tracked in tests/benchmarks/resolution/fixtures/typescript/expected-edges.json (3 edges, runCallbackDemo -> logUser/upperUser/hasEmail, mode callback) which are intentionally left in place (not deleted) because they describe a real, decidable fact about the code — TS resolution recall for the typescript fixture dropped from 47/47 to 44/47 (93.6%, still well above the 0.72 CI floor) as a result.
Why a name allowlist can't fix this
There is no way to distinguish, from the call site alone, processEach(users, logUser) (real callback) from analyzeDrift(communities, communityDirs) (real issue #1741 repro — communities is plain data) using only the callee's name. Both are "some identifier called with another identifier as an argument."
Suggested approach
The distinguishing signal is available at the callee's own definition: processEach's second parameter is typed UserProcessor (a type alias for (user: User) => void — function-shaped), whereas analyzeDrift's parameters (communities: CommunityObject[], communityDirs: Map<...>) are not. A resolver-level (not extractor-level) enhancement could:
- For TS/TSX files, collect each function/method definition's parameter type annotations (name/position -> type text), plus
type X = ... alias definitions, during extraction (this repo already has typeMap/returnTypeMap machinery to build on).
- When resolving an identifier-argument call site, look up the callee's definition and check whether the parameter at that position is function-shaped (arrow-function type literal,
Function, or an alias that resolves to one).
- If so, treat the identifier argument as a callback reference regardless of the callee's name; otherwise, fall back to the existing name/position allowlist.
- Plain JS has no type annotations, so this only helps TS/TSX — JS would keep relying on the name allowlist alone.
Why this wasn't included in #1741
This is a genuinely new capability (type-aware parameter analysis), not a bug fix to existing logic:
- Needs new same-file (and ideally cross-file/import-aware) parameter-type + type-alias tracking, threaded into the call-processing pass in both
src/extractors/javascript.ts (walk and query paths) and crates/codegraph-core/src/extractors/javascript.rs (or more likely, into the resolver layer — src/domain/graph/resolve.rs / crates/codegraph-core/src/domain/graph/resolve.rs — since the callee's definition may live in a different file than the call site).
- Requires deciding what counts as "function-shaped" (arrow type literal,
Function, generic/union types, imported aliases, interface call signatures) with a defensible heuristic, not full type-checking.
- Needs test coverage and parity verification across both engines.
Suggested scope for this issue
- Design the same-file case first (callee defined in the same file as the call site) — matches the
callbacks.ts fixture exactly.
- Extend to cross-file via existing import-resolution machinery if the same-file case proves out.
- Add fixtures/tests distinguishing "identifier passed to function-typed param" (should resolve) from "identifier passed to data-typed param" (should not) for both engines.
- Re-evaluate whether
typescript's resolution-benchmark recall floor (THRESHOLDS.typescript in tests/benchmarks/resolution/resolution-benchmark.test.ts) should ratchet up once these 3 edges resolve again.
Context
Follow-up from #1741 (identifier-argument dynamic-call gating). That issue fixed a false-positive bug where every bare identifier argument to any call was treated as a callback reference, regardless of the callee — fixed by gating on
CALLBACK_ACCEPTING_CALLEES/HTTP_VERB_CALLEES(a curated name allowlist covering stdlib/framework APIs:.map,.forEach,.then, Express routes,Array.from, etc.), mirroring howmember_expressionargs were already gated since #974.A name allowlist can only ever cover known API names. It cannot, in principle, recognize a project's own arbitrary higher-order functions. Concretely,
tests/benchmarks/resolution/fixtures/typescript/callbacks.ts:processEachis a project-defined function, not a stdlib/framework name, so it is correctly absent from the allowlist — but that means#1741's fix now also drops the legitimate edgerunCallbackDemo -> logUser. This is tracked intests/benchmarks/resolution/fixtures/typescript/expected-edges.json(3 edges,runCallbackDemo -> logUser/upperUser/hasEmail, modecallback) which are intentionally left in place (not deleted) because they describe a real, decidable fact about the code — TS resolution recall for thetypescriptfixture dropped from 47/47 to 44/47 (93.6%, still well above the 0.72 CI floor) as a result.Why a name allowlist can't fix this
There is no way to distinguish, from the call site alone,
processEach(users, logUser)(real callback) fromanalyzeDrift(communities, communityDirs)(real issue #1741 repro —communitiesis plain data) using only the callee's name. Both are "some identifier called with another identifier as an argument."Suggested approach
The distinguishing signal is available at the callee's own definition:
processEach's second parameter is typedUserProcessor(a type alias for(user: User) => void— function-shaped), whereasanalyzeDrift's parameters (communities: CommunityObject[],communityDirs: Map<...>) are not. A resolver-level (not extractor-level) enhancement could:type X = ...alias definitions, during extraction (this repo already hastypeMap/returnTypeMapmachinery to build on).Function, or an alias that resolves to one).Why this wasn't included in #1741
This is a genuinely new capability (type-aware parameter analysis), not a bug fix to existing logic:
src/extractors/javascript.ts(walk and query paths) andcrates/codegraph-core/src/extractors/javascript.rs(or more likely, into the resolver layer —src/domain/graph/resolve.rs/crates/codegraph-core/src/domain/graph/resolve.rs— since the callee's definition may live in a different file than the call site).Function, generic/union types, imported aliases, interface call signatures) with a defensible heuristic, not full type-checking.Suggested scope for this issue
callbacks.tsfixture exactly.typescript's resolution-benchmark recall floor (THRESHOLDS.typescriptintests/benchmarks/resolution/resolution-benchmark.test.ts) should ratchet up once these 3 edges resolve again.