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);
}
+10 -2
View File
@@ -3628,10 +3628,12 @@ function isReceiverLessCall(ref: UnresolvedRef, context: ResolutionContext): boo
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1]
?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
if (line === undefined) return false;
// `^<name>\s*[(<]` at the call's column, without compiling a pattern per call.
// `^<name>\s*[(<]` at the call's column (or a JS/TS wrapper's tail after the
// name), without compiling a pattern per call.
if (!line.startsWith(ref.referenceName, ref.column)) return false;
CALL_OPENER.lastIndex = ref.column + ref.referenceName.length;
if (!CALL_OPENER.test(line)) return false;
JS_PEELED_CALLEE_TAIL.lastIndex = CALL_OPENER.lastIndex;
if (!CALL_OPENER.test(line) && !(JS_FAMILY.has(ref.language) && JS_PEELED_CALLEE_TAIL.test(line))) return false;
// Nothing but whitespace, an operator or an opener may precede a bare call:
// read the text before the column backwards, past trailing whitespace,
// instead of end-anchoring a pattern that scans the prefix from its start.
@@ -3649,6 +3651,12 @@ function isReceiverLessCall(ref: UnresolvedRef, context: ResolutionContext): boo
/** `\s*[(<]` from a given index (sticky) — an optional call's `?.(` too. */
const CALL_OPENER = /\s*(?:\?\.\s*)?[(<]/y;
/**
* What follows a JS/TS callee's name inside a wrapper the extractor sees
* through — `fn!()`, `(fn)()`, `(fn as F)()`, `(fn satisfies F)()` — whose
* reference sits at the name, as a plain call's does (sticky).
*/
const JS_PEELED_CALLEE_TAIL = /\s*[!)]|\s+(?:as|satisfies)(?![\w$])/y;
const WHITESPACE = /\s/;
const WORD_CHAR = /\w/;
/** A character that ends a receiver: `.`, a word character, `$`, `]` or `)`. */