mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 13:53:01 +08:00
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 main02d22aeb, then again on maindd706469after #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:
@@ -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.
|
||||
|
||||
@@ -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)']);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user