fix(cpp): a base class links to the class C++ name lookup finds, not a namesake (#2407)

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 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-07 03:01:39 +00:00
committed by GitHub
parent fe9518357c
commit 4040eab975
6 changed files with 924 additions and 18 deletions
+1
View File
@@ -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<const char*, KeyComparator> 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.
+513
View File
@@ -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<string, string> = {
// 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 <typename Key, class Comparator>',
'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 TValue>',
'class InternalIteratorBase {',
' public:',
' virtual void Next() = 0;',
' void SeekToFirst() {}',
'};',
'using InternalIterator = InternalIteratorBase<Slice>;',
'template <bool B> using bool_constant = std::integral_constant<bool, B>;',
'} // 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 <typename T>',
'struct is_fast_float : bool_constant<sizeof(T) <= sizeof(double)> {};',
'} // 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 Base>',
'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<sizeof(T) <= sizeof(double)>`: 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)']);
});
});
+287
View File
@@ -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 Base> 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<string> = 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<is_utf8_enabled>`): 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<T, Cmp<int>>::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<ResolutionContext, Map<string, CppUsing[]>>();
/** 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;
}
+114 -16
View File
@@ -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 <bool B> using bool_constant =
* std::integral_constant<bool, B>;`), 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<Slice>;` 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<string> {
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;
+7
View File
@@ -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).
+2 -2
View File
@@ -12697,7 +12697,7 @@ const CPP_CLOSER_BODY = /^(?:[A-Za-z_]\w*\s+)*\}(?:\s*\})*\s*;?$/;
const CPP_NS_ALIASES = new WeakMap<ResolutionContext, Map<string, string>>();
/** The project's namespace aliases: `namespace py = pybind11;`. */
function cppNamespaceAliases(context: ResolutionContext): Map<string, string> {
export function cppNamespaceAliases(context: ResolutionContext): Map<string, string> {
const hit = CPP_NS_ALIASES.get(context);
if (hit) return hit;
const aliases = new Map<string, string>();
@@ -12760,7 +12760,7 @@ function cppNamespaceMacros(context: ResolutionContext): { openers: Map<string,
}
/** The line ranges of a C / C++ file each namespace macro opens, with the namespace path it opens. */
function cppMacroNamespaceFrames(file: string, context: ResolutionContext): Array<{ start: number; end: number; path: string[] }> {
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();