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 new file mode 100644 index 0000000000..7d7edc9230 --- /dev/null +++ b/__tests__/cpp-implicit-this-calls.test.ts @@ -0,0 +1,524 @@ +/** + * 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++ 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; + + 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/name-matcher.ts b/src/resolution/name-matcher.ts index 2cfcd124c7..079139e45f 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9218,6 +9218,351 @@ function cppMethodOf(cls: Node, name: string, ref: UnresolvedRef, context: Resol 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(); }`. + * - 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 + * 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 | undefined = null; + for (const scope of cppEnclosingClasses(owner, caller, context)) { + found = cppScopeMethod(scope, name, receiver, ref, context); + 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; + 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; + /** Other classes of that qualified name were left out: another translation unit's. */ + narrowed: 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 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 scope(local.length > 0 ? local : decls, false); + const included = decls.filter((d) => cppIncluders(d.filePath, context).has(inner.filePath)); + 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. 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)) : []; + // 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. + 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) 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; +} + +/** 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_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>(); + +/** + * 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 @@ -9861,6 +10206,8 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { KOTLIN_FRAMES.delete(context); 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); @@ -10989,6 +11336,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; }