diff --git a/CHANGELOG.md b/CHANGELOG.md index e16561e30..14b41d626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - A method that hands its call on to another object's method of the same name, like a wrapper's `def unbind(value) = wrapped.unbind(value)` or `form.validate()` inside its own `validate()`, no longer links that call back to itself, so the wrapper no longer looks recursive in callers and impact. Calls through `this` or `self`, bare calls, and calls in Go keep their link when they really are recursive. Thanks @danusha2345. - In VB.NET, a type name is looked up the way VB.NET does it — through the namespaces around it, the file's and project's `Imports` (aliases included), and the caller's own project — so a class declared in several namespaces or projects no longer draws every call to whichever copy was indexed first, and two candidates nothing tells apart get no link at all. - In VB.NET, a call with no receiver, or on `Me`, now reaches only a member of the class it is written in, of a class it inherits, or of a Module, and no longer a nearby class's member of the same name. +- In Java and Scala a call with no receiver, and in Python, TypeScript and JavaScript a call on `self` or `this`, now links to the method of the class it is written in, or to the one that class inherits, instead of a same-named method of another class declared closer in the file; in Java and Scala a call inside a nested class also finds the enclosing class's method, and a `super` call in Java or Python reaches the parent's. Re-index Java, Scala, Python, TypeScript and JavaScript projects after upgrading. Thanks @danusha2345. (#1952) - VB.NET designer code such as `New System.Drawing.Point(…)` or `Me.Size = New System.Drawing.Size(…)` is no longer linked to a project type or member that only shares the name, and a class nested inside another is matched by its bare name only from inside that class or one that inherits it. - Upgrading CodeGraph while an agent session is open no longer leaves the old version's background server in charge of your project: the first session started from the new install stops it and starts a current one in its place, even while sessions opened before the upgrade are still running. That old server could no longer load the language parsers the upgrade removed, so it saved every file it re-indexed with no symbols, while new sessions could only read the index beside it without keeping it up to date. A background server from a newer install is never stopped, and sessions opened before the upgrade keep the old version until you restart them. Thanks @lipchey for the report. (#2335) - A file is no longer saved with no symbols when its language parser can't be loaded, which is what happened to every file a background server re-indexed after an upgrade removed its install: the file keeps what it had and is indexed again once the parser loads, and files an earlier version emptied this way are re-indexed by the next sync. A background server also exits on its own once its install is upgraded or removed, so the next session starts one from the current install. Thanks @lipchey for the report. (#2335) diff --git a/__tests__/enclosing-class-member-call.test.ts b/__tests__/enclosing-class-member-call.test.ts new file mode 100644 index 000000000..543d35269 --- /dev/null +++ b/__tests__/enclosing-class-member-call.test.ts @@ -0,0 +1,211 @@ +/** + * A call that means a member of the class it is written in — a receiver-less + * `render()` in Java / Scala, or `this.render()` / `self.render()` in + * TypeScript / Python — binds to that class's + * member (then what it inherits, then an enclosing class), not to whichever + * same-named method is declared nearest the call. Each fixture puts the + * caller's own `render` far above the call and a sibling class's `render` + * right below it, which is what the same-file line-distance term used to pick. + * C++ has its own class lookup (matchCppImplicitThisCall); its cases here pin + * that it keeps the same answer. + */ + +import { describe, it, expect, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; + +let tempDir: string; +let cg: CodeGraph | null = null; + +afterEach(() => { + cg?.close(); + cg = null; + fs.rmSync(tempDir, { recursive: true, force: true }); +}); + +const filler = (prefix: string): string => + Array.from({ length: 40 }, (_, i) => `${prefix} filler ${i}`).join('\n'); + +/** qualified names of every `calls` target of the method/function `from`. */ +async function callees(file: string, source: string, from: string): Promise { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-enclosing-')); + fs.writeFileSync(path.join(tempDir, file), source); + cg = await CodeGraph.init(tempDir, { index: true }); + cg.resolveReferences(); + const caller = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find( + (n) => n.qualifiedName === from + ); + expect(caller).toBeDefined(); + return cg + .getOutgoingEdges(caller!.id) + .filter((e) => e.kind === 'calls') + .map((e) => cg!.getNode(e.target)?.qualifiedName ?? '?'); +} + +const OWN_CLASS: Array<[string, string, string, string]> = [ + ['Scala', 'A.scala', `class A { + def render(): String = "a" +${filler(' //')} + def show(): String = render() +} +class B { + def render(): String = "b" +} +`, 'A::show'], + ['C++ (inline members)', 'a.cpp', `class A { +public: + int render() { return 1; } +${filler(' //')} + int show() { return render(); } +}; +class B { +public: + int render() { return 2; } +}; +`, 'A::show'], + ['C++ (out-of-line members)', 'b.cpp', `class A { public: int render(); int show(); }; +class B { public: int render(); }; +int A::render() { return 1; } +${filler('//')} +int A::show() { return render(); } +int B::render() { return 2; } +`, 'A::show'], + ['Python (self.)', 'a.py', `class A: + def render(self): + return "a" +${filler(' #')} + def show(self): + return self.render() +class B: + def render(self): + return "b" +`, 'A::show'], + ['TypeScript (this.)', 'a.ts', `export class A { + render() { return "a"; } +${filler(' //')} + show() { return this.render(); } +} +export class B { + render() { return "b"; } +} +`, 'A::show'], +]; + +describe('a call meaning a member of the enclosing class', () => { + it.each(OWN_CLASS)('%s: binds to the caller\'s own class, not the nearer sibling', async (_lang, file, source, from) => { + const out = await callees(file, source, from); + expect(out.map((q) => q.toLowerCase())).toContain('a::render'); + expect(out.map((q) => q.toLowerCase())).not.toContain('b::render'); + }); + + it('Java: `super.render()` skips the overriding method itself', async () => { + const out = await callees('S.java', `class Base { + String render() { return "base"; } +} +${filler('//')} +class Kid extends Base { + String render() { return super.render(); } +} +`, 'Kid::render'); + expect(out).toEqual(['Base::render']); + }); + + it('Java: same-named nested types only inherit from what their own header names', async () => { + const out = await callees('Msg.java', `interface AOrBuilder { String describe(); } +interface BOrBuilder { String describe(); } +class A { + static class Builder implements AOrBuilder { + public String describe() { return "a"; } + } +} +class B { + static class Builder implements BOrBuilder { + String show() { return describe(); } + public String describe() { return "b"; } + } +} +class C { + static abstract class Builder implements AOrBuilder { +${filler(' //')} + String show() { return describe(); } + } +} +`, 'C::Builder::show'); + expect(out).toEqual(['AOrBuilder::describe']); + }); +}); + +/** `qualifiedName@file` of every `calls` target of `from`, in a project of several files. */ +async function calleesIn(files: Record, from: string): Promise { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-enclosing-')); + for (const [rel, text] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(tempDir, rel)), { recursive: true }); + fs.writeFileSync(path.join(tempDir, rel), text); + } + cg = await CodeGraph.init(tempDir, { index: true }); + cg.resolveReferences(); + const caller = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === from); + expect(caller).toBeDefined(); + return cg + .getOutgoingEdges(caller!.id) + .filter((e) => e.kind === 'calls') + .map((e) => { + const target = cg!.getNode(e.target); + return target ? `${target.qualifiedName}@${target.filePath}` : '?'; + }); +} + +describe('which inherited member a call reaches', () => { + it('Python: a base named like types in other files is not taken for the one another file declares', async () => { + const out = await calleesIn({ + 'a/base.py': 'class BaseEditor:\n def execute(self, sql):\n return sql\n', + 'b/pg.py': 'from a.base import BaseEditor\n\nclass DatabaseSchemaEditor(BaseEditor):\n def execute(self, sql):\n return super().execute(sql)\n', + 'c/lite.py': 'from a.base import BaseEditor\n\nclass DatabaseSchemaEditor(BaseEditor):\n sql_create = "x"\n', + 'd/gis.py': 'from c.lite import DatabaseSchemaEditor\n\nclass GisEditor(DatabaseSchemaEditor):\n def drop(self):\n return self.execute("drop")\n', + }, 'GisEditor::drop'); + expect(out).not.toContain('DatabaseSchemaEditor::execute@b/pg.py'); + }); + + it('Python: the first base listed that has the member is the one `self.` reaches', async () => { + const files = { + 'mixins.py': 'class Second:\n def describe(self):\n return "second"\n\nclass First:\n def describe(self):\n return "first"\n\n' + + 'class Both(First, Second):\n def show(self):\n return self.describe()\n\n' + + 'class Other(Second, First):\n def show(self):\n return self.describe()\n', + }; + expect(await calleesIn(files, 'Both::show')).toEqual(['First::describe@mixins.py']); + cg?.close(); + cg = null; + fs.rmSync(tempDir, { recursive: true, force: true }); + expect(await calleesIn(files, 'Other::show')).toEqual(['Second::describe@mixins.py']); + }); + + it('Java: a class further up that implements the member wins over an interface that only declares it', async () => { + const out = await calleesIn({ + 'p/Rotation.java': 'package p;\ninterface Rotation {\n String current();\n}\n', + 'p/Dispatcher.java': 'package p;\nclass Dispatcher {\n String current() { return "d"; }\n}\n', + 'p/Contextual.java': 'package p;\nclass Contextual extends Dispatcher {\n}\n', + 'p/RotationButton.java': 'package p;\nclass RotationButton extends Contextual implements Rotation {\n String show() { return current(); }\n}\n', + }, 'p::RotationButton::show'); + expect(out).toEqual(['p::Dispatcher::current@p/Dispatcher.java']); + }); + + it('Java: a variable named `parent` is not `super`', async () => { + // The extractor drops the `parent.` receiver, so the call arrives as a + // bare `getName()`; it must not be read as `super.getName()`. + const out = await calleesIn({ + 'p/Base.java': 'package p;\nclass Base {\n String getName() { return "base"; }\n}\n', + 'p/Child.java': 'package p;\nclass Node {\n String getName() { return "node"; }\n}\nclass Child extends Base {\n Node parent;\n String getName() { return "child"; }\n String show() { return parent.getName(); }\n}\n', + }, 'p::Child::show'); + expect(out).not.toContain('p::Base::getName@p/Base.java'); + }); + + it('C++: a call in a namespace takes the overload its arguments fit, not the one in its own file', async () => { + const out = await calleesIn({ + 'core.h': '#pragma once\nnamespace detail {\ntemplate Out copy(In b, In e, Out out) { return out; }\n}\n', + 'format.h': '#pragma once\n#include "core.h"\nnamespace detail {\ntemplate Out copy(V s, Out out) { return out; }\ntemplate Out write_it(Out out, const Char* b, const Char* e) {\n return copy(b, e, out);\n}\n}\n', + }, 'detail::write_it'); + expect(out).toEqual(['detail::copy@core.h']); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index a488ee0bf..e93d4ed7a 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -10,6 +10,7 @@ import { Language, Node, IMPLICIT_RECORD_ACCESSOR_DOC } from '../types'; import { escapeRegExp } from '../utils'; import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSupertypeTarget, CPP_DEFINE_SIGNATURE, isInheritanceRef, isImportableKind } from './types'; import { blankStringContents, stripCommentsForRegex } from './strip-comments'; +import { isJsSuperCallSite } from './js-super-calls'; import { JS_ARRAY_METHODS, JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins'; import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-visibility'; import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, dartPrivateVisible, inSameDartLibrary } from './dart-libraries'; @@ -11876,6 +11877,296 @@ function innerScopeShadows(nodes: Node[], ref: UnresolvedRef, context: Resolutio (s.startLine < n.startLine || (s.endLine ?? s.startLine) > (n.endLine ?? n.startLine)))); } +/** + * Languages where a receiver-less call inside a class body means `this.m()`, + * and whose scoping filters in matchByExactName can still leave the caller's + * own member and a sibling class's side by side (Scala keeps every same-file + * member; Java keeps every type a same-named nested type extends). Kotlin, + * C#, Swift, Dart and Ruby filter a bare call to what is in reach, and C++ + * has its own class lookup (matchCppImplicitThisCall). + */ +const IMPLICIT_THIS_LANGUAGES = new Set(['java', 'scala']); +/** Languages whose extractor drops a `this.`/`self.` receiver but where a bare call never means a member. */ +const EXPLICIT_THIS_LANGUAGES = new Set(['python', 'typescript', 'tsx', 'javascript', 'jsx']); +const MEMBER_OWNER_KINDS = new Set(['class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'union', 'module']); + +/** + * How a call names its receiver, read back from the call site's line: the + * extractor emits `render()`, `this.render()`, `self.render()` and + * `super.render()` all as `render`. `null` when the line can't be read or the + * receiver is something else. + */ +function callReceiverKind(ref: UnresolvedRef, context: ResolutionContext): 'bare' | 'self' | 'super' | null { + const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] + ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1]; + if (line === undefined) return null; + const nameEsc = ref.referenceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // The ref's column is the call expression's start, except in Dart, where it + // is the argument list right after the name: take the first occurrence at + // or after the column, else the last one before it. + let at = -1; + for (const m of line.matchAll(new RegExp('(?= ref.column) break; + } + if (at < 0) return null; + const before = line.slice(0, at); + // Only the languages' own words: Java's extractor drops a `parent.` or + // `cls.` receiver too, and there it is a variable (protobuf's + // `parent.getFullName()` on the EnumDescriptor an enum value belongs to). + if (/(?:^|[^\w$.])super(?:\s*\([^()]*\)|<[^<>]*>)?\s*\.\s*$/.test(before)) return 'super'; + if (/(?:^|[^\w$.])(?:this|self)\s*\??\.\s*$/.test(before)) return 'self'; + if (ref.language === 'python' && /(?:^|[^\w$.])cls\s*\.\s*$/.test(before)) return 'self'; + if (/(?:\.|->|::)\s*$/.test(before)) return null; + return 'bare'; +} + +/** + * A call that means a member of the class it is written in — a receiver-less + * `render()` in Java / Scala, or `this.render()` / `self.render()` in + * TypeScript / JavaScript / Python — binds by + * the language's scoping, not by which same-named method sits nearest: the + * caller's own class first, then what it inherits, then (for a bare call) the + * classes it is nested in. Without this the same-file line-distance term + * handed `RegName.toString()`'s `render()` to a longer sibling class's + * `render` declared closer to the call. Walks the caller's qualified-name + * scopes innermost-out and returns the first candidate declared directly in + * one (a local function of an enclosing method counts, same file only); a + * `super.` call skips its own class. Returns null — existing ranking — when + * nothing in scope declares the name. + */ +function matchEnclosingScopeMember( + ref: UnresolvedRef, + candidates: Node[], + context: ResolutionContext +): Node | null { + if (ref.referenceKind !== 'calls' || /[.:]/.test(ref.referenceName)) return null; + const implicit = IMPLICIT_THIS_LANGUAGES.has(ref.language); + if (!implicit && !EXPLICIT_THIS_LANGUAGES.has(ref.language)) return null; + const caller = context.getNodeById?.(ref.fromNodeId); + if (!caller?.qualifiedName) return null; + const receiver = callReceiverKind(ref, context); + if (!receiver || (receiver === 'bare' && !implicit)) return null; + + const name = ref.referenceName; + // One pass over the candidates, keyed by the type that declares each: a + // ubiquitous name (`get`, `init`) can bring thousands, and the walk below + // asks about several scopes and supertypes. + const byOwner = new Map(); + const byOwnerName = new Map(); + for (const c of candidates) { + const sep = c.qualifiedName.lastIndexOf('::'); + if (sep <= 0 || c.qualifiedName.slice(sep + 2) !== name) continue; + const owner = c.qualifiedName.slice(0, sep); + const simple = owner.includes('::') ? owner.slice(owner.lastIndexOf('::') + 2) : owner; + byOwner.set(owner, [...(byOwner.get(owner) ?? []), c]); + byOwnerName.set(simple, [...(byOwnerName.get(simple) ?? []), c]); + } + // Whatever a scope declares under the name shadows the scopes outside it — + // a Scala `val` applied like a function, a nested class constructed by a + // bare call — but a method is the likelier target when both exist. + const callableFirst = (found: Node[]): Node[] => + [...found].sort((a, b) => Number(b.kind === 'method' || b.kind === 'function') - Number(a.kind === 'method' || a.kind === 'function')); + // `Owner::Owner` is a constructor: a bare `Owner(...)` builds a new + // instance, it does not call an inherited or enclosing member. + const members = (owner: string): Node[] => + owner === name || owner.endsWith(`::${name}`) + ? [] + : callableFirst(byOwner.get(owner) ?? []); + const typesNamed = (qn: string): Node[] => + context.getNodesByQualifiedName(qn).filter((n) => MEMBER_OWNER_KINDS.has(n.kind) && sameLanguageFamily(n.language, ref.language)); + // One member of `owner`: the caller's file first; another file only when a + // single type in the index carries that qualified name. + const pick = (owner: string, isType: boolean): Node | null => { + const found = members(owner); + if (found.length === 0) return null; + const local = found.find((c) => c.filePath === ref.filePath); + if (local) return local; + return isType && typesNamed(owner).length === 1 ? found[0]! : null; + }; + // The declaration of `scope` the call sits in, else the only one. + const declOf = (scope: string): Node | null => { + const decls = typesNamed(scope); + const enclosing = decls.filter((n) => n.filePath === ref.filePath && n.startLine <= ref.line && (n.endLine ?? n.startLine) >= ref.line); + if (enclosing.length > 0) return enclosing[enclosing.length - 1]!; + return decls.length === 1 ? decls[0]! : null; + }; + // A declaration's supertypes through its own resolved extends/implements + // edges, in the order its header names them. + const supertypesOf = (type: Node): Node[] => { + const supers = context.getSupertypeNodes?.(type.id) ?? []; + if (supers.length < 2) return supers; + const header = headerOf(type); + const at = (s: Node): number => { + const i = header.search(new RegExp('(? at(a) - at(b)); + }; + // Inherited members through those edges, breadth-first: the members of the + // very types the declaration extends, where a walk by name reaches every + // type of that name (Django's per-backend `DatabaseSchemaEditor`s differ in + // what they override). + const inheritedThrough = (type: Node): Node | null => { + const seen = new Set([type.id]); + let frontier = [type]; + // An interface's or an abstract declaration answers only when no class + // further up implements the member: SystemUI's `RotationContextButton + // extends ContextualButton implements RotationButton` runs + // ButtonDispatcher's `getCurrentView`, not RotationButton's. + let declared: Node | null = null; + for (let depth = 0; depth < 4 && frontier.length > 0; depth++) { + const next: Node[] = []; + for (const t of frontier) { + for (const s of supertypesOf(t)) { + if (seen.has(s.id)) continue; + seen.add(s.id); + next.push(s); + if (s.name === name) continue; + const abstractOnly = s.kind === 'interface' || s.kind === 'protocol'; + const found = callableFirst((byOwner.get(s.qualifiedName) ?? []).filter((c) => + c.filePath === s.filePath && + !(receiver === 'super' && (c.isAbstract || abstractOnly)))); + if (found.length === 0) continue; + if (!abstractOnly && !found[0]!.isAbstract) return found[0]!; + declared ??= found[0]!; + } + } + frontier = next; + } + return declared; + }; + // Inherited members, breadth-first over the resolved extends/implements + // edges, starting from the caller's own type's supertypes. + const inherited = (firstSupers: string[], typeName: string): Node | null => { + if (!context.getSupertypes) return null; + let frontier = [typeName]; + const seen = new Set(frontier); + for (let depth = 0; depth < 4 && frontier.length > 0; depth++) { + const next: string[] = []; + for (const t of frontier) { + for (const s of depth === 0 ? firstSupers : context.getSupertypes(t, ref.language)) { + if (seen.has(s)) continue; + seen.add(s); + next.push(s); + if (s === name) continue; + // Supertypes come back as bare names: when the caller's language + // declares a type of that name, a same-named type of another + // language (Scala `Results` vs Java `play.mvc.Results`) is not it. + if (!byOwnerName.has(s)) continue; + const ownLanguage = context.getNodesByName(s).some((n) => MEMBER_OWNER_KINDS.has(n.kind) && n.language === ref.language); + const found = callableFirst((byOwnerName.get(s) ?? []).filter((c) => + (!ownLanguage || c.language === ref.language) && + // `super.m()` runs a superclass body, never an interface's + // abstract declaration. + !(receiver === 'super' && (c.isAbstract || isInterfaceMember(c))))); + // Overloads share a file; copies of a same-named type in several + // files are a guess unless one is the caller's. + if (found.length > 0 && found.every((c) => c.filePath === found[0]!.filePath)) return found[0]!; + if (found.length > 0) return found.find((c) => c.filePath === ref.filePath) ?? null; + } + } + frontier = next; + } + return null; + }; + + const segments = caller.qualifiedName.split('::'); + // A method's scopes start at its owner; a class caller (a field initializer) + // is its own scope. + const start = MEMBER_OWNER_KINDS.has(caller.kind) ? segments.length : segments.length - 1; + for (let i = start; i > 0; i--) { + const scope = segments.slice(0, i).join('::'); + const isType = typesNamed(scope).length > 0; + if (!isType) { + // `this.`/`super.` name a class member; only a bare call sees locals. + if (receiver !== 'bare') continue; + const localFn = members(scope).find((c) => c.filePath === ref.filePath && (c.kind === 'method' || c.kind === 'function')); + if (localFn) return localFn; + continue; + } + if (receiver !== 'super') { + const own = pick(scope, true); + if (own) return own; + } + // The type's own supertypes first; by name when they reach nothing, as + // when the index resolved an `extends` to a namesake (Minestom's + // `Player extends LivingEntity` to a nested `EntityStatuses.LivingEntity`). + const decl = declOf(scope); + const supers = directSupertypes(scope, segments[i - 1]!); + const viaSuper = (decl && inheritedThrough(decl)) || inherited(supers, segments[i - 1]!); + if (viaSuper) return viaSuper; + // `this`/`self`/`super` is the innermost instance; outer classes are + // reachable only by a bare call. + if (receiver !== 'bare') return null; + // A supertype outside the index (an Android `View`, the listener an + // anonymous class implements) may declare the name, and it would win + // over the outer class: stop rather than guess past it. + if (supers.length === 0 && declaresSupertype(scope, segments[i - 1]!)) return null; + } + return null; + + /** + * The declaration's header — its text up to the body's `{`. The declaration + * that encloses the call when there is one (a Scala `object Cookies` beside + * a `trait Cookies`), else every declaration of that name (a C++ class whose + * member is defined out of line). + */ + function typeHeader(scope: string): string | null { + const decls = typesNamed(scope); + const enclosing = decls.filter((n) => n.filePath === ref.filePath && n.startLine <= ref.line && (n.endLine ?? n.startLine) >= ref.line); + let header = ''; + for (const decl of enclosing.length > 0 ? enclosing.slice(-1) : decls) { + const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split('\n'); + if (!lines) return null; + for (let l = decl.startLine - 1; l < Math.min(lines.length, decl.startLine + 30); l++) { + const brace = lines[l]!.indexOf('{'); + header += ' ' + (brace >= 0 ? lines[l]!.slice(0, brace) : lines[l]); + if (brace >= 0) break; + } + } + return decls.length > 0 ? header : null; + } + + /** A type declaration's header: its text up to the body's `{`. */ + function headerOf(decl: Node): string { + const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split('\n'); + let header = ''; + for (let l = decl.startLine - 1; lines && l < Math.min(lines.length, decl.startLine + 30); l++) { + const brace = lines[l]!.indexOf('{'); + header += ' ' + (brace >= 0 ? lines[l]!.slice(0, brace) : lines[l]); + if (brace >= 0) break; + } + return header; + } + + // getSupertypes answers for every type of that simple name (all of a + // protobuf file's `Builder`s at once), so keep the ones this declaration's + // header actually names, in the order it names them: the first base that + // has the member is the one a call reaches (`class A(B, C)`). + function directSupertypes(scope: string, simple: string): string[] { + const all = context.getSupertypes?.(simple, ref.language) ?? []; + if (all.length === 0) return all; + const header = typeHeader(scope); + if (header === null) return all; + const at = (t: string): number => header.search(new RegExp('(? at(t) >= 0).sort((a, b) => at(a) - at(b)); + } + + function isInterfaceMember(member: Node): boolean { + const sep = member.qualifiedName.lastIndexOf('::'); + return sep > 0 && context.getNodesByQualifiedName(member.qualifiedName.slice(0, sep)).some((n) => n.kind === 'interface' || n.kind === 'protocol'); + } + + function declaresSupertype(scope: string, simple: string): boolean { + if (simple.startsWith('<')) return true; // an anonymous class always has one + const header = typeHeader(scope); + if (header === null) return true; + // `extends` / `implements` / `with`, a C++ `:` base list. + return /\b(?:extends|implements|with)\b|[\w>)\]]\s*:(?!:)/.test(header); + } +} + /** * Try to resolve a reference by exact name match */ @@ -12093,18 +12384,37 @@ export function matchByExactName( }; } + // A member of the caller's own class (or what it inherits / is nested in) + // is what the name means there, however far away it is declared. Not for a + // call written on `super`: the one in a class expression's method gets here + // (js-super-calls declines to place it), and its `super` is that + // expression's parent, never the enclosing class's (#2497). + const member = isJsSuperCallSite(ref, context) ? null : matchEnclosingScopeMember(ref, candidates, context); + // The scope names the member, not the overload: when the class declares + // the name more than once, the one the arguments fit, else the nearest, is + // chosen below — among those overloads only, so a sibling class's namesake + // stays out of it. + let pool = candidates; + if (member) { + const overloads = candidates.filter((c) => c.qualifiedName === member.qualifiedName && c.filePath === member.filePath); + if (overloads.length <= 1) { + return { original: ref, targetNodeId: member.id, confidence: 0.9, resolvedBy: 'exact-match' }; + } + pool = overloads; + } + // Ubiquitous-name ceiling (#999): above it, picking one target among K // same-named defs by directory proximity is unreliable AND O(K) per ref — the // quadratic behind the "Resolving refs" wedge on theme/SDK-vendoring repos. // Decline; the precise strategies (qualified-name, import, class-name) already // ran. Falls through to fuzzy, which itself only resolves a UNIQUE candidate. - if (candidates.length > AMBIGUOUS_NAME_CEILING) { + if (pool.length > AMBIGUOUS_NAME_CEILING) { return null; } // Multiple matches - try to narrow down - const nearest = findBestMatch(ref, candidates, context); - const bestMatch = nearest && cppFunctionOverload(nearest, candidates, ref, context); + const nearest = findBestMatch(ref, pool, context); + const bestMatch = nearest && cppFunctionOverload(nearest, pool, ref, context); if (bestMatch && isCrossFileReachable(bestMatch, ref, context)) { // Lower confidence when the match is from a distant/unrelated module const proximity = computePathProximity(ref.filePath, bestMatch.filePath);