From 6678cace96f20223611a35367469d005496dfef5 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sat, 10 Oct 2026 02:33:44 -0500 Subject: [PATCH 1/2] wip: C++ implicit-this member lookup --- __tests__/cpp-implicit-this-calls.test.ts | 381 ++++++++++++++++++++++ src/resolution/index.ts | 16 + src/resolution/name-matcher.ts | 330 ++++++++++++++++++- src/resolution/types.ts | 8 + 4 files changed, 734 insertions(+), 1 deletion(-) create mode 100644 __tests__/cpp-implicit-this-calls.test.ts diff --git a/__tests__/cpp-implicit-this-calls.test.ts b/__tests__/cpp-implicit-this-calls.test.ts new file mode 100644 index 0000000000..36e65ce91a --- /dev/null +++ b/__tests__/cpp-implicit-this-calls.test.ts @@ -0,0 +1,381 @@ +/** + * A C++ call written with no receiver, or on `this`, inside a member function + * is a call on the function's object: C++ looks the name up in the function's + * class, then in the classes it derives from, then in the classes it is nested + * in, and only then at namespace scope. The resolver matched it by name alone, + * so a same-named member of another class won by file proximity: + * - protobuf's generated `Api::operator=` calling `InternalSwap(&from)` went to + * `Any::InternalSwap`, in every generated message class; + * - `Any::InternalSwap` calling the `GetArena()` it inherits from `MessageLite` + * went to `Arena::InternalHelper::GetArena`; + * - googletest's `~linked_ptr() { depart(); }` (vendored in rocksdb) went to + * `linked_ptr_internal::depart`, declared just above it. + * Nothing changes for a call the lookup can't place: a free function's, one + * through a receiver the extractor dropped, one naming a parameter, or one + * into a base that depends on a template parameter. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +function writeProject(prefix: string, files: Record): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return root; +} + +/** The edges carrying a call's name out of the function or method `qualifiedName` in `file`. */ +function callEdgesFrom(cg: CodeGraph, file: string, qualifiedName: string) { + const from = cg.getNodesInFile(file).filter((n) => n.qualifiedName === qualifiedName && (n.kind === 'function' || n.kind === 'method')); + expect(from.length).toBeGreaterThan(0); + return cg.getOutgoingEdgesFrom(from.map((n) => n.id)).flatMap((e) => { + const metadata = e.metadata as { refName?: string; resolvedBy?: string } | undefined; + return metadata?.refName ? [{ refName: metadata.refName, resolvedBy: metadata.resolvedBy, target: cg.getNode(e.target)!.qualifiedName }] : []; + }); +} + +/** `name -> target qualified name` for each call (or construction) the named function or method in `file` makes. */ +function callsFrom(cg: CodeGraph, file: string, qualifiedName: string): string[] { + return callEdgesFrom(cg, file, qualifiedName).map((e) => `${e.refName} -> ${e.target}`).sort(); +} + +/** Whether C++ name lookup from the calling function (not a guess by name) placed the call `refName`. */ +function placedByLookup(cg: CodeGraph, file: string, qualifiedName: string, refName: string): boolean { + return callEdgesFrom(cg, file, qualifiedName).some((e) => e.refName === refName && e.resolvedBy === 'instance-method'); +} + +const MESSAGE_LITE = `#pragma once +namespace pb { +class Arena { + public: + class InternalHelper { + public: + static Arena* GetArena(const void* object) { return nullptr; } + }; +}; + +class MessageLite { + public: + Arena* GetArena() const { return arena_; } + void Clear() {} + + private: + Arena* arena_ = nullptr; +}; + +class Message : public MessageLite {}; +} // namespace pb +`; + +// Two generated message classes, each defining InternalSwap out of line; +// Any's comes first in the index. +const ANY_H = `#pragma once +#include "pb/message.h" +namespace pb { +class Any final : public Message { + public: + Any& operator=(Any&& from) noexcept { + InternalSwap(&from); + return *this; + } + void InternalSwap(Any* other); +}; +} // namespace pb +`; + +const API_H = `#pragma once +#include "pb/message.h" +namespace pb { +class Api final : public Message { + public: + Api& operator=(Api&& from) noexcept { + InternalSwap(&from); + return *this; + } + void Swap(Api* other) { + if (other == this) return; + this->InternalSwap(other); + } + void InternalSwap(Api* other); +}; +} // namespace pb +`; + +describe('C++ calls on the calling function’s own object', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-cpp-this-', { + 'src/pb/message.h': MESSAGE_LITE, + 'src/pb/any.pb.h': ANY_H, + 'src/pb/any.pb.cc': `#include "pb/any.pb.h" +namespace pb { +void Any::InternalSwap(Any* other) { + auto* arena = GetArena(); + (void)arena; +} +} // namespace pb +`, + 'src/pb/api.pb.h': API_H, + 'src/pb/api.pb.cc': `#include "pb/api.pb.h" +namespace pb { +void Api::InternalSwap(Api* other) { + (*this).Clear(); +} +} // namespace pb +`, + 'third_party/gtest/linked_ptr.h': `#pragma once +namespace testing { +namespace internal { +class linked_ptr_internal { + public: + bool depart() { return next_ == this; } + + private: + mutable linked_ptr_internal const* next_; +}; + +template +class linked_ptr { + public: + ~linked_ptr() { depart(); } + void reset(T* ptr = nullptr) { + depart(); + value_ = ptr; + } + + T* get() const { return value_; } + T* operator->() const { return value_; } + T& operator*() const { return *value_; } + + bool operator==(T* p) const { return value_ == p; } + bool operator!=(T* p) const { return value_ != p; } + + template + bool operator==(linked_ptr const& ptr) const { + return value_ == ptr.get(); + } + template + bool operator!=(linked_ptr const& ptr) const { + return value_ != ptr.get(); + } + + private: + template + friend class linked_ptr; + + // Takes ownership of p, which must be the only linked_ptr to it: + // the link starts out as a one-element ring. + void capture(T* p) { + value_ = p; + } + + // Joins the ring another linked_ptr belongs to, sharing what it points to. + template + void copy(linked_ptr const* ptr) { + value_ = ptr->get(); + } + + // Leaves the ring; the last one out deletes the object. + void depart() { + if (link_.depart()) delete value_; + } + + T* value_; + linked_ptr_internal link_; +}; +} // namespace internal +} // namespace testing +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }, 60_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('links a bare call to the calling class’s own method, not another class’s namesake', () => { + expect(callsFrom(cg, 'src/pb/api.pb.h', 'pb::Api::operator=')).toEqual(['InternalSwap -> pb::Api::InternalSwap']); + expect(callsFrom(cg, 'src/pb/any.pb.h', 'pb::Any::operator=')).toEqual(['InternalSwap -> pb::Any::InternalSwap']); + }); + + it('links a call on `this->` or `(*this).` the same way', () => { + expect(callsFrom(cg, 'src/pb/api.pb.h', 'pb::Api::Swap')).toEqual(['InternalSwap -> pb::Api::InternalSwap']); + expect(callsFrom(cg, 'src/pb/api.pb.cc', 'pb::Api::InternalSwap')).toEqual(['Clear -> pb::MessageLite::Clear']); + }); + + it('links a method the class inherits, through its own base classes', () => { + expect(callsFrom(cg, 'src/pb/any.pb.cc', 'pb::Any::InternalSwap')).toEqual(['GetArena -> pb::MessageLite::GetArena']); + }); + + it('prefers the class’s own method over a namesake declared nearer the call', () => { + expect(callsFrom(cg, 'third_party/gtest/linked_ptr.h', 'testing::internal::linked_ptr::~linked_ptr')) + .toEqual(['depart -> testing::internal::linked_ptr::depart']); + expect(callsFrom(cg, 'third_party/gtest/linked_ptr.h', 'testing::internal::linked_ptr::reset')) + .toEqual(['depart -> testing::internal::linked_ptr::depart']); + }); +}); + +describe('C++ name lookup from a member function', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-cpp-this-scopes-', { + 'src/db/db.h': `#pragma once +#include +namespace db { +class Stats { + public: + static int Count() { return 1; } +}; + +class Table { + public: + static int Count(); + void Flush(); + void Compact(); + void Reset() {} + void Close() {} + void OnDone(std::function Close) { Close(); } + + class Builder { + public: + int Size() const { return Count(); } + }; +}; + +struct Item { + void Clear() {} +}; +} // namespace db +`, + 'src/db/table.cc': `#include "db/db.h" +namespace db { +static void Reset() {} +static void Trim() {} + +class Job { + public: + void Compact() {} +}; + +void Table::Flush() { + Reset(); + Trim(); + auto finish = [this]() { Compact(); }; + finish(); +} + +void Rebuild() { + Trim(); + Reset(); +} +} // namespace db +`, + 'src/db/compact.cc': `#include "db/db.h" +namespace db { +int Table::Count() { return 0; } + +void Table::Compact() { + Item items[2]; + items[0].Clear(); +} +} // namespace db +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }, 60_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('takes the class’s member over a file-static function of its name, and the function when the class has none', () => { + const calls = callsFrom(cg, 'src/db/table.cc', 'db::Table::Flush'); + expect(calls).toContain('Reset -> db::Table::Reset'); + expect(calls).toContain('Trim -> db::Trim'); + }); + + it('links a bare call in a lambda to the member function’s class', () => { + expect(callsFrom(cg, 'src/db/table.cc', 'db::Table::Flush')).toContain('Compact -> db::Table::Compact'); + }); + + it('links a nested class’s bare call to the enclosing class’s static member', () => { + expect(callsFrom(cg, 'src/db/db.h', 'db::Table::Builder::Size')).toEqual(['Count -> db::Table::Count']); + }); + + it('leaves a free function, static or not, to find what its namespace declares', () => { + expect(callsFrom(cg, 'src/db/table.cc', 'db::Rebuild')).toEqual(['Reset -> db::Reset', 'Trim -> db::Trim']); + }); + + it('never takes a receiver the extractor dropped for the calling object', () => { + expect(callsFrom(cg, 'src/db/compact.cc', 'db::Table::Compact')).toEqual(['Clear -> db::Item::Clear']); + }); + + it('does not take a parameter named like a member for the member', () => { + expect(placedByLookup(cg, 'src/db/db.h', 'db::Table::OnDone', 'Close')).toBe(false); + }); +}); + +describe('C++ name lookup in a class template', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-cpp-this-template-', { + 'src/util/list.h': `#pragma once +namespace util { +template +class ListBase { + public: + void Grow(int n) {} + void Shrink() {} +}; + +inline void Grow(int n) {} + +template +class List : public ListBase { + public: + void Push() { + Grow(1); + this->Shrink(); + } +}; + +class IntList : public ListBase { + public: + void Push() { Grow(1); } +}; +} // namespace util +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }, 60_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('looks a bare name up in a base that depends on a template parameter only when written `this->`', () => { + expect(callsFrom(cg, 'src/util/list.h', 'util::List::Push')).toContain('Shrink -> util::ListBase::Shrink'); + expect(placedByLookup(cg, 'src/util/list.h', 'util::List::Push', 'Shrink')).toBe(true); + // `ListBase` is not searched for a bare `Grow(1)`: C++ finds the namespace's `util::Grow`. + expect(placedByLookup(cg, 'src/util/list.h', 'util::List::Push', 'Grow')).toBe(false); + }); + + it('looks it up in a base with no template parameter of the class in it', () => { + expect(callsFrom(cg, 'src/util/list.h', 'util::IntList::Push')).toEqual(['Grow -> util::ListBase::Grow']); + }); +}); diff --git a/src/resolution/index.ts b/src/resolution/index.ts index e04f9a274d..6da6482b63 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -291,6 +291,8 @@ export class ReferenceResolver { // 565s of combined worker time (~581µs each, recursion-multiplied). private supertypeGen = 0; private supertypeMemo = new Map(); + // getSupertypeNodes' memo, by node id, under the same generation tag. + private supertypeNodeMemo = new Map(); /** Invalidate the getSupertypes memo — call when resolved edges may have advanced. */ private advanceSupertypeGeneration(): void { @@ -298,6 +300,7 @@ export class ReferenceResolver { // Lazy invalidation via the gen tag; bound the map so a long run over many // batches doesn't accrete dead entries. if (this.supertypeMemo.size > 50_000) this.supertypeMemo.clear(); + if (this.supertypeNodeMemo.size > 50_000) this.supertypeNodeMemo.clear(); } // Node kinds are a small fixed set (~24), so this is a plain Map, not an LRU. // getNodesByKind returns the FULL node list for a kind; it was previously @@ -525,6 +528,7 @@ export class ReferenceResolver { this.methodMatchCache.clear(); this.methodOwnerIndexCache.clear(); this.supertypeMemo.clear(); + this.supertypeNodeMemo.clear(); this.supertypeGen++; this.nodesByKindCache.clear(); this.fileExistsMemo.clear(); @@ -898,6 +902,18 @@ export class ReferenceResolver { return supers; }, + getSupertypeNodes: (id: string) => { + const hit = this.supertypeNodeMemo.get(id); + if (hit && hit.gen === this.supertypeGen) return hit.nodes; + const nodes: Node[] = []; + for (const edge of this.queries.getOutgoingEdges(id, RESOLUTION_READ_EDGE_KINDS)) { + const target = this.nodeById(edge.target); + if (target) nodes.push(target); + } + this.supertypeNodeMemo.set(id, { gen: this.supertypeGen, nodes }); + return nodes; + }, + getImportMappings: (filePath: string, language) => { const cacheKey = filePath; const cached = this.importMappingCache.get(cacheKey); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 58618bb5b2..f56091d0d9 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -14,7 +14,7 @@ import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-vis import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries'; import { isDartLocallyBound } from './dart-local-scope'; import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers'; -import { cppAliasedTypeName, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments } from './cpp-type-aliases'; +import { cppAliasedTypeName, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments } from './cpp-type-aliases'; import { cppIncludedFile, cppIncluders } from './cpp-includers'; import { isTestPath } from '../search/query-utils'; import { isMinifiedContent } from '../extraction/generated-detection'; @@ -9039,6 +9039,329 @@ function isCppLibraryType(raw: string, context: ResolutionContext): boolean { return !declared.some((n) => n.qualifiedName === spelled || n.qualifiedName.endsWith(`::${spelled}`)); } +const CPP_CLASS_KINDS: ReadonlySet = new Set(['class', 'struct', 'union']); + +/** + * C++ class `cls`'s method `name`: its own, else the nearest one of a class + * it derives from, through the base edges every declaration of `cls` has. + */ +function cppMethodOf(cls: Node, name: string, ref: UnresolvedRef, context: ResolutionContext, depth = 0, seen = new Set()): Node | null { + const want = `${cls.qualifiedName}::${name}`; + const named = context.getMethodMatches?.(cls.name, name, ref.language) ?? + context.getNodesByName(name).filter((n) => n.kind === 'method' && sameLanguageFamily(n.language, ref.language)); + const own = named.filter((n) => n.qualifiedName === want); + if (own.length > 0) return preferCallSiteFile(own, ref.filePath)[0]!; + seen.add(cls.qualifiedName); + if (depth >= 4 || !context.getSupertypeNodes) return null; + for (const decl of context.getNodesByQualifiedName(cls.qualifiedName)) { + if ((decl.language !== 'cpp' && decl.language !== 'c') || !CPP_CLASS_KINDS.has(decl.kind)) continue; + for (const base of context.getSupertypeNodes(decl.id)) { + if (!CPP_CLASS_KINDS.has(base.kind) || seen.has(base.qualifiedName)) continue; + const inherited = cppMethodOf(base, name, ref, context, depth + 1, seen); + if (inherited) return inherited; + } + } + return null; +} + +/** + * A C++ call written with no receiver, or on `this` (`this->Clear()`, + * `(*this).Clear()`), in a member function — or in a lambda in one, whose + * calls the extractor gives to the function — calls what C++ name lookup + * finds there: a member of the function's class, else of a class it derives + * from, else of a class it is nested in, before anything at namespace scope. + * By its name alone, protobuf's generated `Api::operator=` calling + * `InternalSwap(&from)` reached `Any::InternalSwap`, and `Any::InternalSwap` + * calling the `GetArena()` it inherits from `MessageLite` reached + * `Arena::InternalHelper::GetArena`. + * - Bases are the class's own base edges, so a namesake class elsewhere + * lends it nothing. A bare call skips a base that depends on the class's + * template parameters, which C++ doesn't look in (`this->` is how such a + * member is called). + * - `this` is an object of the innermost class: never an enclosing class's. + * - A parameter or local the function declares before a bare call is what + * it calls: `void Set(Callback callback) { callback(); }`. + * Undefined when the call is not of that shape or no class on the way has a + * method of that name in the index — a free function, `static` or not, has + * no class; a base outside the project lends no node: the name strategies + * decide, as before. + */ +function matchCppImplicitThisCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | undefined { + const name = ref.referenceName; + if (ref.referenceKind !== 'calls' || !/^[A-Za-z_]\w*$/.test(name)) return undefined; + const receiver = cppImplicitReceiver(ref, context); + if (receiver === null) return undefined; + const caller = context.getNodeById?.(ref.fromNodeId); + const owner = caller ? cppMemberOwner(caller, context) : null; + if (!caller || owner === null) return undefined; + let found: Node | null = null; + for (const scope of cppEnclosingClasses(owner, caller, context)) { + found = cppScopeMethod(scope, name, receiver, ref, context); + if (found || receiver === 'this') break; + } + // (A recursive call names the function itself, which no local shadows.) + if (!found || (receiver === 'bare' && name !== caller.name && isCppLocalName(name, caller, ref, context))) return undefined; + return { original: ref, targetNodeId: found.id, confidence: 0.9, resolvedBy: 'instance-method' }; +} + +/** `this->` / `(*this).` before a member's name, with a `template` keyword after it. */ +const CPP_THIS_RECEIVER = /^(?:this\s*->|\(\s*\*\s*this\s*\)\s*\.)\s*(?:template\s+)?/; + +/** + * How a C++ call the extractor recorded by its bare name is written, read at + * its column: `'bare'` with no receiver, `'this'` on `this->` / `(*this).`, + * null for anything else. The extractor also drops receivers it can't spell + * (`arr_[0].Foo()`, `(p_)->Foo()`, `this->p_->Foo()`), whose calls are no + * member of the caller's class; and a name its argument list doesn't follow + * is no call of it. + */ +function cppImplicitReceiver(ref: UnresolvedRef, context: ResolutionContext): 'bare' | 'this' | null { + const line = (context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/))?.[ref.line - 1]; + if (line === undefined) return null; + const at = (column: number): 'bare' | 'this' | null => { + const text = line.slice(column); + const self = CPP_THIS_RECEIVER.exec(text)?.[0] ?? ''; + const before = line.slice(0, column).replace(/\s+$/, ''); + // A column past the receiver: on `this`, or some other object's call. + const onThis = self !== '' || /(?:^|[^\w$])(?:this\s*->|\(\s*\*\s*this\s*\)\s*\.)$/.test(before); + if (!onThis && /(?:\.|->|::)$/.test(before)) return null; + const rest = text.slice(self.length); + if (!rest.startsWith(ref.referenceName) || /[\w$]/.test(rest.charAt(ref.referenceName.length))) return null; + return cppArgumentListFollows(rest, ref.referenceName.length) ? (onThis ? 'this' : 'bare') : null; + }; + const verdict = at(Math.max(0, ref.column)); + if (verdict !== null || !/[^\x00-\x7f]/.test(line)) return verdict; + // The column may count UTF-8 bytes, which wider characters before the call outnumber. + let bytes = 0; + let index = 0; + while (index < line.length && bytes < ref.column) { + const point = line.codePointAt(index)!; + bytes += point < 0x80 ? 1 : point < 0x800 ? 2 : point < 0x10000 ? 3 : 4; + index += point > 0xffff ? 2 : 1; + } + return bytes === ref.column && index !== ref.column ? at(index) : null; +} + +/** Whether `text` from `from` on is an argument list, after any template arguments (`(`). */ +function cppArgumentListFollows(text: string, from: number): boolean { + let i = from; + while (i < text.length && /\s/.test(text[i]!)) i++; + if (text[i] === '<') { + let depth = 0; + for (; i < text.length; i++) { + const c = text[i]!; + if (c === '<') depth++; + else if (c === '>' && --depth === 0) break; + else if (c === ';' || c === '{' || c === '}') return false; + } + if (depth !== 0) return false; + i++; + while (i < text.length && /\s/.test(text[i]!)) i++; + } + return text[i] === '('; +} + +/** The C and C++ class, struct and union declarations of a qualified name. */ +function cppClassDecls(qualifiedName: string, context: ResolutionContext): Node[] { + return context.getNodesByQualifiedName(qualifiedName).filter((n) => + CPP_CLASS_KINDS.has(n.kind) && (n.language === 'cpp' || n.language === 'c')); +} + +/** + * The qualified name of the class whose scope a C++ caller's body is in: a + * member function's, defined in the class or out of line (`void + * Api::InternalSwap(…) {…}`), static or not — or a class's own, for code in + * its body. A function node counts only when its definition is written + * `Class::name(…)`. Null for a free function, and for one defined through a + * namespace (`void detail::helper() {}` is a method node of no class). + */ +function cppMemberOwner(caller: Node, context: ResolutionContext): string | null { + if (CPP_CLASS_KINDS.has(caller.kind)) return caller.qualifiedName; + if (caller.kind !== 'method' && caller.kind !== 'function') return null; + const cut = caller.qualifiedName.lastIndexOf('::'); + if (cut <= 0) return null; + const owner = caller.qualifiedName.slice(0, cut); + if (caller.kind === 'function') { + const lines = context.getFileLines?.(caller.filePath) ?? context.readFile(caller.filePath)?.split(/\r?\n/); + const head = lines?.slice(caller.startLine - 1, caller.startLine + 2).join(' ') ?? ''; + const cls = cppLastSegment(owner).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const fn = caller.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + if (!new RegExp(`\\b${cls}\\s*(?:<[^<>;{}]*>\\s*)?::\\s*~?${fn}\\s*\\(`).test(head)) return null; + } + if (cppClassDecls(owner, context).length > 0) return owner; + // A class declared in another and defined elsewhere, which the index has + // no node for: protobuf's `Any::_Internal`, declared in `Any`. + const outer = owner.lastIndexOf('::'); + return outer > 0 && cppClassDecls(owner.slice(0, outer), context).length > 0 ? owner : null; +} + +/** One class C++ lookup passes through, with the declarations that are the class there. */ +interface CppClassScope { + qualifiedName: string; + decls: Node[]; + /** `decls[0]` holds the code the lookup starts from. */ + holds: boolean; +} + +/** + * The classes a C++ caller's name lookup passes through, innermost first: + * the caller's class, then each class it is nested in. After a class + * declared in a function body comes that function's class. Several classes + * can share a qualified name — another translation unit's local fixture or + * helper — so each scope keeps only the declaration holding the code, else + * those in its file, else those its file includes, else all of them. + */ +function cppEnclosingClasses(owner: string, caller: Node, context: ResolutionContext): CppClassScope[] { + const scopes: CppClassScope[] = []; + let qualifiedName = owner; + let inner = caller; + for (let hops = 0; hops < 8 && qualifiedName !== ''; hops++) { + const decls = cppClassDecls(qualifiedName, context); + if (decls.length === 0 && hops > 0) { + // A namespace: lookup leaves the classes. Unless it is the function a + // local class is declared in (`Clear::Local`), whose class comes next. + const fn = (context.getNodesInFileNamed?.(inner.filePath, cppLastSegment(qualifiedName)) ?? []) + .find((n) => (n.kind === 'method' || n.kind === 'function') && rangeWithin(inner, n) && !sameRange(inner, n)); + const fnOwner = fn ? cppMemberOwner(fn, context) : null; + if (!fn || fnOwner === null) break; + qualifiedName = fnOwner; + inner = fn; + continue; + } + const scope = cppScopeDeclarations(qualifiedName, decls, inner, context); + scopes.push(scope); + if (scope.decls.length > 0) inner = scope.decls[0]!; + const cut = qualifiedName.lastIndexOf('::'); + qualifiedName = cut > 0 ? qualifiedName.slice(0, cut) : ''; + } + return scopes; +} + +/** A class scope's declarations as cppEnclosingClasses keeps them. */ +function cppScopeDeclarations(qualifiedName: string, decls: Node[], inner: Node, context: ResolutionContext): CppClassScope { + const holding = decls.filter((d) => d.filePath === inner.filePath && rangeWithin(inner, d) && d.id !== inner.id); + if (holding.length > 0) return { qualifiedName, decls: holding, holds: true }; + const local = decls.filter((d) => d.filePath === inner.filePath); + if (local.length > 0 || decls.length < 2) return { qualifiedName, decls: local.length > 0 ? local : decls, holds: false }; + const included = decls.filter((d) => cppIncluders(d.filePath, context).has(inner.filePath)); + return { qualifiedName, decls: included.length > 0 ? included : decls, holds: false }; +} + +/** + * The method `name` a class scope gives C++ lookup: the class's own — first + * one written in the very declaration holding the code, as an inline member + * — else the nearest one of a class it derives from, through the kept + * declarations' base edges. + */ +function cppScopeMethod(scope: CppClassScope, name: string, receiver: 'bare' | 'this', ref: UnresolvedRef, context: ResolutionContext): Node | null { + const own = context.getNodesByQualifiedName(`${scope.qualifiedName}::${name}`) + .filter((n) => n.kind === 'method' && (n.language === 'cpp' || n.language === 'c')); + const holder = scope.holds ? scope.decls[0]! : null; + const inline = holder ? own.filter((n) => n.filePath === holder.filePath && rangeWithin(n, holder)) : []; + if (inline.length > 0) return cppOverloadFor(inline, name, ref, context); + // A class with no name (a partial specialization, an unnamed struct) + // shares its qualified name with every other: only its inline members are its own. + if (own.length > 0 && cppLastSegment(scope.qualifiedName) !== '') { + return cppOverloadFor(preferCallSiteFile(own, ref.filePath), name, ref, context); + } + if (!context.getSupertypeNodes) return null; + for (const decl of scope.decls) { + for (const base of context.getSupertypeNodes(decl.id)) { + if (!CPP_CLASS_KINDS.has(base.kind)) continue; + if (receiver === 'bare' && isCppDependentBase(decl, base, context)) continue; + const inherited = cppMethodOf(base, name, ref, context, 1, new Set([scope.qualifiedName])); + if (inherited) return inherited; + } + } + return null; +} + +/** Of one class's overloads of `name` (in preference order), the one the call's arguments fit best. */ +function cppOverloadFor(overloads: Node[], name: string, ref: UnresolvedRef, context: ResolutionContext): Node { + if (overloads.length === 1) return overloads[0]!; + const args = cppCallArguments(ref, name, context); + if (!args) return overloads[0]!; + let best = overloads[0]!; + let bestFit = cppOverloadFit(best, name, args, context); + for (const n of overloads.slice(1)) { + const fit = cppOverloadFit(n, name, args, context); + if (fit > bestFit) { + best = n; + bestFit = fit; + } + } + return best; +} + +const CPP_BASE_SPECS = new WeakMap>(); + +/** + * The base specifiers a C or C++ class declaration writes (`public + * Base`, `private Mixin`), read from its own head; none for a forward + * declaration. + */ +function cppBaseSpecifiers(decl: Node, context: ResolutionContext): string[] { + let memo = CPP_BASE_SPECS.get(context); + if (!memo) CPP_BASE_SPECS.set(context, (memo = new Map())); + const hit = memo.get(decl.id); + if (hit) return hit; + const specs: string[] = []; + const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/); + const head = lines?.slice(decl.startLine - 1, decl.startLine + 11) ?? []; + if (head.length > 0) { + head[0] = head[0]!.slice(decl.startColumn); + const text = head.map((l) => cppCodeOf(l) ?? '').join('\n'); + const brace = text.indexOf('{'); + const colon = brace < 0 ? -1 : text.slice(0, brace).search(/(?= 0 && !text.slice(0, brace).includes(';')) { + specs.push(...splitCppTopLevel(text.slice(colon + 1, brace)).map((s) => s.replace(/\b(?:public|protected|private|virtual)\b/g, ' ').trim())); + } + } + memo.set(decl.id, specs); + return specs; +} + +/** + * Whether `base`, a base of the C++ class declaration `decl`, depends on the + * template parameters of `decl` or a class around it: `template + * class Foo : public Base`. A bare name is not looked up in such a base. + */ +function isCppDependentBase(decl: Node, base: Node, context: ResolutionContext): boolean { + const parameters = cppTemplateParameters(decl, context); + if (parameters.size === 0) return false; + const spec = cppBaseSpecifiers(decl, context).find((s) => cppLastSegment(stripCppTemplateArguments(s).trim()) === base.name); + return spec !== undefined && [...parameters].some((p) => new RegExp(`(? done;` reads as a declaration once its template arguments are gone. + for (const variant of [code, stripCppTemplateArguments(code)]) { + const type = declarator.exec(variant)?.[1]; + // `Owner::name(` is a definition's own declarator; `return name(` no declaration. + if (type !== undefined && !/::\s*$/.test(type) && normalizeCppTypeName(type) !== null) return true; + } + } + return false; +} + /** * A C++ source line as code, for reading declarations: null for a line of a * comment (`// …`, ` * …`), else the line with its comments blanked, columns @@ -9672,6 +9995,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { CPP_NS_FRAMES.delete(context); CPP_NS_ALIASES.delete(context); CPP_CLASSES_IN_FILE.delete(context); + CPP_BASE_SPECS.delete(context); SOLIDITY_SUPERS.delete(context); DECLARED_SUPERS.delete(context); INHERITED_METHODS.delete(context); @@ -10783,6 +11107,10 @@ export function matchMethodCall( const match = dotMatch || colonMatch || luaColonMatch || rDollarMatch; if (!match) { + // A C++ call written with no receiver, or on `this`, in a member function + // is a call on the function's object: a member of its class, of a class + // it derives from or of one it is nested in, when one has that name. + if (ref.language === 'cpp') return nmTimedT('mc-cppthis', ref, () => matchCppImplicitThisCall(ref, context)) ?? null; return null; } diff --git a/src/resolution/types.ts b/src/resolution/types.ts index 52d62be2ac..05f3c45e65 100644 --- a/src/resolution/types.ts +++ b/src/resolution/types.ts @@ -156,6 +156,14 @@ export interface ResolutionContext { * method). Optional so external/test contexts compile without it. */ getSupertypes?(typeName: string, language: Language): string[]; + /** + * The nodes one type declaration extends or implements, through its own + * resolved `implements`/`extends` edges: where getSupertypes unions those of + * every type sharing a simple name, this is the supertypes of the node + * `id`. Empty during the first resolution pass, as getSupertypes is. + * Optional so external/test contexts compile without it. + */ + getSupertypeNodes?(id: string): Node[]; /** * Look up a node by its id. Lets matchers derive the FROM-symbol's * enclosing-class scope (Swift implicit-self method scoping, `this.X` From 7abdd1b897ccc46b2cc4a5b559c7f9b4981df37c Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sat, 10 Oct 2026 03:21:57 -0500 Subject: [PATCH 2/2] fix(cpp): a call a method makes with no receiver links to its class's own or inherited method A C++ call written with no receiver, or through `this->`, inside a method was matched by its name alone, so another class's method of that name won by file proximity: protobuf's generated `Api::operator=` calling `InternalSwap(&from)` reached `Any::InternalSwap`, `Any::InternalSwap`'s inherited `GetArena()` reached `Arena::InternalHelper::GetArena`, and googletest's `~linked_ptr() { depart(); }` reached `linked_ptr_internal::depart`. matchMethodCall now resolves C++'s implicit `this` the way it resolves a typed receiver: the calling method's class, then the classes it derives from (its own base edges, through #2440's cppMethodOf), then the classes it is nested in, with the overload the arguments fit. The call's shape is read at its column (the extractor drops `this->` and some receivers it can't spell). A free function, a parameter or local of the name, a dependent base reached by a bare call, a namesake class in another translation unit and a member a macro may declare all leave the call to the name strategies. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-implicit-this-calls.test.ts | 143 ++++++++++++++++++++++ src/resolution/name-matcher.ts | 78 +++++++++--- 3 files changed, 207 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f615431b3d..82fdfc544d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a class written with attribute macros the parser can't read now keeps its members. That covers several macros between `class` and the class name, like protocolbuffers/protobuf's generated `class PROTOBUF_EXPORT PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED Any final`, a macro with arguments there, like RocksDB's `struct ALIGN_AS(64U) HandleImpl`, or one before a template specialization, and macros on a member: in front of it (`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD`), between a pointer and its name (`PROTOBUF_NONNULL`), after its parameters or its name (`ABSL_ATTRIBUTE_LIFETIME_BOUND`, google/leveldb's `LOCKS_EXCLUDED(mu_)` and `GUARDED_BY(mu_)`), or alone on a line above a comment ({fmt}'s `FMT_BEGIN_EXPORT`). Before, protobuf's generated message classes were missing and their members, like `GetDescriptor`, `GetReflection` and `unknown_fields`, were indexed as functions outside any class, and leveldb's annotated methods were indexed under the annotation's name, so calls to them linked to nothing. Re-index C++ projects after upgrading. - In C++, a call on a range-based `for` loop's variable, like `suite->SetVerbose(verbose)` inside `for (ConformanceTestSuite *suite : suites)`, now links to the method of the type the loop declares. Before, the loop's declaration was never read, so the call was matched by the variable's name alone: in RocksDB, `log->file()` on a `log::Writer` went to `BlobLogWriter::file`, and a loop variable named like a member of its class was taken for that member. The variable counts only inside its loop, and loops over `auto` elements resolve as before. Re-index C++ projects after upgrading. - In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr>`, the call no longer links to a project method. Re-index C++ projects after upgrading. +- In C++, a call a method makes with no receiver, or through `this->`, now links to the method C++ itself would call: one of the method's own class, else one the class inherits from a base class, else one of a class it is nested in. The same goes for a call in a lambda inside the method, and of a class's overloads the one the call's arguments fit is taken. Before, the call was matched by its name alone, so another class's method of that name could win because it was declared nearer or found first: in protocolbuffers/protobuf, every generated message's `Swap` and move assignment calling `InternalSwap(…)` reached `Any::InternalSwap`, and the `GetArena()` messages inherit from `MessageLite` reached an arena helper's `GetArena`, while googletest's `~linked_ptr() { depart(); }`, vendored in facebook/rocksdb, reached `linked_ptr_internal::depart`. Calls C++ doesn't look up in the class are matched as before: a free function's, a call to a parameter or local of that name, and a template's bare call into a base class that depends on its template parameters. Re-index C++ projects after upgrading. Thanks @danusha2345 for reporting the C++ case in #1952. - Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading. - In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged. - In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading. diff --git a/__tests__/cpp-implicit-this-calls.test.ts b/__tests__/cpp-implicit-this-calls.test.ts index 36e65ce91a..7d7edc9230 100644 --- a/__tests__/cpp-implicit-this-calls.test.ts +++ b/__tests__/cpp-implicit-this-calls.test.ts @@ -327,6 +327,149 @@ void Table::Compact() { }); }); +describe('C++ overloads a call’s arguments pick', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-cpp-this-overloads-', { + 'include/opts/configurable.h': `#pragma once +#include +namespace opts { +class Configurable { + public: + template + void RegisterOptions(T* opt_ptr, const int* opt_map) { + RegisterOptions(T::kClassName(), opt_ptr, opt_map); + } + void RegisterOptions(const std::string& name, void* opt_ptr, const int* opt_map); + + void Add(int key) { Add(key, 1); } + void Add(int key, int count); +}; +} // namespace opts +`, + 'src/opts/configurable.cc': `#include "opts/configurable.h" +namespace opts { +void Configurable::RegisterOptions(const std::string& name, void* opt_ptr, const int* opt_map) {} +void Configurable::Add(int key, int count) {} +} // namespace opts +`, + 'test/simple.cc': `#include "opts/configurable.h" +namespace opts { +class SimpleConfigurable : public Configurable { + public: + SimpleConfigurable() { + RegisterOptions(std::string("Simple") + "Unique", &unique_, &info_); + } + + private: + int unique_ = 0; + int info_ = 0; +}; +} // namespace opts +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }, 60_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('takes the inherited overload the arguments fit', () => { + // Both overloads are `opts::Configurable::RegisterOptions`: the three-parameter one is defined in the .cc. + const threeParameters = cg.getNodesInFile('src/opts/configurable.cc').find((n) => n.name === 'RegisterOptions'); + const ctor = cg.getNodesInFile('test/simple.cc').find((n) => n.qualifiedName === 'opts::SimpleConfigurable::SimpleConfigurable'); + const edge = cg.getOutgoingEdges(ctor!.id).find((e) => (e.metadata as { refName?: string } | undefined)?.refName === 'RegisterOptions'); + expect(edge?.target).toBe(threeParameters?.id); + }); + + it('weighs a class’s out-of-line overloads with its inline ones', () => { + const add = cg.getNodesInFile('include/opts/configurable.h').find((n) => n.name === 'Add'); + const outOfLine = cg.getNodesInFile('src/opts/configurable.cc').find((n) => n.name === 'Add'); + const edge = cg.getOutgoingEdges(add!.id).find((e) => (e.metadata as { refName?: string } | undefined)?.refName === 'Add'); + expect(edge?.target).toBe(outOfLine?.id); + }); +}); + +describe('C++ classes the index can only see part of', () => { + let root = ''; + let cg: CodeGraph; + + beforeAll(async () => { + root = writeProject('cg-cpp-this-partial-', { + // Two translation units each declare a `FileState` of their own. + 'helpers/memenv.cc': `namespace kv { +namespace { +class FileState { + public: + void Truncate() {} + ~FileState() { Truncate(); } +}; +} // namespace +} // namespace kv +`, + 'db/fault_injection_test.cc': `namespace kv { +namespace { +int Truncate(const char* name, long length) { return 0; } + +struct FileState { + const char* filename_; + long pos_; + int DropUnsyncedData() const; +}; +} // namespace + +int FileState::DropUnsyncedData() const { + return Truncate(filename_, pos_); +} +} // namespace kv +`, + 'src/reflection.cc': `#define LOCAL_VAR_ACCESSOR(type, name) \\ + type Get##name() const { return value; } + +namespace pb { +class Message {}; + +class Reflection { + public: + Message* GetMessage() const { return nullptr; } + void SwapField() const; +}; + +void Reflection::SwapField() const { + struct LocalVarWrapper { + LOCAL_VAR_ACCESSOR(Message*, Message); + Message* UnsafeGetMessage() const { return GetMessage(); } + Message* value; + }; +} +} // namespace pb +`, + }); + cg = await CodeGraph.init(root, { index: true }); + }, 60_000); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + it('never lends one translation unit’s class members to another’s class of the same name', () => { + expect(callsFrom(cg, 'helpers/memenv.cc', 'kv::FileState::~FileState')).toEqual(['Truncate -> kv::FileState::Truncate']); + expect(placedByLookup(cg, 'db/fault_injection_test.cc', 'kv::FileState::DropUnsyncedData', 'Truncate')).toBe(false); + expect(callsFrom(cg, 'db/fault_injection_test.cc', 'kv::FileState::DropUnsyncedData')).not.toContain('Truncate -> kv::FileState::Truncate'); + }); + + it('stops at a class whose macro may declare the name, rather than take an outer class’s', () => { + const wrapper = cg.getNodesInFile('src/reflection.cc').find((n) => n.name === 'UnsafeGetMessage'); + expect(wrapper).toBeDefined(); + expect(placedByLookup(cg, 'src/reflection.cc', wrapper!.qualifiedName, 'GetMessage')).toBe(false); + }); +}); + describe('C++ name lookup in a class template', () => { let root = ''; let cg: CodeGraph; diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index e52af154aa..079139e45f 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9235,6 +9235,8 @@ function cppMethodOf(cls: Node, name: string, ref: UnresolvedRef, context: Resol * - `this` is an object of the innermost class: never an enclosing class's. * - A parameter or local the function declares before a bare call is what * it calls: `void Set(Callback callback) { callback(); }`. + * - A class that may declare the name through a macro (`Get##name`), which + * no node shows, ends the walk. * Undefined when the call is not of that shape or no class on the way has a * method of that name in the index — a free function, `static` or not, has * no class; a base outside the project lends no node: the name strategies @@ -9248,10 +9250,10 @@ function matchCppImplicitThisCall(ref: UnresolvedRef, context: ResolutionContext const caller = context.getNodeById?.(ref.fromNodeId); const owner = caller ? cppMemberOwner(caller, context) : null; if (!caller || owner === null) return undefined; - let found: Node | null = null; + let found: Node | null | undefined = null; for (const scope of cppEnclosingClasses(owner, caller, context)) { found = cppScopeMethod(scope, name, receiver, ref, context); - if (found || receiver === 'this') break; + if (found !== null || receiver === 'this') break; } // (A recursive call names the function itself, which no local shadows.) if (!found || (receiver === 'bare' && name !== caller.name && isCppLocalName(name, caller, ref, context))) return undefined; @@ -9355,6 +9357,8 @@ interface CppClassScope { decls: Node[]; /** `decls[0]` holds the code the lookup starts from. */ holds: boolean; + /** Other classes of that qualified name were left out: another translation unit's. */ + narrowed: boolean; } /** @@ -9393,38 +9397,52 @@ function cppEnclosingClasses(owner: string, caller: Node, context: ResolutionCon /** A class scope's declarations as cppEnclosingClasses keeps them. */ function cppScopeDeclarations(qualifiedName: string, decls: Node[], inner: Node, context: ResolutionContext): CppClassScope { - const holding = decls.filter((d) => d.filePath === inner.filePath && rangeWithin(inner, d) && d.id !== inner.id); - if (holding.length > 0) return { qualifiedName, decls: holding, holds: true }; + const scope = (kept: Node[], holds: boolean): CppClassScope => ({ qualifiedName, decls: kept, holds, narrowed: kept.length < decls.length }); + // (Code in a class's own body, a member's initializer, has that class as `inner`.) + const holding = decls.filter((d) => d.filePath === inner.filePath && rangeWithin(inner, d)); + if (holding.length > 0) return scope(holding, true); const local = decls.filter((d) => d.filePath === inner.filePath); - if (local.length > 0 || decls.length < 2) return { qualifiedName, decls: local.length > 0 ? local : decls, holds: false }; + if (local.length > 0 || decls.length < 2) return scope(local.length > 0 ? local : decls, false); const included = decls.filter((d) => cppIncluders(d.filePath, context).has(inner.filePath)); - return { qualifiedName, decls: included.length > 0 ? included : decls, holds: false }; + return scope(included.length > 0 ? included : decls, false); } /** * The method `name` a class scope gives C++ lookup: the class's own — first * one written in the very declaration holding the code, as an inline member * — else the nearest one of a class it derives from, through the kept - * declarations' base edges. - */ -function cppScopeMethod(scope: CppClassScope, name: string, receiver: 'bare' | 'this', ref: UnresolvedRef, context: ResolutionContext): Node | null { - const own = context.getNodesByQualifiedName(`${scope.qualifiedName}::${name}`) - .filter((n) => n.kind === 'method' && (n.language === 'cpp' || n.language === 'c')); + * declarations' base edges. Undefined when the class may declare `name` + * through a macro, which no node shows: lookup would stop there. + */ +function cppScopeMethod(scope: CppClassScope, name: string, receiver: 'bare' | 'this', ref: UnresolvedRef, context: ResolutionContext): Node | null | undefined { + const own = context.getNodesByQualifiedName(`${scope.qualifiedName}::${name}`).filter((n) => + n.kind === 'method' && (n.language === 'cpp' || n.language === 'c') && + // A namesake class's members are defined where it is: leveldb's + // fault-injection test declares a `FileState` of its own, with no + // `Truncate`, beside memenv.cc's. + (!scope.narrowed || scope.decls.some((d) => d.filePath === n.filePath || cppIncluders(d.filePath, context).has(n.filePath)))); const holder = scope.holds ? scope.decls[0]! : null; const inline = holder ? own.filter((n) => n.filePath === holder.filePath && rangeWithin(n, holder)) : []; - if (inline.length > 0) return cppOverloadFor(inline, name, ref, context); // A class with no name (a partial specialization, an unnamed struct) // shares its qualified name with every other: only its inline members are its own. - if (own.length > 0 && cppLastSegment(scope.qualifiedName) !== '') { - return cppOverloadFor(preferCallSiteFile(own, ref.filePath), name, ref, context); + const outOfLine = cppLastSegment(scope.qualifiedName) === '' ? [] : own.filter((n) => !inline.includes(n)); + if (inline.length + outOfLine.length > 0) { + return cppOverloadFor([...inline, ...preferCallSiteFile(outOfLine, ref.filePath)], name, ref, context); } + if (scope.decls.some((d) => cppMemberMacroArguments(d, context).some((a) => name.startsWith(a) || name.endsWith(a)))) return undefined; if (!context.getSupertypeNodes) return null; for (const decl of scope.decls) { for (const base of context.getSupertypeNodes(decl.id)) { if (!CPP_CLASS_KINDS.has(base.kind)) continue; if (receiver === 'bare' && isCppDependentBase(decl, base, context)) continue; const inherited = cppMethodOf(base, name, ref, context, 1, new Set([scope.qualifiedName])); - if (inherited) return inherited; + if (!inherited) continue; + // Its class's other overloads, which the arguments may fit better + // (rocksdb's `RegisterOptions(name, &opts, &info)` is not the + // two-parameter template beside it). + const siblings = context.getNodesByQualifiedName(inherited.qualifiedName).filter((n) => + n.id !== inherited.id && n.kind === 'method' && (n.language === 'cpp' || n.language === 'c')); + return cppOverloadFor([inherited, ...preferCallSiteFile(siblings, ref.filePath)], name, ref, context); } } return null; @@ -9447,6 +9465,35 @@ function cppOverloadFor(overloads: Node[], name: string, ref: UnresolvedRef, con return best; } +const CPP_MEMBER_MACRO_ARGS = new WeakMap>(); + +/** + * The words in the arguments of the macros a C++ class declaration calls + * among its members, like `Message` in protobuf's + * `LOCAL_VAR_ACCESSOR(Message*, Message);`: such a macro can declare a member + * the index has no node for (`Get##name` makes `GetMessage`). + */ +function cppMemberMacroArguments(decl: Node, context: ResolutionContext): string[] { + let memo = CPP_MEMBER_MACRO_ARGS.get(context); + if (!memo) CPP_MEMBER_MACRO_ARGS.set(context, (memo = new Map())); + const hit = memo.get(decl.id); + if (hit) return hit; + const words: string[] = []; + const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? []; + let depth = 0; + for (let i = decl.startLine - 1; i < Math.min(lines.length, decl.endLine); i++) { + const code = cppCodeOf(i === decl.startLine - 1 ? lines[i]!.slice(decl.startColumn) : lines[i]!) ?? ''; + const call = depth === 1 ? /^\s*[A-Z][A-Z0-9_]*\s*\(([^;{}]*)\)\s*;?\s*$/.exec(code) : null; + if (call) words.push(...call[1]!.split(/\W+/).filter((w) => w.length >= 3)); + for (const c of code) { + if (c === '{') depth++; + else if (c === '}') depth--; + } + } + memo.set(decl.id, words); + return words; +} + const CPP_BASE_SPECS = new WeakMap>(); /** @@ -10160,6 +10207,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { clearCppNamespaceMemos(context); CPP_CLASSES_IN_FILE.delete(context); CPP_BASE_SPECS.delete(context); + CPP_MEMBER_MACRO_ARGS.delete(context); SOLIDITY_SUPERS.delete(context); DECLARED_SUPERS.delete(context); INHERITED_METHODS.delete(context);