diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e544b5..a3e955c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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()` or `render()`, 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()` 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. diff --git a/__tests__/expression-receiver-calls.test.ts b/__tests__/expression-receiver-calls.test.ts index 17c09f39..3e8aeb08 100644 --- a/__tests__/expression-receiver-calls.test.ts +++ b/__tests__/expression-receiver-calls.test.ts @@ -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', ]); }); }); diff --git a/__tests__/fixtures/kernel-parity/torture.js b/__tests__/fixtures/kernel-parity/torture.js index 71b8b3fc..9516474b 100644 --- a/__tests__/fixtures/kernel-parity/torture.js +++ b/__tests__/fixtures/kernel-parity/torture.js @@ -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; diff --git a/__tests__/kernel-tsjs-parity.test.ts b/__tests__/kernel-tsjs-parity.test.ts index aabba4f4..a0f3b7dd 100644 --- a/__tests__/kernel-tsjs-parity.test.ts +++ b/__tests__/kernel-tsjs-parity.test.ts @@ -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', ]); }); diff --git a/__tests__/new-expression-receiver.test.ts b/__tests__/new-expression-receiver.test.ts new file mode 100644 index 00000000..18ac2dc8 --- /dev/null +++ b/__tests__/new-expression-receiver.test.ts @@ -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 = { + '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 = { + '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 = { + '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(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']); + }); +}); diff --git a/codegraph-kernel/src/tsjs/extractors.rs b/codegraph-kernel/src/tsjs/extractors.rs index 8ab23568..f97ae6d6 100644 --- a/codegraph-kernel/src/tsjs/extractors.rs +++ b/codegraph-kernel/src/tsjs/extractors.rs @@ -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 ().` 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, } } diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index e070e96a..68b1a808 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -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 && diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 95ac850a..a4c8c35c 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -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); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 35d4403b..f6d7916f 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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)); }