From 4040eab975cff282df484d59bde9f8f131c4211b Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Wed, 7 Oct 2026 03:01:39 +0000 Subject: [PATCH] fix(cpp): a base class links to the class C++ name lookup finds, not a namesake (#2407) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A C++ base class (`class DynamicMessage final : public Message`) was resolved by its name alone, so a base named like a class of another namespace, or of another language, bound to whichever namesake ranked first. In protocolbuffers/protobuf, `DynamicMessage`, `MapEntry`, `ZeroFieldsBase` and two arena_unittest classes derived from `json_internal::ResolverPool::Message`; before #2397 they derived from the PHP extension's C struct `Message`. The Go framework resolver claimed those: protobuf ships Go examples, so the resolver is detected, its `resolve()` runs for every language's refs (`languages` only gates extraction), its PascalCase-struct rule found the C struct, and the framework language gate lets C++ reach C. google/leveldb's and rocksdb's iterators derived from an unrelated nested `Iterator` (`SkipList::Iterator`, `WriteThread::WriteGroup::Iterator`), and leveldb's test comparators from a `Comparator` struct that skiplist_test.cc declares for itself. C and C++ inheritance refs now resolve in their own matcher (cpp-supertypes.ts), ahead of every other strategy and with no fallthrough, so no framework resolver, import strategy or name matcher sees them, and only C and C++ declarations are candidates. The base is looked up the way C++ looks it up: - from the scope the deriving class is declared in, outwards, using the lookup cpp-type-aliases.ts (#2399) already has: a class scope with its bases, a qualified name a segment at a time, an alias followed to the class it names; `::Base` from the global scope only; - then through what the file writes before the class: namespace aliases (`namespace _pbi = ::google::protobuf::internal;`), using-declarations and using-directives, and fmt's macro-opened namespaces; - a class a .cc/.c file defines belongs to that translation unit; - a template parameter, or a member of one, names no class: no edge. When none of that finds the class, the only visible C/C++ class with that qualified tail is taken, since the index loses some classes' namespaces (`class PROTOBUF_EXPORT FieldDescriptor`, classes after a partial specialization); several are a guess, so no edge. The base is read as written, template arguments included, so an alias template (`bool_constant<…>`) is found. An alias that leads to no project class (one of `std::integral_constant`, one each `#if` arm declares differently) is linked itself, as before. Validated with site-keyed edge diffs on leveldb, fmt, protobuf and rocksdb, and redis, SDWebImage and gson as controls (dumps byte-identical): first against main 02d22aeb, then again on main dd706469 after #2405, #2408 and #2400 landed, with identical site diffs (#2402, merged after, changes only Go's implements synthesis). Every removed edge was wrong: 9 protobuf `suite_.ReportFailure` guesses that reached `ForkPipeRunner`, cpp-override edges from the wrong bases, and two rocksdb bases that are template parameters. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-base-class-lookup.test.ts | 513 ++++++++++++++++++++++++ src/resolution/cpp-supertypes.ts | 287 +++++++++++++ src/resolution/cpp-type-aliases.ts | 130 +++++- src/resolution/index.ts | 7 + src/resolution/name-matcher.ts | 4 +- 6 files changed, 924 insertions(+), 18 deletions(-) create mode 100644 __tests__/cpp-base-class-lookup.test.ts create mode 100644 src/resolution/cpp-supertypes.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index de748bde7..61c468bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In a repository that holds more than one Shopify theme, a theme's `{% render %}`, `{% include %}` and `{% section %}` tags and the sections its JSON templates list now link only to that theme's own snippets and sections, which is where Shopify looks for them. Before, a section or snippet the theme doesn't have was linked to another theme's file of the same name, and a theme kept in a subfolder could link to the root theme's copy instead of its own. A reference the theme can't satisfy now links to nothing. Projects with a single theme are indexed as before. Re-index projects with several Shopify themes after upgrading. - In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading. - In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. Re-index C++ projects after upgrading. +- In C++, a base class like `Message` in `class DynamicMessage final : public Message` now links to the class C++ itself would pick: the one declared in the namespaces around the class, or brought in by the file's `using` declarations and namespace aliases, and the class a `typedef` or `using` alias names. Before, a base class was matched by its name alone, so it could land on a class of the same name in another namespace or another language: in protocolbuffers/protobuf, `DynamicMessage` derived from the JSON parser's internal `Message` class, and in google/leveldb and RocksDB, iterators derived from an unrelated nested `Iterator` class such as the skip list's. The wrong base showed up in `codegraph_explore`'s type hierarchy and in impact, and tied the base's methods to the wrong overrides. A template parameter used as a base no longer links to a class that shares its name, and a base written from the global scope, like `public ::testing::Test`, now links to its class. Re-index C++ projects after upgrading. - 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 Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. - In Go, working out which structs implement an interface now counts the methods of the interfaces it embeds, and the methods a struct gets from the types it embeds. Before, only each type's own methods counted: etcd's `AuthReadTx`, which embeds `UnsafeAuthReader` and adds `RLock` and `RUnlock`, was listed as implemented by every read-write lock in the project, an interface that only embeds others had no implementations at all, and a struct that gets its methods from an embedded base, like prometheus's service discoveries embedding `refresh.Discovery` or gin's `Engine` embedding `RouterGroup`, was missing. `codegraph_explore`, impact and the type hierarchy now list the right implementations, and a call through the interface reaches a method the embedding struct overrides, like gin's `Engine.Use`. Re-index Go projects after upgrading. diff --git a/__tests__/cpp-base-class-lookup.test.ts b/__tests__/cpp-base-class-lookup.test.ts new file mode 100644 index 000000000..5774eb6ba --- /dev/null +++ b/__tests__/cpp-base-class-lookup.test.ts @@ -0,0 +1,513 @@ +/** + * A C++ base class — `class DynamicMessage final : public Message` — is the + * class C++ name lookup finds from where the deriving class is declared. + * + * The base was matched by its name alone, so one named like a class of + * another namespace bound to whichever namesake ranked first. + * protocolbuffers/protobuf's `google::protobuf::DynamicMessage` derived from + * `json_internal::ResolverPool::Message` (and, before #2397, from the PHP + * extension's C struct `Message`); google/leveldb's iterators derived from + * `SkipList::Iterator`, and its test comparators from a struct + * `skiplist_test.cc` declares for itself. The wrong supertype then fed the + * type hierarchy, the cpp-override edges from a base method to its overrides, + * and the supertype walk that resolves a call to an inherited method. + * + * The lookup goes outwards from the scope the class is declared in: a class + * scope with its bases, a qualified name a segment at a time, an alias + * followed to the class it names, `::Base` from the global scope. Then + * through what the file writes before the class: a namespace alias, a + * using-declaration, a using-directive. A class a source file defines is that + * translation unit's own. A template parameter names no class. When none of + * that finds it, a class of that name the file can see is taken only when it + * is the only one. + */ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { buildTypeHierarchy } from '../src/graph/type-hierarchy'; +import type { Node } from '../src/types'; + +const FILES: Record = { + // protocolbuffers/protobuf: the real `Message`, a nested namesake in + // json_internal, and the PHP extension's C struct. + 'src/google/protobuf/message_lite.h': [ + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'class MessageGlobalsBase {', + ' public:', + ' int default_instance() const { return 0; }', + '};', + '} // namespace internal', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'src/google/protobuf/message.h': [ + '#include "google/protobuf/message_lite.h"', + 'namespace google {', + 'namespace protobuf {', + 'class Message {', + ' public:', + ' virtual ~Message();', + ' virtual int ByteSizeLong() const = 0;', + '};', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'src/google/protobuf/json/internal/untyped_message.h': [ + 'namespace google {', + 'namespace protobuf {', + 'namespace json_internal {', + 'class ResolverPool {', + ' public:', + ' class Message {', + ' public:', + ' int ByteSizeLong() const { return 0; }', + ' };', + '};', + '} // namespace json_internal', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'php/ext/google/protobuf/message.c': [ + 'typedef struct {', + ' int std;', + '} Message;', + '', + ].join('\n'), + 'src/google/protobuf/dynamic_message.cc': [ + '#include "google/protobuf/message.h"', + 'namespace google {', + 'namespace protobuf {', + 'class DynamicMessage final : public Message {', + ' public:', + ' int ByteSizeLong() const override { return 1; }', + '};', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + 'src/google/protobuf/generated_message_bases.h': [ + '#include "google/protobuf/message.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'class ZeroFieldsBase : public Message {', + ' public:', + ' int ByteSizeLong() const override { return 0; }', + '};', + '} // namespace internal', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + // Generated code names its bases through a namespace alias. + 'src/google/protobuf/any.pb.cc': [ + '#include "google/protobuf/message_lite.h"', + 'namespace _pbi = ::google::protobuf::internal;', + 'namespace google {', + 'namespace protobuf {', + 'struct AnyGlobalsTypeInternal : ::_pbi::MessageGlobalsBase {', + ' int x;', + '};', + '} // namespace protobuf', + '} // namespace google', + '', + ].join('\n'), + + // google/leveldb: the public iterator, the skip list's nested one, a test's + // own `Comparator`, and the C API's using-declarations. + 'include/leveldb/iterator.h': [ + 'namespace leveldb {', + 'class Iterator {', + ' public:', + ' virtual ~Iterator();', + ' virtual bool Valid() const = 0;', + ' virtual void Next() = 0;', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/skiplist.h': [ + 'namespace leveldb {', + 'template ', + 'class SkipList {', + ' public:', + ' class Iterator {', + ' public:', + ' bool Valid() const { return node_ != nullptr; }', + ' void Next() { node_ = nullptr; }', + ' void SeekToFirst() { node_ = nullptr; }', + ' private:', + ' const char* node_;', + ' };', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/db_iter.cc': [ + '#include "include/leveldb/iterator.h"', + 'namespace leveldb {', + 'class DBIter : public Iterator {', + ' public:', + ' bool Valid() const override { return valid_; }', + ' void Next() override { valid_ = false; }', + ' private:', + ' bool valid_;', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'include/leveldb/comparator.h': [ + 'namespace leveldb {', + 'class Comparator {', + ' public:', + ' virtual int Compare(const char* a, const char* b) const = 0;', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/skiplist_test.cc': [ + 'namespace leveldb {', + 'struct Comparator {', + ' int operator()(const int& a, const int& b) const { return a - b; }', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/db_test.cc': [ + '#include "include/leveldb/comparator.h"', + 'namespace leveldb {', + 'class NumberComparator : public Comparator {', + ' public:', + ' int Compare(const char* a, const char* b) const override { return 0; }', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'include/leveldb/filter_policy.h': [ + 'namespace leveldb {', + 'class FilterPolicy {', + ' public:', + ' virtual const char* Name() const = 0;', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'db/c.cc': [ + '#include "include/leveldb/filter_policy.h"', + 'using leveldb::FilterPolicy;', + 'struct leveldb_filterpolicy_t : public FilterPolicy {', + ' const char* Name() const override { return "c"; }', + '};', + '', + ].join('\n'), + + // facebook/rocksdb: a nested class's base is a member its outer class + // inherits; a base named through an alias; a using-directive. + 'cache/clock_cache.h': [ + 'namespace rocksdb {', + 'namespace clock_cache {', + 'class BaseClockTable {', + ' public:', + ' struct BaseOpts {', + ' int eviction_effort_cap;', + ' };', + '};', + 'class AutoHyperClockTable : public BaseClockTable {', + ' public:', + ' struct Opts : public BaseOpts {', + ' int min_avg_value_size;', + ' };', + '};', + '} // namespace clock_cache', + '} // namespace rocksdb', + '', + ].join('\n'), + 'cache/lru_cache.h': [ + 'namespace rocksdb {', + 'namespace lru_cache {', + 'struct BaseOpts {', + ' int capacity;', + '};', + '} // namespace lru_cache', + '} // namespace rocksdb', + '', + ].join('\n'), + 'table/internal_iterator.h': [ + 'namespace rocksdb {', + 'class Slice {};', + 'template ', + 'class InternalIteratorBase {', + ' public:', + ' virtual void Next() = 0;', + ' void SeekToFirst() {}', + '};', + 'using InternalIterator = InternalIteratorBase;', + 'template using bool_constant = std::integral_constant;', + '} // namespace rocksdb', + '', + ].join('\n'), + 'table/merging_iterator.cc': [ + '#include "table/internal_iterator.h"', + 'namespace rocksdb {', + 'class MergingIterator : public InternalIterator {', + ' public:', + ' void Next() override {}', + '};', + 'void Rewind(MergingIterator* iter) {', + ' iter->SeekToFirst();', + '}', + 'template ', + 'struct is_fast_float : bool_constant {};', + '} // namespace rocksdb', + '', + ].join('\n'), + 'include/rocksdb/listener.h': [ + 'namespace rocksdb {', + 'class EventListener {', + ' public:', + ' virtual void OnFlushCompleted() {}', + '};', + '} // namespace rocksdb', + '', + ].join('\n'), + 'examples/compact_files_example.cc': [ + '#include "include/rocksdb/listener.h"', + 'using namespace rocksdb;', + 'class Compactor : public EventListener {', + ' public:', + ' void OnFlushCompleted() override {}', + '};', + '', + ].join('\n'), + + // googletest: a base named from the global scope. + 'test/gtest/gtest.h': [ + 'namespace testing {', + 'class Test {', + ' public:', + ' virtual void TestBody() = 0;', + '};', + 'namespace internal {', + 'class SuiteApiResolver {', + ' public:', + ' typedef int Test;', + '};', + '} // namespace internal', + '} // namespace testing', + '', + ].join('\n'), + 'test/gtest-extra-test.cc': [ + '#include "test/gtest/gtest.h"', + 'class single_evaluation_test : public ::testing::Test {', + ' protected:', + ' void TestBody() override {}', + '};', + '', + ].join('\n'), + + // A template parameter names no class, whatever the project declares. + 'util/mixin.h': [ + 'class Base {', + ' public:', + ' void Run() {}', + '};', + 'template ', + 'class Logged : public Base {', + ' public:', + ' void Log() {}', + '};', + '', + ].join('\n'), + + // A header's using-directive the file itself does not repeat. + 'util/testharness.h': [ + '#include "include/leveldb/env.h"', + 'using namespace leveldb;', + '', + ].join('\n'), + 'include/leveldb/env.h': [ + 'namespace leveldb {', + 'class EnvWrapper {', + ' public:', + ' virtual int NowMicros() { return 0; }', + '};', + '} // namespace leveldb', + '', + ].join('\n'), + 'util/env_test.cc': [ + '#include "util/testharness.h"', + 'class SleepingEnv : public EnvWrapper {', + ' public:', + ' int NowMicros() override { return 1; }', + '};', + '', + ].join('\n'), +}; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-bases-')); + 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); + } + cg = await CodeGraph.init(root, { index: true }); +}, 60_000); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +function node(qualifiedName: string, file: string): Node { + const name = qualifiedName.split('::').pop()!; + const found = cg.getNodesByName(name).filter((n) => n.qualifiedName === qualifiedName && n.filePath === file && n.kind !== 'import'); + expect(found, `${qualifiedName} in ${file}`).toHaveLength(1); + return found[0]!; +} + +/** The supertypes of a class, as `kind qualifiedName (file) resolvedBy`. */ +function supertypes(qualifiedName: string, file: string): string[] { + return cg + .getOutgoingEdgesFrom([node(qualifiedName, file).id], ['extends', 'implements']) + .map((e) => { + const target = cg.getNode(e.target); + return `${e.kind} ${target?.qualifiedName} (${target?.filePath}) ${String(e.metadata?.resolvedBy)}`; + }) + .sort(); +} + +/** The base names a class's file names that stay unresolved. */ +function unresolved(qualifiedName: string, file: string): string[] { + return cg + .getUnresolvedReferencesFrom(node(qualifiedName, file).id) + .filter((r) => r.referenceKind === 'extends' || r.referenceKind === 'implements') + .map((r) => r.referenceName) + .sort(); +} + +describe('a C++ base class is the one name lookup finds from the deriving class', () => { + it('finds the base in the namespaces around the class, not a namesake elsewhere', () => { + expect(supertypes('google::protobuf::DynamicMessage', 'src/google/protobuf/dynamic_message.cc')).toEqual([ + 'extends google::protobuf::Message (src/google/protobuf/message.h) qualified-name', + ]); + expect(supertypes('google::protobuf::internal::ZeroFieldsBase', 'src/google/protobuf/generated_message_bases.h')).toEqual([ + 'extends google::protobuf::Message (src/google/protobuf/message.h) qualified-name', + ]); + expect(supertypes('leveldb::DBIter', 'db/db_iter.cc')).toEqual([ + 'extends leveldb::Iterator (include/leveldb/iterator.h) qualified-name', + ]); + }); + + it('never takes a class another source file defines for its own translation unit', () => { + expect(supertypes('leveldb::NumberComparator', 'db/db_test.cc')).toEqual([ + 'extends leveldb::Comparator (include/leveldb/comparator.h) qualified-name', + ]); + }); + + it('looks a nested class base up in its outer class and the classes that one inherits', () => { + expect(supertypes('rocksdb::clock_cache::AutoHyperClockTable::Opts', 'cache/clock_cache.h')).toEqual([ + 'extends rocksdb::clock_cache::BaseClockTable::BaseOpts (cache/clock_cache.h) qualified-name', + ]); + }); + + it('reads the namespace aliases, using-declarations and using-directives the file writes', () => { + expect(supertypes('google::protobuf::AnyGlobalsTypeInternal', 'src/google/protobuf/any.pb.cc')).toEqual([ + 'extends google::protobuf::internal::MessageGlobalsBase (src/google/protobuf/message_lite.h) qualified-name', + ]); + expect(supertypes('leveldb_filterpolicy_t', 'db/c.cc')).toEqual([ + 'extends leveldb::FilterPolicy (include/leveldb/filter_policy.h) qualified-name', + ]); + expect(supertypes('Compactor', 'examples/compact_files_example.cc')).toEqual([ + 'extends rocksdb::EventListener (include/rocksdb/listener.h) qualified-name', + ]); + }); + + it('finds `::testing::Test` from the global scope, not a nested `Test` typedef', () => { + expect(supertypes('single_evaluation_test', 'test/gtest-extra-test.cc')).toEqual([ + 'extends testing::Test (test/gtest/gtest.h) qualified-name', + ]); + }); + + it('follows an alias to the class it names, and links an alias of an outside type itself', () => { + expect(supertypes('rocksdb::MergingIterator', 'table/merging_iterator.cc')).toEqual([ + 'extends rocksdb::InternalIteratorBase (table/internal_iterator.h) qualified-name', + ]); + // `bool_constant`: the comparison is no template bracket. + expect(supertypes('rocksdb::is_fast_float', 'table/merging_iterator.cc')).toEqual([ + 'extends rocksdb::bool_constant (table/internal_iterator.h) qualified-name', + ]); + }); + + it('leaves a template parameter unresolved', () => { + expect(supertypes('Logged', 'util/mixin.h')).toEqual([]); + expect(unresolved('Logged', 'util/mixin.h')).toEqual(['Base']); + }); + + it('takes the only class of that name the file can see when the lookup finds none', () => { + // `using namespace leveldb;` is in a header the file includes. + expect(supertypes('SleepingEnv', 'util/env_test.cc')).toEqual([ + 'extends leveldb::EnvWrapper (include/leveldb/env.h) exact-match', + ]); + }); + + it('never resolves a C or C++ base through a framework resolver or to another language', () => { + const bases = cg + .getNodesByKind('class') + .concat(cg.getNodesByKind('struct')) + .filter((n) => n.language === 'cpp' || n.language === 'c'); + const edges = cg.getOutgoingEdgesFrom(bases.map((n) => n.id), ['extends', 'implements']); + expect(edges.length).toBeGreaterThan(0); + for (const e of edges) { + const target = cg.getNode(e.target); + expect(e.metadata?.resolvedBy, `${cg.getNode(e.source)?.qualifiedName} → ${target?.qualifiedName}`).not.toBe('framework'); + expect(['c', 'cpp']).toContain(target?.language); + } + expect(cg.getIncomingEdgesTo([node('Message', 'php/ext/google/protobuf/message.c').id], ['extends'])).toEqual([]); + }); +}); + +describe('what the hierarchy feeds', () => { + it('links each base method to the overrides of the right subclasses', () => { + const overrides = (qualifiedName: string, file: string): string[] => + cg + .getOutgoingEdgesFrom([node(qualifiedName, file).id], ['calls']) + .filter((e) => e.metadata?.synthesizedBy === 'cpp-override') + .map((e) => cg.getNode(e.target)?.qualifiedName ?? '') + .sort(); + expect(overrides('leveldb::Iterator::Next', 'include/leveldb/iterator.h')).toEqual(['leveldb::DBIter::Next']); + expect(overrides('leveldb::SkipList::Iterator::Next', 'db/skiplist.h')).toEqual([]); + expect(overrides('google::protobuf::Message::ByteSizeLong', 'src/google/protobuf/message.h')).toEqual([ + 'google::protobuf::DynamicMessage::ByteSizeLong', + 'google::protobuf::internal::ZeroFieldsBase::ByteSizeLong', + ]); + expect(overrides('google::protobuf::json_internal::ResolverPool::Message::ByteSizeLong', 'src/google/protobuf/json/internal/untyped_message.h')).toEqual([]); + // Through the alias: InternalIterator is InternalIteratorBase. + expect(overrides('rocksdb::InternalIteratorBase::Next', 'table/internal_iterator.h')).toEqual(['rocksdb::MergingIterator::Next']); + }); + + it('resolves a call to an inherited method on the base an alias names', () => { + const callees = cg + .getOutgoingEdgesFrom([node('rocksdb::Rewind', 'table/merging_iterator.cc').id], ['calls']) + .map((e) => `${cg.getNode(e.target)?.qualifiedName} (${cg.getNode(e.target)?.filePath})`); + expect(callees).toEqual(['rocksdb::InternalIteratorBase::SeekToFirst (table/internal_iterator.h)']); + }); + + it('shows the real ancestors in the type hierarchy', () => { + const ancestors = (qualifiedName: string, file: string): string[] => + (buildTypeHierarchy(cg, node(qualifiedName, file))?.ancestors ?? []).map((a) => `${a.relation} ${a.node.qualifiedName} (${a.node.filePath})`); + expect(ancestors('google::protobuf::DynamicMessage', 'src/google/protobuf/dynamic_message.cc')).toEqual([ + 'extends google::protobuf::Message (src/google/protobuf/message.h)', + ]); + expect(ancestors('leveldb::DBIter', 'db/db_iter.cc')).toEqual(['extends leveldb::Iterator (include/leveldb/iterator.h)']); + }); +}); diff --git a/src/resolution/cpp-supertypes.ts b/src/resolution/cpp-supertypes.ts new file mode 100644 index 000000000..ff47ebad2 --- /dev/null +++ b/src/resolution/cpp-supertypes.ts @@ -0,0 +1,287 @@ +/** + * A C or C++ base class — `class DynamicMessage final : public Message` — is + * the class C++ name lookup finds for it. + * + * The base used to be matched by its name alone, so one named like a class of + * another namespace, or of another language, bound to whichever namesake + * ranked first: protobuf's `google::protobuf::DynamicMessage` derived from + * `json_internal::ResolverPool::Message`, and before #2397 from the PHP + * extension's C struct `Message`; leveldb's iterators from + * `SkipList::Iterator`. A wrong supertype reaches everything that walks the + * hierarchy: the supertype walk that finds an inherited method, the + * cpp-override edges, the type hierarchy `codegraph_explore` prints. + * + * The name is looked up as C++ looks it up: + * - from the scope the class is declared in (its own members are not in + * scope in its base clause), outwards to the global scope; a class scope + * includes its bases, a qualified name is resolved a segment at a time and + * an alias is followed to the class it names (cpp-type-aliases.ts); + * `::Base` is the global one; + * - then through what the file writes before the class: a namespace alias + * (`namespace _pbi = ::google::protobuf::internal;`), a using-declaration + * (`using leveldb::FilterPolicy;`), a using-directive (`using namespace + * ROCKSDB_NAMESPACE;`); and through a namespace a macro opens + * (`fmt::detail::buffer` under fmt's FMT_BEGIN_NAMESPACE); + * - a class a source file (`.cc`, `.cpp`, `.c`) defines belongs to that + * translation unit, so only that file sees it (leveldb's skiplist test + * declares its own `leveldb::Comparator`); + * - a template parameter (`template class X : public Base`) or + * a member of one (`public T::Base`) is whatever a template argument makes + * it: no edge. + * When none of that finds the class, a class of that name that is the only + * one the file can see is taken: the index loses a class's namespace in a + * few shapes (`class PROTOBUF_EXPORT FieldDescriptor`, the classes after a + * partial specialization), and the lookup from the wrong scope finds nothing. + * Several such classes would be a guess, so there is no edge. + * + * Only C and C++ declarations are candidates, and no other strategy (a + * framework's, the import resolver, name matching) sees these references. + */ +import type { Node } from '../types'; +import type { ResolvedRef, ResolutionContext, UnresolvedRef } from './types'; +import { + cppClassNamed, + cppParentScope, + cppScopesWithin, + cppTemplateParameters, + cppTypeSegments, + type CppNamedClass, +} from './cpp-type-aliases'; +import { cppMacroNamespaceFrames, cppNamespaceAliases } from './name-matcher'; +import { stripCommentsForRegex } from './strip-comments'; + +const CLASS_KINDS: ReadonlySet = new Set(['class', 'struct', 'union']); + +/** A C or C++ source file, whose definitions belong to its own translation unit. */ +const SOURCE_FILE = /\.(?:c|cc|cpp|cxx|c\+\+|cu|metal)$/i; + +/** Does `ref` name a C or C++ class's base? */ +export function isCppSupertypeRef(ref: UnresolvedRef): boolean { + return (ref.language === 'cpp' || ref.language === 'c') && + (ref.referenceKind === 'extends' || ref.referenceKind === 'implements'); +} + +function isCFamily(n: Node): boolean { + return n.language === 'cpp' || n.language === 'c'; +} + +/** The declarations code in `file` can see: a header's, and the file's own. */ +function visibleFrom(file: string): (n: Node) => boolean { + return (n) => n.filePath === file || !SOURCE_FILE.test(n.filePath); +} + +export function matchCppSupertype(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + const derived = context.getNodeById?.(ref.fromNodeId); + if (!derived || !isCFamily(derived) || !CLASS_KINDS.has(derived.kind)) return null; + const written = writtenBase(ref, context); + const segments = cppTypeSegments(written); + if (!segments) return null; + const global = written.startsWith('::'); + if (!global && cppTemplateParameters(derived, context).has(segments[0]!)) return null; + + const visible = visibleFrom(derived.filePath); + const scopes = cppScopesWithin(cppParentScope(derived.qualifiedName)); + const resolved = (target: Node | null | undefined): ResolvedRef | null => + target ? { original: ref, targetNodeId: target.id, confidence: 0.9, resolvedBy: 'qualified-name' } : null; + + // The scopes around the class, or only the global one for `::Base`. + const lexical = classNode(cppClassNamed(written, global ? [''] : scopes, ref, context, visible), derived, context); + if (lexical !== undefined) return resolved(lexical); + + // What the file writes before the class, the nearest first: each stands for + // the name's first segment, and the rest stays as written. + const path = segments.join('::'); + const first = /^\s*(?:::\s*)?[A-Za-z_]\w*/.exec(written)?.[0] ?? ''; + const after = written.slice(first.length); + const usings = usingsIn(derived.filePath, context).filter((u) => u.line < ref.line).reverse(); + const lookUp = (spelled: string): Node | undefined => { + const absolute = spelled.startsWith('::'); + return classNode(cppClassNamed(spelled, absolute ? [''] : scopes, ref, context, visible), derived, context) ?? undefined; + }; + // `::_pbi::MessageGlobalsBase` under `namespace _pbi = ::google::protobuf::internal;`. + if (segments.length > 1) { + const local = usings.find((u) => u.kind === 'alias' && u.name === segments[0]); + const target = local?.path ?? cppNamespaceAliases(context).get(segments[0]!); + const hit = target ? lookUp(target + after) : undefined; + if (hit) return resolved(hit); + } + if (!global) { + // `using leveldb::FilterPolicy;` … `public FilterPolicy`. + for (const u of usings) { + if (u.kind !== 'declaration' || u.name !== segments[0]) continue; + const hit = lookUp(u.path + after); + if (hit) return resolved(hit); + } + // `using namespace ROCKSDB_NAMESPACE;` … `public EventListener`. + for (const u of usings) { + if (u.kind !== 'directive') continue; + const hit = lookUp(`${u.path}::${written}`); + if (hit) return resolved(hit); + } + } + // `fmt::detail::buffer`, where a macro opens `namespace fmt` around `detail::buffer`. + if (segments.length > 1) { + const hit = pick(namedClasses(segments, derived, context, visible) + .filter((n) => macroQualifiedName(n, context) === path), derived); + if (hit) return resolved(hit); + } + // The only class of that name the file can see, by its qualified tail. + const tail = namedClasses(segments, derived, context, visible) + .filter((n) => n.qualifiedName === path || n.qualifiedName.endsWith(`::${path}`)); + const own = tail.filter((n) => n.filePath === derived.filePath); + const sole = own.length > 0 ? own : tail; + return sole.length === 1 ? { original: ref, targetNodeId: sole[0]!.id, confidence: 0.7, resolvedBy: 'exact-match' } : null; +} + +/** + * The base as the base clause writes it, template arguments included + * (`bool_constant`): the reference carries the name without + * them, and an alias template is only ever named with them. The reference's + * own name when the source there reads otherwise. + */ +function writtenBase(ref: UnresolvedRef, context: ResolutionContext): string { + const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/); + const text = lines?.slice(ref.line - 1, ref.line + 11).join('\n').slice(Math.max(0, ref.column)) ?? ''; + let depth = 0; + let end = text.length; + for (let i = 0; i < text.length; i++) { + const c = text[i]!; + // A template's `<` follows its name; `sizeof(T) <= 8` and `1 << n` compare and shift. + const opens = c === '(' || (c === '<' && /\w/.test(text[i - 1] ?? '') && text[i + 1] !== '=' && text[i + 1] !== '<'); + const closes = c === ')' || (c === '>' && text[i - 1] !== '-' && text[i + 1] !== '='); + if (opens) depth++; + else if (closes) { + if (depth === 0) { + end = i; + break; + } + depth--; + } else if (depth === 0 && (c === ',' || c === '{' || c === ';')) { + end = i; + break; + } + } + const written = text.slice(0, end).trim(); + const squash = (s: string): string => s.replace(/\s+/g, ''); + return squash(stripTemplateArguments(written)) === squash(ref.referenceName) ? written : ref.referenceName.trim(); +} + +/** `Base>::Inner` → `Base::Inner`. */ +function stripTemplateArguments(text: string): string { + let out = ''; + let depth = 0; + for (const c of text) { + if (c === '<') depth++; + else if (c === '>') depth = Math.max(0, depth - 1); + else if (depth === 0) out += c; + } + return out; +} + +/** + * The node of the class a lookup found, as the deriving class's file sees it + * (null: the name names no particular class; undefined: keep looking). + */ +function classNode(named: CppNamedClass, derived: Node, context: ResolutionContext): Node | null | undefined { + if (named === null || named === undefined) return named; + if ('alias' in named) return named.alias; + const classes = context + .getNodesByQualifiedName(named.cls) + .filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== derived.id); + // A lookup that ended on the class itself (its namespace lost) found nothing. + if (classes.length === 0) return undefined; + const visible = classes.filter(visibleFrom(derived.filePath)); + return pick(visible.length > 0 ? visible : classes, derived); +} + +/** + * One of several declarations of a class (separate translation units, + * vendored copies): the deriving class's own file's, then its header's, then + * the nearest by path. + */ +function pick(classes: Node[], derived: Node): Node | undefined { + if (classes.length <= 1) return classes[0]; + const own = classes.find((n) => n.filePath === derived.filePath); + if (own) return own; + const stem = (p: string): string => p.replace(/\.[^./\\]+$/, ''); + const header = classes.find((n) => stem(n.filePath) === stem(derived.filePath)); + if (header) return header; + const shared = (p: string): number => { + const a = p.split('/'); + const b = derived.filePath.split('/'); + let i = 0; + while (i < a.length - 1 && i < b.length - 1 && a[i] === b[i]) i++; + return i; + }; + return classes.reduce((best, n) => (shared(n.filePath) > shared(best.filePath) ? n : best)); +} + +/** C and C++ classes named like the last segment of `segments` that `derived`'s file can see. */ +function namedClasses(segments: readonly string[], derived: Node, context: ResolutionContext, visible: (n: Node) => boolean): Node[] { + return context + .getNodesByName(segments[segments.length - 1]!) + .filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== derived.id && visible(n)); +} + +/** A declaration's qualified name with the namespaces macros open around it: `fmt::detail::buffer` for `detail::buffer`. */ +function macroQualifiedName(n: Node, context: ResolutionContext): string { + const prefix = cppMacroNamespaceFrames(n.filePath, context) + .filter((f) => f.start <= n.startLine && f.end >= n.startLine) + .sort((a, b) => a.start - b.start) + .flatMap((f) => f.path); + return prefix.length > 0 ? `${prefix.join('::')}::${n.qualifiedName}` : n.qualifiedName; +} + +/** A `using` declaration or directive, or a namespace alias, a C or C++ file writes. */ +interface CppUsing { + line: number; + kind: 'declaration' | 'directive' | 'alias'; + /** The name it brings in: the declared name, or the alias. */ + name: string; + /** What it names, as written: `leveldb::FilterPolicy`, `ROCKSDB_NAMESPACE`, `::google::protobuf::internal`. */ + path: string; +} + +const USINGS = new WeakMap>(); + +/** Drop the per-file `using` memo (see ReferenceResolver.clearCaches). */ +export function clearCppSupertypeMemos(context: ResolutionContext): void { + USINGS.delete(context); +} + +const PATH = String.raw`(?:::\s*)?[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*`; +const USING = new RegExp( + String.raw`\busing\s+namespace\s+(${PATH})\s*;` + + String.raw`|\busing\s+(${PATH}\s*::\s*[A-Za-z_]\w*)\s*;` + + String.raw`|\bnamespace\s+([A-Za-z_]\w*)\s*=\s*(${PATH})\s*;`, + 'g', +); + +/** The `using` declarations and directives and namespace aliases of a file, in order. */ +function usingsIn(file: string, context: ResolutionContext): CppUsing[] { + let memo = USINGS.get(context); + if (!memo) USINGS.set(context, (memo = new Map())); + const hit = memo.get(file); + if (hit) return hit; + const found: CppUsing[] = []; + const source = context.readFile(file); + if (source && (source.includes('using') || source.includes('namespace'))) { + const text = stripCommentsForRegex(source, 'cpp'); + let line = 1; + let at = 0; + for (const m of text.matchAll(USING)) { + for (; at < m.index; at++) if (text.charCodeAt(at) === 10) line++; + const squash = (s: string): string => s.replace(/\s+/g, ''); + if (m[1]) { + found.push({ line, kind: 'directive', name: '', path: squash(m[1]) }); + } else if (m[2]) { + const path = squash(m[2]); + found.push({ line, kind: 'declaration', name: path.slice(path.lastIndexOf('::') + 2), path }); + } else if (m[3] && m[4]) { + found.push({ line, kind: 'alias', name: m[3], path: squash(m[4]) }); + } + } + } + memo.set(file, found); + return found; +} diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts index 82070e803..374d57919 100644 --- a/src/resolution/cpp-type-aliases.ts +++ b/src/resolution/cpp-type-aliases.ts @@ -262,13 +262,20 @@ type Declared = | { cls: string } /** An enum, namespace or other non-class type. */ | 'other' - /** Aliases naming different types, none of them the calling file's. */ - | 'ambiguous' + /** + * Aliases naming different types, none of them the calling file's: rocksdb's + * `using DBWithTTLImplBase = …;` in each arm of an `#if USE_COROUTINES`. + */ + | { ambiguous: Node[] } | undefined; +/** Which declarations a lookup can see; every one of them unless a caller narrows it. */ +type Visible = (n: Node) => boolean; + /** C and C++ declarations named `name` that can stand for a type or scope. */ -function typeDeclarations(name: string, context: ResolutionContext): Node[] { - return context.getNodesByName(name).filter((n) => isCFamily(n) && (n.kind === 'type_alias' || DECLARING_KINDS.has(n.kind))); +function typeDeclarations(name: string, context: ResolutionContext, visible?: Visible): Node[] { + return context.getNodesByName(name).filter((n) => + isCFamily(n) && (n.kind === 'type_alias' || DECLARING_KINDS.has(n.kind)) && (!visible || visible(n))); } function declaredIn( @@ -288,7 +295,7 @@ function declaredIn( if (here.some((n) => CLASS_KINDS.has(n.kind))) return { cls: qualified }; if (here.some((n) => n.kind !== 'type_alias')) return 'other'; const alias = pickAlias(here, ref, context); - return alias ? { alias } : 'ambiguous'; + return alias ? { alias } : { ambiguous: here }; } /** @@ -429,8 +436,15 @@ function memberOf( } /** `name` looked up unqualified from `scopes`, innermost first; a class scope includes its bases. */ -function lookupUnqualified(name: string, templated: boolean, scopes: readonly string[], ref: UnresolvedRef, context: ResolutionContext): Declared { - const named = typeDeclarations(name, context); +function lookupUnqualified( + name: string, + templated: boolean, + scopes: readonly string[], + ref: UnresolvedRef, + context: ResolutionContext, + visible?: Visible, +): Declared { + const named = typeDeclarations(name, context, visible); if (named.length === 0) return undefined; for (const scope of scopes) { const found = scope ? memberOf(scope, name, named, templated, ref, context) : declaredIn('', name, named, templated, ref, context); @@ -472,22 +486,95 @@ export function cppAliasedTypeName(resolved: CppAliasedType): string { return [resolved.target[resolved.target.length - 1]!, ...resolved.rest].join('::'); } -function follow( - name: TypeName, - scope: string, +/** What a C++ type name names when it is looked up for a class (see cppClassNamed). */ +export type CppNamedClass = + /** A class, by its qualified name. */ + | { cls: string } + /** + * An alias that leads to none of the project's classes: one of a type + * outside the project (`template using bool_constant = + * std::integral_constant;`), of what a template argument decides, + * or one each `#if` arm declares differently (the first declaration). + */ + | { alias: Node } + /** No particular class: an enum, a member of a type outside the project. */ + | null + /** Nothing in the scopes looked in declares it. */ + | undefined; + +/** + * The class a C++ type written as `written` names, looked up from `scopes` + * (innermost first) as a declared type is — a class scope with its bases, a + * qualified name a segment at a time — and followed through an alias it + * reaches: rocksdb's `InternalIterator` under `using InternalIterator = + * InternalIteratorBase;` names `InternalIteratorBase`. `visible` + * narrows the declarations the lookup can see. + */ +export function cppClassNamed( + written: string, + scopes: readonly string[], ref: UnresolvedRef, context: ResolutionContext, - hops: number, -): CppAliasedType | null | undefined { - const scopes = scopesWithin(scope); - let found = lookupUnqualified(name.names[0]!, name.templated[0]!, scopes, ref, context); + visible?: Visible, +): CppNamedClass { + const name = cppTypeName(written); + if (!name) return null; + const { found, next } = lookupSegments(name, scopes, ref, context, visible); + if (found === undefined) return undefined; + if (typeof found !== 'object') return null; + if ('cls' in found) return found; + if ('ambiguous' in found) { + // An alias each `#if` arm declares differently: the first declaration. + const [first] = [...found.ambiguous].sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine); + return next === name.names.length ? { alias: first! } : null; + } + const alias = found.alias; + const text = declarationText(alias, context); + // The aliased type is written in the alias's own scope. + const aliased = expand(text ? aliasedTypeIn(text, alias.name) : null, name.names.slice(next), name.templated.slice(next), + parentScope(alias.qualifiedName), ref, context, 1, templateParametersAround(alias, context)); + const cls = aliased && !aliased.pointer && classNamed([...aliased.target, ...aliased.rest], scopesWithin(aliased.scope), context); + if (cls) return { cls }; + // A member of a type outside the project (`Alias::Inner`) is not the alias. + return next === name.names.length ? { alias } : null; +} + +/** The scopes visible from inside the scope `qualified`, innermost first, ending with the global scope. */ +export function cppScopesWithin(qualified: string): string[] { + return scopesWithin(qualified); +} + +/** The scope a C++ declaration is declared in: `leveldb::MemTable` for `leveldb::MemTable::Table`. */ +export function cppParentScope(qualified: string): string { + return parentScope(qualified); +} + +/** The template parameters in scope where `node` is declared: its own `template <…>` header and its class templates'. */ +export function cppTemplateParameters(node: Node, context: ResolutionContext): ReadonlySet { + return templateParametersAround(node, context); +} + +/** + * What a written name declares, looked up from `scopes` (innermost first), and + * how many of its segments that took: a class's member is looked up in the + * class, and an alias ends the walk (`next` is where the rest of the name + * starts). + */ +function lookupSegments( + name: TypeName, + scopes: readonly string[], + ref: UnresolvedRef, + context: ResolutionContext, + visible?: Visible, +): { found: Declared; next: number } { + let found = lookupUnqualified(name.names[0]!, name.templated[0]!, scopes, ref, context, visible); let next = 1; if (found === undefined && name.names.length > 1) { // Namespaces are not indexed: `detail::buffer_t` from inside `fmt` is // found by its spelling in an enclosing scope, longest prefix first. for (let i = name.names.length; i >= 2 && found === undefined; i--) { const spelled = name.names.slice(0, i); - const named = typeDeclarations(spelled[spelled.length - 1]!, context); + const named = typeDeclarations(spelled[spelled.length - 1]!, context, visible); for (const s of scopes) { found = declaredIn(s ? `${s}::${spelled.slice(0, -1).join('::')}` : spelled.slice(0, -1).join('::'), spelled[spelled.length - 1]!, named, name.templated[i - 1]!, ref, context); @@ -498,9 +585,20 @@ function follow( } // `MemTable::Table`, `Table::Iterator`: one segment at a time. while (found && typeof found === 'object' && 'cls' in found && next < name.names.length) { - found = memberOf(found.cls, name.names[next]!, typeDeclarations(name.names[next]!, context), name.templated[next]!, ref, context); + found = memberOf(found.cls, name.names[next]!, typeDeclarations(name.names[next]!, context, visible), name.templated[next]!, ref, context); next++; } + return { found, next }; +} + +function follow( + name: TypeName, + scope: string, + ref: UnresolvedRef, + context: ResolutionContext, + hops: number, +): CppAliasedType | null | undefined { + const { found, next } = lookupSegments(name, scopesWithin(scope), ref, context); if (!found || typeof found !== 'object' || !('alias' in found)) return undefined; const alias = found.alias; if (hops >= MAX_HOPS) return null; diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 2d1b50a13..d8eca46e4 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -24,6 +24,7 @@ import { import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified, isGoBareName, GO_TYPE_KINDS } from './name-matcher'; import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility'; import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; +import { isCppSupertypeRef, matchCppSupertype, clearCppSupertypeMemos } from './cpp-supertypes'; import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility'; import { clearDartLibraryMemos } from './dart-libraries'; import { clearVbnetReceiverMemos, isVbMemberRead, isVbPathCall, matchVbMemberRead, matchVbPathCall } from './vbnet-receivers'; @@ -532,6 +533,7 @@ export class ReferenceResolver { clearTypeParameterMemos(this.context); clearDartLocalScopeMemos(this.context); clearCppTypeAliasMemos(this.context); + clearCppSupertypeMemos(this.context); } } @@ -1198,6 +1200,11 @@ export class ReferenceResolver { // A local C++ object construction (`T obj(args)`, ref `ns::T::T/1`) // resolves ONLY to a constructor of the lexically nearest `T` (#1839). if (isCppConstructorRef(ref)) return matchCppConstructor(ref, this.context); + // A C or C++ base class is the one C++ name lookup finds from where the + // class is declared — never a namesake of another namespace or language, + // which a framework's heuristics or name matching would take (see + // ./cpp-supertypes). + if (isCppSupertypeRef(ref)) return matchCppSupertype(ref, this.context); // A Dart member read (`x.area`) links the getter the receiver's type // reaches, as a call, or nothing — never a guess by name (#2338). diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 045fe03fc..08dc5575f 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -12697,7 +12697,7 @@ const CPP_CLOSER_BODY = /^(?:[A-Za-z_]\w*\s+)*\}(?:\s*\})*\s*;?$/; const CPP_NS_ALIASES = new WeakMap>(); /** The project's namespace aliases: `namespace py = pybind11;`. */ -function cppNamespaceAliases(context: ResolutionContext): Map { +export function cppNamespaceAliases(context: ResolutionContext): Map { const hit = CPP_NS_ALIASES.get(context); if (hit) return hit; const aliases = new Map(); @@ -12760,7 +12760,7 @@ function cppNamespaceMacros(context: ResolutionContext): { openers: Map { +export function cppMacroNamespaceFrames(file: string, context: ResolutionContext): Array<{ start: number; end: number; path: string[] }> { let memo = CPP_NS_FRAMES.get(context); if (!memo) { memo = new Map();