Skip to content

TS: await fn<T>(…) gets no calls edge; the reference is recorded as "await fn" #2586

Closed

Description

@inth3shadows

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:

The extractor already looks through await_expression for a member call's receiver ((await x).m()), via TS_JS_TRANSPARENT_RECEIVER_TYPES / peelTsJsReceiver:

  • 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

Not checked

  • The Rust kernel path (default, CODEGRAPH_KERNEL unset). It parses with its own tree-sitter bindings and may record a different name.
  • Plain .js files 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.
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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions