fix(js,ts): a callee that names nothing records no reference, and a wrapped callee is the plain call (#2742)

A TS/JS call whose callee was no name and no member access (an IIFE, a
curried `connect(mapState)(View)`, `handlers[type]()`, `(0, fn)()`, a
tagged template on a call) stored the callee's whole source text as the
reference's name. Nothing can answer such a name, so each one parked as a
failed reference, an IIFE's entire body among them. extractCall now reads
the callee past the wrappers that keep it the same callee (parentheses,
`!`, `as` / `satisfies`, `await`, type arguments) and records nothing when
that is not a name or a dotted chain; the calls inside are visited on
their own. Mirrored in the kernel (tsjs/extractors.rs callee_name_of).

The reference of a wrapped callee sits at its name, where a plain call's
does, and the resolver's bare-call check reads `fn!(`, `(fn)(`,
`(fn as F)(` and `(fn satisfies F)(` as it reads `fn(`: SigNoz's
`(callback as FrameRequestCallback)(0)` and `(callback)(0)` name the
arrow's own parameter, and from the `(` they bound a generated interface's
`callback` property.

tree-sitter-typescript parses a prefix operator over a generic call as it
parses `await f<T>()` (#2586): `!settings.get<boolean>('X')` and
`void apiRequest<T>()` are calls whose callee is `!settings.get` /
`void apiRequest`. The callee peel that handled `await` now peels a prefix
operator too (peelTsPrefixedCallee; the kernel's peel_await_callee), so the
call is to `settings.get`, and `void apiRequest<T>().then()` encodes as
`apiRequest().then`, not `voidapiRequest().then`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 11:01:53 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent aa01692620
commit ae0fc631e0
8 changed files with 211 additions and 19 deletions
+46 -7
View File
@@ -559,13 +559,17 @@ const TS_JS_TRANSPARENT_RECEIVER_TYPES = new Set([
/**
* tree-sitter-typescript parses `await f<T>(x)` as a call whose callee is
* `await f` (#2586); the call is to `f`. Peels every `await`, skipping a
* comment written after one. Mirrored in the kernel (tsjs/extractors.rs).
* `await f` (#2586), and a prefix operator over a generic call the same way:
* `!settings.get<boolean>('X')`, `void apiRequest<T>()`, `typeof load<T>()`.
* The call is to `f`. Peels every `await` and prefix operator, skipping a
* comment written after an `await`. Mirrored in the kernel (tsjs/extractors.rs).
*/
function peelTsAwaitCallee(node: SyntaxNode): SyntaxNode {
function peelTsPrefixedCallee(node: SyntaxNode): SyntaxNode {
let cur = node;
while (cur.type === 'await_expression') {
const inner = cur.namedChildren.find((c: SyntaxNode) => c.type !== 'comment');
while (cur.type === 'await_expression' || cur.type === 'unary_expression') {
const inner = cur.type === 'unary_expression'
? getChildByField(cur, 'argument')
: cur.namedChildren.find((c: SyntaxNode) => c.type !== 'comment');
if (!inner) break;
cur = inner;
}
@@ -593,6 +597,32 @@ function peelTsJsReceiver(node: SyntaxNode): SyntaxNode {
return cur;
}
/**
* The name a TS/JS call's callee writes when it is no member access, past the
* wrappers that keep it the same callee — `(fn)()`, `fn!()`, `(fn as F)()`,
* `await f<T>()`, the type arguments of a tagged template's tag
* (`css<Props>\`…\``) — or null when it names nothing: a call (`connect(map)
* (View)`, `create<T>()(…)`, `breakpoint('tablet')\`…\``), a function (an
* IIFE), a subscript (`handlers[type]()`), `(0, fn)()`. Its text used to be
* the reference's name, an IIFE's whole body among them, which no symbol is.
* A dotted chain in parentheses keeps its text, `(a.b)()` → `a.b`, as the
* parenthesized-callee normalization in extractCall reads it. `at` is where the
* name starts, which is where the reference sits, as a plain `fn()` call's does:
* scope checks read the name there (a parameter `fn` shadows a namesake).
*/
function tsJsCalleeName(func: SyntaxNode, source: string): { name: string; at: SyntaxNode } | null {
let cur = peelTsJsReceiver(func);
while (cur.type === 'instantiation_expression') {
const inner = getChildByField(cur, 'function') ?? cur.namedChild(0);
if (!inner) return null;
cur = peelTsJsReceiver(inner);
}
if (cur.type === 'identifier' || cur.type === 'super' || cur.type === 'import') return { name: getNodeText(cur, source), at: cur };
if (cur.type !== 'member_expression') return null;
const chain = getNodeText(cur, source).replace(/\s+/g, '');
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(chain) ? { name: chain, at: cur } : null;
}
/**
* Whether a TS/JS receiver still collapses to the bare method name: `this` /
* `super` (the resolver reads the owner off the enclosing class), a member
@@ -5603,7 +5633,7 @@ export class TreeSitterExtractor {
} else {
let func = getChildByField(node, 'function') || node.namedChild(0);
if (func && (this.language === 'typescript' || this.language === 'tsx' || this.language === 'arkts')) {
const peeled = peelTsAwaitCallee(func);
const peeled = peelTsPrefixedCallee(func);
if (peeled !== func) refStart = peeled.startPosition;
func = peeled;
}
@@ -5909,7 +5939,7 @@ export class TreeSitterExtractor {
// wrong edge. The inner call is visited on its own either way.
// Mirrored in the kernel (tsjs/extractors.rs, python.rs).
let innerFn = getChildByField(receiver, 'function');
if (innerFn && (this.language === 'typescript' || this.language === 'tsx')) innerFn = peelTsAwaitCallee(innerFn);
if (innerFn && (this.language === 'typescript' || this.language === 'tsx')) innerFn = peelTsPrefixedCallee(innerFn);
const innerCallee = innerFn ? getNodeText(innerFn, this.source).replace(/\s+/g, '') : '';
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(innerCallee)) return;
calleeName = `${innerCallee}().${methodName}`;
@@ -5992,6 +6022,15 @@ export class TreeSitterExtractor {
} else {
calleeName = getNodeText(func, this.source);
}
} else if (TS_JS_CHAIN_LANGUAGES.has(this.language)) {
// A callee that names nothing — a call, a function, a subscript —
// is no reference: a silent miss, never a name no symbol has. A
// curried call's inner call is visited on its own. Mirrored in the
// kernel's extract_call (tsjs/extractors.rs callee_name_of).
const named = tsJsCalleeName(func, this.source);
if (named === null) return;
calleeName = named.name;
refStart = named.at.startPosition;
} else {
calleeName = getNodeText(func, this.source);
}