mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
4040eab975
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>
514 lines
18 KiB
TypeScript
514 lines
18 KiB
TypeScript
/**
|
|
* 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)']);
|
|
});
|
|
});
|