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
+25 -8
View File
@@ -708,13 +708,13 @@ function tsThisSubscriptReceiver(node: SyntaxNode, source: string): string | nul
/**
* 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
* `super` (the resolver reads the owner off the enclosing class) and a member
* chain rooted at either or at `window` (the project-global escape of
* {@link isUnresolvedTsJsChain}), and `new C()` (its class is written at the
* call). A subscript anywhere in the chain does not: the resolver cannot
* read the receiver back from it, so the bare name would exact-match any
* project method of that name. (An element of a this-field is typed instead,
* see {@link tsThisSubscriptReceiver}.)
* {@link isUnresolvedTsJsChain}). `new C().m()` has its own encoding. A
* subscript anywhere in the chain does not: the resolver cannot read the
* receiver back from it, so the bare name would exact-match any project
* method of that name. (An element of a this-field is typed instead, see
* {@link tsThisSubscriptReceiver}.)
*/
function keepsBareTsJsReceiver(node: SyntaxNode, source: string): boolean {
let cur: SyntaxNode | null = node;
@@ -725,8 +725,7 @@ function keepsBareTsJsReceiver(node: SyntaxNode, source: string): boolean {
}
if (!cur) return false;
if (cur.type === 'this' || cur.type === 'super') return true;
if (cur.type === 'identifier') return getNodeText(cur, source) === 'window';
return cur === node && cur.type === 'new_expression';
return cur.type === 'identifier' && getNodeText(cur, source) === 'window';
}
/**
@@ -6309,6 +6308,24 @@ export class TreeSitterExtractor {
const chain = getNodeText(func, this.source).replace(/\s+/g, '').replace(/\?\./g, '.');
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*){2,}$/.test(chain)) return;
calleeName = chain;
} else if (TS_JS_CHAIN_LANGUAGES.has(this.language) && receiver?.type === 'new_expression') {
// Constructor receiver — `new Runner().run()`, `new RegExp(p).exec(s)`.
// The class is written at the call, so keep it: `new Runner().run`
// tells the resolver the receiver is an INSTANCE of `Runner`, and
// the method resolves on that class (or a supertype) or nowhere —
// a built-in or external class has no project method to bind to.
// The bare method name this used to emit exact-matched any
// same-named project method (`new RegExp(p).exec()` onto a
// database wrapper's `exec`). A constructor that is not a plain
// name or member chain (`new (f())().m()`) emits nothing.
// Mirrored in the kernel's extract_call (tsjs/extractors.rs).
const ctor = getChildByField(receiver, 'constructor');
const ctorName = ctor ? getNodeText(ctor, this.source).replace(/\s+/g, '') : '';
if (!/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(ctorName)) return;
// `new this.constructor().copy(this)`, a static `new this().init()`:
// an instance of the enclosing class, which no name is written for,
// keeps the bare method name, as a call on `this` does.
calleeName = ctorName === 'this' || ctorName === 'this.constructor' ? methodName : `new ${ctorName}().${methodName}`;
} else if (
TS_JS_CHAIN_LANGUAGES.has(this.language) &&
receiver &&
+14 -2
View File
@@ -21,7 +21,7 @@ import {
isImportableKind,
CPP_DEFINE_SIGNATURE,
} from './types';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, isRustPathCallReachable, rustChainedCallByName, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, matchGoChainedCall, preferGoBuildTwin, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, matchPythonTypedLocalCall, isImportShadowedByJsBinding, isManualMockPath, isTestSuitePath } from './name-matcher';
import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchByFilePath, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, isRustForeignImplCallable, isRustPathCallReachable, rustChainedCallByName, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoProjectLocalCall, GO_STDLIB_PACKAGES, isGoBareName, goTypePositionTarget, goDotImportedSupertype, GO_TYPE_KINDS, matchGoAssertedCall, matchGoIndexedCall, matchGoChainedCall, preferGoBuildTwin, isGoAssertedLocal, isGoLocallyBound, isPythonFromImportModule, pythonAttrRefResolves, matchPythonTypedLocalCall, isImportShadowedByJsBinding, isManualMockPath, isTestSuitePath, matchNewReceiverCall, newReceiverClass, NEW_RECEIVER_SHAPE } from './name-matcher';
import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility';
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes';
@@ -1799,7 +1799,17 @@ export class ReferenceResolver {
CHAIN_SHAPE.test(ref.referenceName) &&
(ref.language === 'typescript' || ref.language === 'javascript' || ref.language === 'tsx' || ref.language === 'jsx' || ref.language === 'python')
) {
return this.gateLanguage(matchReference(ref, this.context), ref);
const chainResult = this.gateLanguage(matchReference(ref, this.context), ref);
// `new Sub().inherited()`: the method may live on a supertype of a
// project class, resolvable once extends edges exist — defer to the
// conformance pass like the other chained calls.
if (!chainResult && NEW_RECEIVER_SHAPE.test(ref.referenceName)) {
const cls = newReceiverClass(ref, this.context);
if (cls && this.context.getNodesByName(cls).some((n) => n.kind === 'class' && sameLanguageFamily(n.language, ref.language))) {
this.deferReference(ref, this.deferredChainRefs);
}
}
return chainResult;
}
const tImp = this.profileStages ? process.hrtime.bigint() : 0n;
@@ -2247,6 +2257,8 @@ export class ReferenceResolver {
// dotted-receiver languages on `.` (matchDottedCallChain).
const chainMatch = (ref.language === 'php' && PHP_PROP_SHAPE.test(ref.referenceName))
? matchMethodCall(ref, this.context)
: NEW_RECEIVER_SHAPE.test(ref.referenceName)
? matchNewReceiverCall(ref, this.context)
: SCOPED_CHAIN_LANGUAGES.has(ref.language)
? matchScopedCallChain(ref, this.context)
: matchDottedCallChain(ref, this.context);
+64
View File
@@ -25985,6 +25985,67 @@ function matchStoreAccessorChain(ref: UnresolvedRef, context: ResolutionContext)
return { original: ref, targetNodeId: callables[0]!.id, confidence: 0.6, resolvedBy: 'exact-match' };
}
/**
* TS/JS constructor receiver `new Runner(args).run()`, encoded by the
* extractor as `new Runner().run`. The receiver is an instance of the class
* written at the call, so this shape resolves only through that class.
*/
export const NEW_RECEIVER_SHAPE = /^new ([\w$.]+)\(\)\.([\w$]+)$/;
/**
* The class a `new C().m` ref constructs, as the name its declaration carries:
* `ns.Runner` → `Runner`, an aliased named import `R` → `Runner`. A JS/TS
* built-in (`RegExp`, `Map`, `Date`…) counts only when the file imports or
* declares a class of that name — otherwise a project class that merely
* shares the name would take every `new Map().get()` in the repo.
*/
export function newReceiverClass(ref: UnresolvedRef, context: ResolutionContext): string | null {
const m = ref.referenceName.match(NEW_RECEIVER_SHAPE);
if (!m || !m[1]) return null;
const written = m[1];
const last = written.split('.').pop()!;
const imported = written.includes('.')
? undefined
: context.getImportMappings(ref.filePath, ref.language).find((i) => i.localName === written);
if (imported && !imported.isDefault && !imported.isNamespace && /^[A-Za-z_$][\w$]*$/.test(imported.exportedName)) {
return imported.exportedName;
}
if (!imported && !written.includes('.') && JS_BUILT_INS.has(last) &&
!context.getNodesInFile(ref.filePath).some((n) => n.kind === 'class' && n.name === last)) {
return null;
}
return last;
}
/**
* Resolve a {@link NEW_RECEIVER_SHAPE} call on the constructed class or one of
* its supertypes (validated by resolveMethodOnType), or on a function assigned
* onto an ES5 constructor function's prototype in its file, or not at all: a
* class with no project declaration — `new URL(u).toString()` — has no
* project method to bind to.
*/
export function matchNewReceiverCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
const cls = newReceiverClass(ref, context);
const shape = ref.referenceName.match(NEW_RECEIVER_SHAPE);
const method = shape?.[2];
if (!cls || !method) return null;
// Several classes may share the name: the one the call's file imports
// under the written name is the class constructed (tsImportedTypeFile).
const onClass = resolveMethodOnType(cls, method, ref, context, 0.9, 'instance-method', tsImportedTypeFile(shape![1]!, ref, context));
if (onClass) return onClass;
// An ES5 constructor function (`function Params() {}`) keeps its methods as
// functions assigned onto its prototype in its own file
// (`prototype.render = function render() {…}`).
const ctors = context.getNodesByName(cls).filter((n) => n.kind === 'function' && sameLanguageFamily(n.language, ref.language));
if (ctors.length !== 1) return null;
const lines = context.getFileLines?.(ctors[0]!.filePath) ?? context.readFile(ctors[0]!.filePath)?.split(/\r?\n/);
const onProto = context.getNodesInFile(ctors[0]!.filePath).filter((n) =>
n.name === method && (n.kind === 'function' || n.kind === 'method') && /\bprototype\b/.test(lines?.[n.startLine - 1] ?? ''));
return onProto.length === 1
? { original: ref, targetNodeId: onProto[0]!.id, confidence: 0.8, resolvedBy: 'instance-method' }
: null;
}
/** Resolve the implementation inside the identified store, not a namesake or
* an interface signature elsewhere in the project. Import resolution already
* follows aliases/barrels; containment already excludes nested action locals. */
@@ -27385,6 +27446,9 @@ function matchReferenceInner(
const chained = nmTimed('pythonFactoryChain', ref, () => matchPythonFactoryChain(ref, context));
if (chained) return chained;
}
if (ref.referenceName.startsWith('new ')) {
return nmTimed('newReceiver', ref, () => matchNewReceiverCall(ref, context));
}
return nmTimed('storeAccessorChain', ref, () => matchStoreAccessorChain(ref, context));
}