Repository navigation
TS: await fn<T>(…) gets no calls edge; the reference is recorded as "await fn" #2586
Description
Summary
An awaited call with explicit type arguments — return await fetchApi<User>("/me") — produces no calls edge. The unresolved reference is recorded with the name await fetchApi, which matches nothing. The same call without await, or with await but without type arguments, resolves to the imported function. Typed fetch wrappers (await api.get<T>(…), await request<T>(…)) are a common shape in TypeScript front ends, so their callers go missing from the graph.
Reproduction (verified)
On main at 8fe3156, built from source, Linux (WSL2), Node 22.23.2, WASM extractor (CODEGRAPH_KERNEL=0).
src/api.ts
export async function fetchApi<T>(path: string): Promise<T> {
const r = await fetch(path);
return (await r.json()) as T;
}src/user.ts (the same three functions in a .tsx file give the same result)
import { fetchApi } from "./api";
interface User { id: number }
export async function plain(): Promise<User> {
return fetchApi<User>("/me");
}
export async function awaited(): Promise<User> {
return await fetchApi<User>("/me");
}
export async function awaitedNoTypeArgs(): Promise<unknown> {
return await fetchApi("/me");
}codegraph init -i, then the calls edges and the unresolved refs on line 8:
src/user.ts:plain -[calls L5]-> src/api.ts:function:fetchApi
src/user.ts:awaitedNoTypeArgs -[calls L11]-> src/api.ts:function:fetchApi
src/user2.tsx:plainX -[calls L5]-> src/api.ts:function:fetchApi
src/user2.tsx:awaitedXNoTypeArgs -[calls L11]-> src/api.ts:function:fetchApi
unresolved_refs:
src/user.ts L8 await fetchApi
src/user2.tsx L8 await fetchApi
const u = await fetchApi<User>("/me") gives the same await fetchApi ref, and through a namespace import await api.fetchApi<User>("/me") gives await api.fetchApi.
Observed: no edge from awaited to fetchApi.
Expected: awaited -[calls]-> fetchApi, as for plain and awaitedNoTypeArgs.
Likely cause
The parse tree is verified; the extractor path is inferred from reading the code.
tree-sitter-typescript (both the typescript and tsx grammars shipped in dist/extraction/wasm/) parses await f<T>(x) with the await inside the callee:
return await fetchApi<User>("/me");
(return_statement (call_expression function: (await_expression (identifier)) type_arguments: (type_arguments (type_identifier)) arguments: (arguments (string ...))))
return await fetchApi("/me");
(return_statement (await_expression (call_expression function: (identifier) arguments: (arguments (string ...)))))
extractCall takes the function field as the callee. The node is an await_expression, not an identifier or member_expression, so it falls through to the last branch, which uses the node's full text as the callee name:
codegraph/src/extraction/tree-sitter.ts
Line 5314 in 8fe3156
const func = getChildByField(node, 'function') || node.namedChild(0); codegraph/src/extraction/tree-sitter.ts
Lines 5648 to 5649 in 8fe3156
} else { calleeName = getNodeText(func, this.source);
The extractor already looks through await_expression for a member call's receiver ((await x).m()), via TS_JS_TRANSPARENT_RECEIVER_TYPES / peelTsJsReceiver:
codegraph/src/extraction/tree-sitter.ts
Lines 475 to 478 in 8fe3156
const TS_JS_TRANSPARENT_RECEIVER_TYPES = new Set([ 'parenthesized_expression', 'non_null_expression', 'as_expression', 'satisfies_expression', 'type_assertion', 'await_expression', ]);
Peeling an await_expression off the callee itself, when the call has type_arguments, would give the identifier or member expression the call is really made on. This is the same kind of grammar quirk #2375 handled for Dart generic calls.
Related
- fix(dart): link generic calls the parser reads as two comparisons #2375 (Dart: generic calls the parser reads as two comparisons).
- TypeScript: split() on an awaited Promise<string> result resolves to an unrelated class method (v1.6.0) #1840 (a different awaited-call shape, about the receiver type rather than the callee name).
Not checked
- The Rust kernel path (default,
CODEGRAPH_KERNELunset). It parses with its own tree-sitter bindings and may record a different name. - Plain
.jsfiles with JSDoc generics (no type arguments in JS syntax, so probably unaffected). yield f<T>()and other prefix operators (!f<T>(),void f<T>()), which may have the same precedence quirk.
Activity
fix(js,ts): a callee that names nothing records no reference, and a w…