fix(js,ts): a call on a new C() receiver resolves through C

A TypeScript or JavaScript call on a constructed object, new C(args).m(),
reached the resolver as the bare method name in both engines, so exact
matching bound it to any project method called m: new RegExp(p).exec(s)
to a database wrapper's exec, new Array(n).fill(0) to a text class's
fill, new TextEncoder().encode(s) to a project encode function.

Both engines now emit new C().m (new ns.C().m for a namespaced class, and
nothing for a constructor that is not a plain name or member chain). The
resolver looks m up on C with resolveMethodOnType, inherited methods on
the conformance pass once extends edges exist, an aliased named import
mapped back to its class, and a JS built-in (RegExp, Map, Date...) never
a project class unless the file imports or declares one. An ES5
constructor function keeps its prototype method.

Our changes: the lookup passes the file the call's file imports the class
from (tsImportedTypeFile), so of two classes sharing a name the imported
one is the one constructed (the PR as written took the first indexed copy,
at 0.9); new this.constructor() and a static new this() keep the bare
method name, as a call on this does (the PR dropped three.js's clone
idiom, 29 links to a class's own copy); rebased onto PR #2852, which
edits the same keepsBareTsJsReceiver and test lists; resolved the import
list conflict with rustChainedCallByName from PR #2846; CHANGELOG bullet
moved next to the JS/TS receiver bullets.

Validation: the new tests fail 4 of 6 on main 449eefa7 with both engines
and pass here. A/B against main, kernel per arm: flask, gin and vite
byte-identical. codegraph 48 links added (new ToolHandler(cg).execute), 2
retargeted and 14 typed, all right, 58 removed, 57 wrong (RegExp exec and
test, Array fill, Set add) and 1 right one lost to the grammar's parse of
new nodeSqlite!.DatabaseSync(). excalidraw 22 wrong removed (TextEncoder,
TextDecoder, URLSearchParams, Array fill), 3 typed. pixijs 14 retargeted
and 278 typed, all right, 7 wrong removed. pdf.js 5 added, 47 retargeted
and 68 typed, all right, 76 wrong removed (Array keys and fill, typed
arrays, Response). three.js 33 added, 404 retargeted (Capsule::set to
Matrix4::set and Matrix3::set, NURBSCurve::copy to the class's own copy)
and 429 typed, all right; 135 removed, 133 wrong (UINumber onChange, which
three.js attaches at run time, TextDecoder, typed arrays, URL) and 2 right
only by name (Loader methods inherited through a base imported from the
three package). No default-imported class under another name lost an edge.
Kernel parity holds on all six TS/JS repos and this repository, 0 diffs.
Index CPU time unchanged or lower: codegraph 81.5 s to 79.5 s, three.js
72.0 s to 71.2 s (medians of 3, interleaved).

Original PR #1945 by @danusha2345.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
danusha2345
2026-10-23 18:34:25 +00:00
committed by Colby Mchenry
co-authored by Claude Opus 5.5
parent 16f2d357bc
commit 0d46dba08f
9 changed files with 305 additions and 16 deletions
+24 -4
View File
@@ -1671,6 +1671,27 @@ impl<'t> Walker<'t> {
// TreeSitterExtractor.extractCall.
let Some(inner) = self.plain_inner_callee(r) else { return };
callee_name = format!("{inner}().{method_name}");
} else if let Some(r) = receiver.filter(|r| r.kind() == "new_expression") {
// Constructor receiver — `new Runner().run()`: keep the
// class as `new <Class>().<method>` so the resolver
// resolves the method on it or nowhere; emit nothing for
// a constructor that is not a plain name / member chain.
// Mirrors TreeSitterExtractor.extractCall.
let Some(ctor) = r
.child_by_field_name("constructor")
.and_then(|c| Self::plain_member_name(self.text(c)))
else {
return;
};
// `new this.constructor().copy(this)`, a static
// `new this().init()`: an instance of the enclosing
// class keeps the bare method name, as a call on
// `this` does.
callee_name = if ctor == "this" || ctor == "this.constructor" {
method_name.to_string()
} else {
format!("new {ctor}().{method_name}")
};
} else if receiver.is_some_and(|r| !keeps_bare_receiver(r, self.src)) {
// An expression receiver with no static type
// (`(a ?? b).map()`, `f().list.map()`): emit nothing
@@ -2119,9 +2140,9 @@ fn peel_receiver(node: Node<'_>) -> Node<'_> {
cur
}
/// `this` / `super`, a member chain rooted at either or at `window`, or
/// `new C()` still collapse to the bare method name; a subscript anywhere in
/// the chain does not (mirrors keepsBareTsJsReceiver).
/// `this` / `super`, or a member chain rooted at either or at `window`, still
/// collapse to the bare method name; a subscript anywhere in the chain does
/// not (mirrors keepsBareTsJsReceiver).
fn keeps_bare_receiver(node: Node<'_>, src: &str) -> bool {
let mut cur = node;
while matches!(cur.kind(), "member_expression" | "subscript_expression") {
@@ -2136,7 +2157,6 @@ fn keeps_bare_receiver(node: Node<'_>, src: &str) -> bool {
match cur.kind() {
"this" | "super" => true,
"identifier" => &src[cur.byte_range()] == "window",
"new_expression" => cur.id() == node.id(),
_ => false,
}
}