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
+1
View File
@@ -18,6 +18,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In JavaScript and TypeScript, a call or `new` in a parameter's default value, like `function renderWithNote(note = buildNote())`, a destructured `{ size = defaultSize() }` or `constructor(private svc = new Service())`, now counts as the function's own call. Before, it was dropped, so `buildNote` was missing that caller and impact on it came back incomplete. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report and the pull request. (#2588)
- In React, a component rendered at module scope, in a top-level constant or inside a test's `it(…)` callback, like `createRoot(el).render(<App />)` or `render(<Card />)`, now counts as rendered there, so an app's entry reaches its root component and a component's tests show among its callers. (#2587)
- In JavaScript and TypeScript, react-dom's `root.render(<Popup />)` no longer links to your own class component's `render()`, such as an error boundary's, when `root` is declared outside the function making the call, like a module's `let root: Root | null`, or holds what `createRoot(el)` returned. A call on a variable declared outside the calling function now goes by the type that declaration gives it, so one declared with your own class, like `const store = new Store()` at the top of a file, links to that class's method, and a class component's `render()` and lifecycle methods, which only React calls, are no longer a guess for an untyped value. Re-index JavaScript and TypeScript projects after upgrading. Thanks @inth3shadows for the report. (#2589)
- In JavaScript and TypeScript, a method called on a freshly constructed object, like `new RegExp(p).exec(s)`, `new URL(u).toString()` or `new Runner().run()`, now links only to that class's own or inherited method, on the class the file imports when several share its name, and no longer to an unrelated project method of the same name; a class from outside your project links nothing. Re-index JavaScript and TypeScript projects after upgrading. Thanks @danusha2345. (#1945)
- In Python, `import a.b as X` written inside a function now links its calls like `X.f()`, as the same import at module level already did. Thanks @Egrn for the report. (#2600)
- In Python, `from app import email` no longer links the importing file to a project variable named `app`, such as a FastAPI or Flask `app = FastAPI()`. (#2584)
- In Python, a from-import of a module whose name has no dot, like `from loader import Registry` with `loader.py` at the repository root, or `from mypackage import helper` in a `src/` layout, now links the importing file to that module's file, as `import loader` and `from pkg.loader import Registry` already did. Before, the file's dependency on the module itself was left out, so `codegraph affected` and impact could miss the files that import it. When the file also imported something else under the module's name, like `from django.core import mail` beside `from mail import …`, the module was linked to that instead. Re-index Python projects after upgrading.
+1 -1
View File
@@ -138,7 +138,7 @@ describe('expression receivers', () => {
.map((r) => r.referenceName);
expect(refs).toEqual([
'list().map', 'list', 'x.run', 'y.run', 'x.stop', 'getTarget().install', 'getTarget',
'f', 'run', 'this.listeners[].push', 'this.svc.start', 'go', 'start',
'f', 'run', 'this.listeners[].push', 'this.svc.start', 'new Runner().go', 'start',
]);
});
});
@@ -103,6 +103,10 @@ async function exprReceivers(x) {
(() => 1).call(null);
this.a.b.run();
new Runner().go();
new RegExp(p).exec(s);
(new ns.Widget(1)).draw();
new (pick())().go();
new this.constructor().copy(x);
}
// --- component wrappers walked under the component ------------------------------
const PANEL_GAP = 8;
+6 -1
View File
@@ -300,6 +300,11 @@ async function exprReceivers(x, y) {
${typed ? 'this.items![0].save(); (this.items[1] as X).save();' : ''}
super.stop();
new Runner().go();
(new ns.Widget(1)).draw();
new (pick())().go();
new Runner().a.run();
new this.constructor().copy(x);
new this().init();
window.Api.start();
}
`, language);
@@ -310,7 +315,7 @@ async function exprReceivers(x, y) {
...(typed ? ['x.run', 'y.run', 'x.stop', 'getTarget().install', 'getTarget', 'has'] : []),
'f', 'run', 'this.listeners[].push', 'this.svc.start',
...(typed ? ['this.items[].save', 'this.items[].save'] : []),
'stop', 'go', 'start',
'stop', 'new Runner().go', 'new ns.Widget().draw', 'pick', 'copy', 'init', 'start',
]);
});
+166
View File
@@ -0,0 +1,166 @@
/**
* A method called on a `new C(...)` receiver resolves on `C` or nowhere.
* `new RegExp(p).exec(s)` used to reach the resolver as the bare `exec` and
* exact-match a project database wrapper's `exec`; the extractor now keeps the
* constructed class (`new RegExp().exec`), so a built-in or external class
* binds to nothing while a project class resolves its own (or inherited)
* method.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadAllGrammars } from '../src/extraction/grammars';
let dir: string;
let cg: CodeGraph;
const files: Record<string, string> = {
'src/db.ts':
'export class SqliteDatabase { exec(sql: string) { return sql; } get(k: string) { return k; } }\n' +
'export class Printer { toString() { return "p"; } }\n',
'src/runner.ts':
'export class Base { baseMethod() { return 1; } }\n' +
'export class Runner { run() { return 1; } }\n' +
'export class Sub extends Base { own() { return 2; } }\n',
'src/use.ts':
"import { Runner, Sub, Runner as R } from './runner';\n" +
"import * as rr from './runner';\n" +
'export function builtins(p: string, u: string, s: string) {\n' +
' const a = new RegExp(p).exec(s);\n' +
' const b = new URL(u).toString();\n' +
' const c = new Map([[1, 2]]).get(1);\n' +
' const d = new Date().getTime();\n' +
' return [a, b, c, d];\n' +
'}\n' +
'export function projectClasses() {\n' +
' return new Runner().run() + new Sub().baseMethod() + new Sub().own() + new R().run() + new rr.Runner().run();\n' +
'}\n',
'src/plain.js':
"const { Runner } = require('./runner');\n" +
'function jsCalls(p, s) {\n' +
' new RegExp(p).exec(s);\n' +
' new Set([1]).get(1);\n' +
' return new Runner().run();\n' +
'}\n' +
'module.exports = { jsCalls };\n',
};
beforeAll(async () => {
await initGrammars();
await loadAllGrammars();
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-newrecv-'));
for (const [rel, text] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true });
fs.writeFileSync(path.join(dir, rel), text);
}
cg = CodeGraph.initSync(dir);
await cg.indexAll();
});
afterAll(() => {
cg.destroy();
fs.rmSync(dir, { recursive: true, force: true });
});
const node = (name: string, file: string) =>
cg.getNodesByName(name).find((n) => n.filePath.endsWith(file) && (n.kind === 'function' || n.kind === 'method'))!;
const calleesOf = (name: string, file: string) =>
cg.getCallees(node(name, file).id)
.filter(({ edge }) => edge.kind === 'calls')
.map(({ node: n }) => n.qualifiedName)
.sort();
describe('new-expression receivers', () => {
it('TS: a built-in or external constructed class binds to no project method', () => {
expect(calleesOf('builtins', 'use.ts')).toEqual([]);
expect(cg.getCallers(node('exec', 'db.ts').id)).toEqual([]);
expect(cg.getCallers(node('get', 'db.ts').id)).toEqual([]);
expect(cg.getCallers(node('toString', 'db.ts').id)).toEqual([]);
});
it('TS: a project class resolves its own and inherited methods, via alias and namespace too', () => {
expect([...new Set(calleesOf('projectClasses', 'use.ts'))]).toEqual(['Base::baseMethod', 'Runner::run', 'Sub::own']);
expect(cg.getCallers(node('run', 'runner.ts').id).map(({ node: n }) => n.name)).toContain('projectClasses');
});
it('JS: the same rules apply', () => {
expect(calleesOf('jsCalls', 'plain.js')).toEqual(['Runner::run']);
});
it('TS: of two classes that share a name, the one the file imports is the one constructed', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-newrecv-twin-'));
const twin: Record<string, string> = {
'src/a/runner.ts': 'export class Runner { run() { return 1; } }\n',
'src/b/runner.ts': 'export class Runner { run() { return 2; } }\n',
'src/aa/use-a.ts': "import { Runner } from '../a/runner';\nexport function useA() { return new Runner().run(); }\n",
'src/zz/use-b.ts': "import { Runner as Job } from '../b/runner';\nexport function useB() { return new Job().run(); }\n",
};
for (const [rel, text] of Object.entries(twin)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), text);
}
const graph = CodeGraph.initSync(root);
try {
await graph.indexAll();
const target = (caller: string) => {
const from = graph.getNodesByName(caller).find((n) => n.kind === 'function')!;
return graph.getCallees(from.id).filter(({ edge }) => edge.kind === 'calls').map(({ node: n }) => n.filePath);
};
expect(target('useA')).toEqual(['src/a/runner.ts']);
expect(target('useB')).toEqual(['src/b/runner.ts']);
} finally {
graph.destroy();
fs.rmSync(root, { recursive: true, force: true });
}
});
it("a class building its own instance, with `new this.constructor()` or a static `new this()`, calls its own method", async () => {
// three.js's `clone() { return new this.constructor().copy(this); }`: no class name is
// written, so the call keeps the bare name, as a call on `this` does.
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-newrecv-self-'));
const own: Record<string, string> = {
'src/box.js':
'export class Box {\n' +
' copy(other) { return this; }\n' +
' init() { return this; }\n' +
' clone() { return new this.constructor().copy(this); }\n' +
' static create() { return new this().init(); }\n' +
'}\n',
'src/other.js': 'export class Other {\n copy(o) { return o; }\n init() { return 1; }\n}\n',
};
for (const [rel, text] of Object.entries(own)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), text);
}
const graph = CodeGraph.initSync(root);
try {
await graph.indexAll();
const callees = (caller: string) => {
const from = graph.getNodesByName(caller).find((n) => n.kind === 'method')!;
return graph.getCallees(from.id).filter(({ edge }) => edge.kind === 'calls').map(({ node: n }) => n.qualifiedName);
};
expect(callees('clone')).toEqual(['Box::copy']);
expect(callees('create')).toEqual(['Box::init']);
} finally {
graph.destroy();
fs.rmSync(root, { recursive: true, force: true });
}
});
it('extraction keeps the constructed class in the ref name', () => {
const src =
'function f(p, s) {\n' +
' new RegExp(p).exec(s);\n' +
' (new ns.Widget<T>(1)).draw();\n' +
' new (pick())().go();\n' +
' new Runner().a.run();\n' +
'}\n';
const refs = extractFromSource('t.ts', src, 'typescript').unresolvedReferences
.filter((r) => r.referenceKind === 'calls')
.map((r) => r.referenceName);
expect(refs).toEqual(['new RegExp().exec', 'new ns.Widget().draw', 'pick']);
});
});
+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,
}
}
+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));
}