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
+2
View File
@@ -22,6 +22,8 @@ re!(styled_callee, r"^styled\b");
re!(pascal_case, r"^[A-Z]");
// extractCall parenthesized-conversion normalization
re!(paren_conversion, r"^\(\s*\*?\s*([A-Za-z_][\w.]*)\s*\)$");
// tsJsCalleeName's dotted chain (`a.b.c`); ASCII classes, as JS's `\w` is
re!(dotted_chain, r"^[A-Za-z_$][0-9A-Za-z_$]*(?:\.[A-Za-z_$][0-9A-Za-z_$]*)+$");
// flushFnRefCandidates SIMPLE_NAME
re!(simple_name, r"^[A-Za-z_$][A-Za-z0-9_$]*$");
// flushFnRefCandidates QUALIFIED_IMPORT
+47 -8
View File
@@ -1529,7 +1529,7 @@ impl<'t> Walker<'t> {
// Where the reference sits: the call, or the callee an `await` was
// peeled off (`await f<T>(x)` starts at `await`; the call to `f`
// starts where `f` does, as in `await f(x)`). Mirrors refStart.
let at = match (raw, func) {
let mut at = match (raw, func) {
(Some(r), Some(f)) if r.id() != f.id() => f,
_ => node,
};
@@ -1596,7 +1596,15 @@ impl<'t> Walker<'t> {
}
}
} else {
callee_name = self.text(func).to_string();
// A callee that names nothing (a call, a function, a
// subscript) is no reference. Mirrors tsJsCalleeName.
match self.callee_name_of(func) {
Some((name, name_at)) => {
callee_name = name;
at = name_at;
}
None => return,
}
}
}
@@ -1612,6 +1620,27 @@ impl<'t> Walker<'t> {
}
}
/// The name a non-member callee writes, past `(fn)`, `fn!`, `(fn as F)`,
/// `await` and a tag's type arguments (`css<Props>`), or None for one that
/// names nothing: a call, a function, a subscript, `(0, fn)`. A dotted
/// chain keeps its text, `(a.b)()` → `a.b`. The node is where the name
/// starts, which is where the reference sits. Mirrors tsJsCalleeName.
fn callee_name_of(&self, func: Node<'t>) -> Option<(String, Node<'t>)> {
let mut cur = peel_receiver(func);
while cur.kind() == "instantiation_expression" {
let inner = cur.child_by_field_name("function").or_else(|| cur.named_child(0))?;
cur = peel_receiver(inner);
}
match cur.kind() {
"identifier" | "super" | "import" => Some((self.text(cur).to_string(), cur)),
"member_expression" => {
let chain: String = self.text(cur).chars().filter(|c| !c.is_whitespace()).collect();
util::dotted_chain().is_match(&chain).then_some((chain, cur))
}
_ => None,
}
}
// --- extractInstantiation -----------------------------------------------------------
/// `this.<field>` as a member_expression receiver → Some(field) (#1496, #1987).
@@ -1630,17 +1659,27 @@ impl<'t> Walker<'t> {
Some(self.text(property).to_string())
}
/// `await f<T>(x)` parses with `await f` as the callee; the call is to
/// `f` (#2586). Peels every `await`, skipping a comment written after one.
/// TS-family only. Mirrors peelTsAwaitCallee.
/// `await f<T>(x)` parses with `await f` as the callee (#2586), and a
/// prefix operator over a generic call the same way
/// (`!settings.get<boolean>('X')`, `void apiRequest<T>()`); the call is to
/// `f`. Peels every `await` and prefix operator, skipping a comment written
/// after an `await`. TS-family only. Mirrors peelTsPrefixedCallee.
fn peel_await_callee(&self, node: Node<'t>) -> Node<'t> {
if !self.variant.is_ts() {
return node;
}
let mut cur = node;
while cur.kind() == "await_expression" {
let mut walker = cur.walk();
let Some(inner) = cur.named_children(&mut walker).find(|c| c.kind() != "comment") else { break };
loop {
let inner = match cur.kind() {
"unary_expression" => cur.child_by_field_name("argument"),
"await_expression" => {
let mut walker = cur.walk();
let found = cur.named_children(&mut walker).find(|c| c.kind() != "comment");
found
}
_ => break,
};
let Some(inner) = inner else { break };
cur = inner;
}
cur