mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 06:53:52 +08:00
A C++ call written through a class, `X::m()`, whose class has no node in the index fell through to the last strategy of matchMethodCall, which scores every method named `m` by the words its class shares with `X` and picks one. protocolbuffers/protobuf declares its descriptor messages in descriptor.pb.h, which is over the indexer's size cap, so `FeatureSet::default_instance()` linked to `UnknownFieldSet::default_instance`, `FeatureSetDefaults::GetDescriptor()` to `JavaFeatures_NestInFileClassFeature::GetDescriptor`, and the options messages' `default_instance()` to `pb::enumvalue::JsonEnumValueOptions`'. A class whose name only shares a word with `X` is another class: one named `X` was looked for first, by qualified name and by class name. That guess is now dropped when `X` is a capitalized name (isUndeclaredTypeName) that no C or C++ class, struct, union, enum, alias or namespace node has, and that the calling file makes nothing of itself (cppNamesBroughtIn): not a template parameter of any `template <…>` head in it, not an alias it declares (a function-local `using FD = FieldDescriptor;`, a typedef, or one a macro it expands declares, like rocksdb's IMPORT_RIBBON_IMPL_TYPES), not a namespace it opens, aliases or uses, and not googletest's TypeParam or TestFixture in a typed test. The one method of a name in the project is still taken as before. The scans read the file once (cppTypeNamesDeclared in cpp-type-aliases.ts, shared with the template-head reader) and the project's headers once for macro bodies, only when a guess would otherwise be returned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
497 lines
18 KiB
TypeScript
497 lines
18 KiB
TypeScript
/**
|
|
* A C++ call through a class the index doesn't hold — `FeatureSet::descriptor()`
|
|
* — gets no edge, rather than a guess by the words the qualifier shares with
|
|
* some other class's name.
|
|
*
|
|
* protocolbuffers/protobuf declares `FeatureSet` in `descriptor.pb.h`, which is
|
|
* over the indexer's file-size cap, so no class of that name is indexed. The
|
|
* call fell through to the last strategy, which scores every method named
|
|
* `descriptor` by the words its class shares with the qualifier, and picked
|
|
* `pb::JavaFeatures_NestInFileClassFeature::descriptor` ("Feature"). The
|
|
* Python extension's `OneofOptions::default_instance()` went to
|
|
* `pb::file::CppFileOptions::default_instance` ("Options"). A class whose name
|
|
* only shares a word with the qualifier is, by construction, another class:
|
|
* one named like the qualifier would have been found first.
|
|
*
|
|
* Untouched: a qualifier the index holds (a class, a nested class, an alias),
|
|
* a namespace, and a name the calling file makes something itself — a
|
|
* template parameter, an alias it declares in a function body or through a
|
|
* macro it expands, googletest's `TestFixture` in a typed test. So is the one
|
|
* method of a name in the project, which is still taken: protobuf's
|
|
* `MapTestPeer`, declared in a `.inc` file the index doesn't read as C++,
|
|
* reaches the one `CalculateHiCutoff` there is, the `KeyMapBase` method it
|
|
* hands on to.
|
|
*/
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { CodeGraph } from '../src';
|
|
import type { Node } from '../src/types';
|
|
|
|
const roots: string[] = [];
|
|
afterEach(() => {
|
|
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
async function indexed(files: Record<string, string>): Promise<CodeGraph> {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-undeclared-'));
|
|
roots.push(root);
|
|
for (const [rel, content] of Object.entries(files)) {
|
|
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
|
|
fs.writeFileSync(path.join(root, rel), content);
|
|
}
|
|
return CodeGraph.init(root, { index: true });
|
|
}
|
|
|
|
function callable(cg: CodeGraph, qualifiedName: string): Node {
|
|
const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName);
|
|
if (!node) throw new Error(`no function or method ${qualifiedName}`);
|
|
return node;
|
|
}
|
|
|
|
/** `calls` callees of a function or method, as `qualifiedName`. */
|
|
function calls(cg: CodeGraph, caller: string): string[] {
|
|
return cg
|
|
.getCallees(callable(cg, caller).id)
|
|
.filter((r) => r.edge.kind === 'calls')
|
|
.map((r) => r.node.qualifiedName)
|
|
.sort();
|
|
}
|
|
|
|
/** Where the `calls` edges written in `file` lead, as `name @ file:line` of each target. */
|
|
function callsFrom(cg: CodeGraph, file: string): string[] {
|
|
const out: string[] = [];
|
|
for (const caller of [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')]) {
|
|
if (caller.filePath !== file) continue;
|
|
for (const r of cg.getCallees(caller.id)) {
|
|
if (r.edge.kind === 'calls') out.push(`${r.node.name} @ ${r.node.filePath}:${r.node.startLine}`);
|
|
}
|
|
}
|
|
return out.sort();
|
|
}
|
|
|
|
const lines = (...l: string[]): string => [...l, ''].join('\n');
|
|
|
|
/** protobuf's classes whose methods share their names with the generated messages' ones. */
|
|
const PROTOBUF = {
|
|
'src/google/protobuf/compiler/java/java_features.pb.h': lines(
|
|
'namespace pb {',
|
|
'class JavaFeatures_NestInFileClassFeature {',
|
|
' public:',
|
|
' static const void* descriptor() { return nullptr; }',
|
|
' static const void* GetDescriptor() { return nullptr; }',
|
|
' static const JavaFeatures_NestInFileClassFeature& default_instance() { return *instance_; }',
|
|
' private:',
|
|
' static JavaFeatures_NestInFileClassFeature* instance_;',
|
|
'};',
|
|
'} // namespace pb',
|
|
),
|
|
'src/google/protobuf/cpp_file_options.pb.h': lines(
|
|
'namespace pb {',
|
|
'namespace file {',
|
|
'class CppFileOptions {',
|
|
' public:',
|
|
' static const CppFileOptions& default_instance() { return *instance_; }',
|
|
' private:',
|
|
' static CppFileOptions* instance_;',
|
|
'};',
|
|
'} // namespace file',
|
|
'} // namespace pb',
|
|
),
|
|
'src/google/protobuf/compiler/plugin.pb.h': lines(
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'namespace compiler {',
|
|
'class CodeGeneratorResponse_File {',
|
|
' public:',
|
|
' static const CodeGeneratorResponse_File& default_instance() { return *instance_; }',
|
|
' private:',
|
|
' static CodeGeneratorResponse_File* instance_;',
|
|
'};',
|
|
'} // namespace compiler',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
'src/google/protobuf/message.h': lines(
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'class Message {',
|
|
' public:',
|
|
' const void* GetDescriptor() const { return nullptr; }',
|
|
'};',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
'src/google/protobuf/compiler/cpp/message.h': lines(
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'namespace compiler {',
|
|
'namespace cpp {',
|
|
'class MessageGenerator {',
|
|
' public:',
|
|
' const void* descriptor() const { return descriptor_; }',
|
|
' private:',
|
|
' const void* descriptor_;',
|
|
'};',
|
|
'} // namespace cpp',
|
|
'} // namespace compiler',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
// `descriptor.pb.h`, which declares FeatureSet and the options messages, is
|
|
// over the size cap; its `.cc` defines some of their methods out of line.
|
|
'src/google/protobuf/descriptor.pb.cc': lines(
|
|
'#include "google/protobuf/descriptor.pb.h"',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'void FeatureSet::Clear() {}',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
};
|
|
|
|
describe('a C++ call through a class the index does not hold', () => {
|
|
it('reproduction: `FeatureSet::descriptor()` is no JavaFeatures_NestInFileClassFeature::descriptor', async () => {
|
|
const cg = await indexed({
|
|
...PROTOBUF,
|
|
'src/google/protobuf/compiler/code_generator.cc': lines(
|
|
'#include "google/protobuf/compiler/code_generator.h"',
|
|
'#include "google/protobuf/descriptor.pb.h"',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'namespace compiler {',
|
|
'const void* FeatureSetDescriptor() {',
|
|
' return FeatureSet::descriptor();',
|
|
'}',
|
|
'} // namespace compiler',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'google::protobuf::compiler::FeatureSetDescriptor')).toEqual([]);
|
|
const decoy = callable(cg, 'pb::JavaFeatures_NestInFileClassFeature::descriptor');
|
|
expect(cg.getCallers(decoy.id).filter((r) => r.edge.kind === 'calls')).toEqual([]);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('the Python extension\'s options and source-info defaults, and a message a using-declaration names', async () => {
|
|
const cg = await indexed({
|
|
...PROTOBUF,
|
|
'python/google/protobuf/pyext/descriptor.cc': lines(
|
|
'#include "google/protobuf/descriptor.pb.h"',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'namespace python {',
|
|
'const void* Defaults() {',
|
|
' const void* a = &OneofOptions::default_instance();',
|
|
' const void* b = &ServiceOptions::default_instance();',
|
|
' const void* c = &SourceCodeInfo::default_instance();',
|
|
' return FeatureSetDefaults::GetDescriptor() ? a : b ? b : c;',
|
|
'}',
|
|
'} // namespace python',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
'editions/generated_files_test.cc': lines(
|
|
'#include "editions/golden/editions_default.pb.h"',
|
|
'using ::protobuf_editions_test::EditionsDefaultMessage;',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'const void* DefaultDescriptor() {',
|
|
' return EditionsDefaultMessage::descriptor();',
|
|
'}',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'google::protobuf::python::Defaults')).toEqual([]);
|
|
expect(calls(cg, 'google::protobuf::DefaultDescriptor')).toEqual([]);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('a method the class defines out of line is still found by its qualified name', async () => {
|
|
const cg = await indexed({
|
|
...PROTOBUF,
|
|
'src/google/protobuf/descriptor.cc': lines(
|
|
'#include "google/protobuf/descriptor.pb.h"',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'void ResetFeatures() {',
|
|
' FeatureSet::Clear();',
|
|
' FeatureSet::descriptor();',
|
|
'}',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'google::protobuf::ResetFeatures')).toEqual(['google::protobuf::FeatureSet::Clear']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('qualified calls the index can place are untouched', () => {
|
|
it('a project class, and a nested class from inside its owner', async () => {
|
|
const cg = await indexed({
|
|
'arena.h': lines(
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'class Arena {',
|
|
' public:',
|
|
' static void* Create(int n) { return nullptr; }',
|
|
'};',
|
|
'class ArenaStringPtr {',
|
|
' public:',
|
|
' static void* Create(int n) { return nullptr; }',
|
|
'};',
|
|
'class Outer {',
|
|
' public:',
|
|
' class Inner {',
|
|
' public:',
|
|
' static int Count() { return 0; }',
|
|
' };',
|
|
' int Total() { return Inner::Count(); }',
|
|
'};',
|
|
'class InnerCounter {',
|
|
' public:',
|
|
' static int Count() { return 1; }',
|
|
'};',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
'arena.cc': lines(
|
|
'#include "arena.h"',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'void* Make() { return Arena::Create(1); }',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'google::protobuf::Make')).toEqual(['google::protobuf::Arena::Create']);
|
|
expect(calls(cg, 'google::protobuf::Outer::Total')).toEqual(['google::protobuf::Outer::Inner::Count']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('a project namespace, a namespace alias, and abseil', async () => {
|
|
const cg = await indexed({
|
|
'util/strings.h': lines(
|
|
'namespace Util {',
|
|
'inline int Trim(int x) { return x; }',
|
|
'} // namespace Util',
|
|
'namespace proto {',
|
|
'namespace wire {',
|
|
'inline int Format(int x) { return x; }',
|
|
'} // namespace wire',
|
|
'} // namespace proto',
|
|
),
|
|
'util/use.cc': lines(
|
|
'#include "util/strings.h"',
|
|
'#include "absl/strings/str_cat.h"',
|
|
'namespace Wire = proto::wire;',
|
|
'int Use(int x) {',
|
|
' return Util::Trim(x) + Wire::Format(x) + absl::StrCat(x);',
|
|
'}',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'Use')).toEqual(['Util::Trim', 'proto::wire::Format']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
/** rocksdb's ribbon solutions: an alias named after one shares a word with it. */
|
|
const RIBBON = {
|
|
'util/ribbon_impl.h': lines(
|
|
'namespace rocksdb {',
|
|
'namespace ribbon {',
|
|
'template <class TypesAndSettings>',
|
|
'class InMemSimpleSolution {',
|
|
' public:',
|
|
' static int RoundUpNumSlots(int n) { return n; }',
|
|
'};',
|
|
'template <class TypesAndSettings>',
|
|
'class SerializableInterleavedSolution {',
|
|
' public:',
|
|
' static int RoundUpNumSlots(int n) { return n; }',
|
|
'};',
|
|
'} // namespace ribbon',
|
|
'} // namespace rocksdb',
|
|
'#define IMPORT_RIBBON_IMPL_TYPES(TypesAndSettings) \\',
|
|
' using SimpleSoln = \\',
|
|
' rocksdb::ribbon::InMemSimpleSolution<TypesAndSettings>; \\',
|
|
' using InterleavedSoln = \\',
|
|
' rocksdb::ribbon::SerializableInterleavedSolution<TypesAndSettings>; \\',
|
|
' static_assert(sizeof(SimpleSoln) > 0, "semicolon expected after macro call")',
|
|
),
|
|
};
|
|
|
|
describe('a name the calling file makes something itself', () => {
|
|
it('an alias a macro the file expands declares (rocksdb\'s IMPORT_RIBBON_IMPL_TYPES)', async () => {
|
|
const cg = await indexed({
|
|
...RIBBON,
|
|
'util/ribbon_test.cc': lines(
|
|
'#include "util/ribbon_impl.h"',
|
|
'template <typename TypeParam>',
|
|
'int RoundUp(int n) {',
|
|
' IMPORT_RIBBON_IMPL_TYPES(TypeParam);',
|
|
' return SimpleSoln::RoundUpNumSlots(n) + InterleavedSoln::RoundUpNumSlots(n);',
|
|
'}',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'RoundUp')).toEqual([
|
|
'rocksdb::ribbon::InMemSimpleSolution::RoundUpNumSlots',
|
|
'rocksdb::ribbon::SerializableInterleavedSolution::RoundUpNumSlots',
|
|
]);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('an alias the calling function declares with `using` or `typedef`, and one the index holds', async () => {
|
|
const cg = await indexed({
|
|
...RIBBON,
|
|
'util/ribbon_alias.h': lines(
|
|
'#include "util/ribbon_impl.h"',
|
|
'namespace rocksdb {',
|
|
'using InterleavedSoln = ribbon::SerializableInterleavedSolution<int>;',
|
|
'} // namespace rocksdb',
|
|
),
|
|
'util/ribbon_bench.cc': lines(
|
|
'#include "util/ribbon_alias.h"',
|
|
'namespace rocksdb {',
|
|
'int Slots(int n) {',
|
|
' using SimpleSoln = ribbon::InMemSimpleSolution<int>;',
|
|
' return SimpleSoln::RoundUpNumSlots(n) + InterleavedSoln::RoundUpNumSlots(n);',
|
|
'}',
|
|
'int OldStyleSlots(int n) {',
|
|
' typedef ribbon::InMemSimpleSolution<long> SimpleSolnT;',
|
|
' return SimpleSolnT::RoundUpNumSlots(n);',
|
|
'}',
|
|
'} // namespace rocksdb',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'rocksdb::Slots')).toEqual([
|
|
'rocksdb::ribbon::InMemSimpleSolution::RoundUpNumSlots',
|
|
'rocksdb::ribbon::SerializableInterleavedSolution::RoundUpNumSlots',
|
|
]);
|
|
expect(calls(cg, 'rocksdb::OldStyleSlots')).toEqual(['rocksdb::ribbon::InMemSimpleSolution::RoundUpNumSlots']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('a template parameter with a default argument (fmt\'s `Traits::count()`)', async () => {
|
|
const cg = await indexed({
|
|
'include/fmt/base.h': lines(
|
|
'namespace fmt {',
|
|
'namespace detail {',
|
|
'class buffer_traits {',
|
|
' public:',
|
|
' static int count() { return 0; }',
|
|
'};',
|
|
'class fixed_buffer {',
|
|
' public:',
|
|
' static int count() { return 0; }',
|
|
'};',
|
|
'template <typename OutputIt, typename T,',
|
|
' typename Traits = buffer_traits>',
|
|
'class iterator_buffer : public Traits {',
|
|
' public:',
|
|
' int count() const { return Traits::count() + 1; }',
|
|
'};',
|
|
'} // namespace detail',
|
|
'} // namespace fmt',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'fmt::detail::iterator_buffer::count')).toEqual(['fmt::detail::buffer_traits::count']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('googletest\'s `TestFixture` in a typed test', async () => {
|
|
const cg = await indexed({
|
|
'upb/util/required_fields_test.cc': lines(
|
|
'#include <gtest/gtest.h>',
|
|
'template <typename T>',
|
|
'class RequiredFieldsTest : public testing::Test {',
|
|
' public:',
|
|
' void CheckRequired(const char* json) {}',
|
|
'};',
|
|
'class FieldsChecker {',
|
|
' public:',
|
|
' void CheckRequired(const char* json) {}',
|
|
'};',
|
|
'TYPED_TEST_SUITE(RequiredFieldsTest, MyTypes);',
|
|
'TYPED_TEST(RequiredFieldsTest, Empty) {',
|
|
' TestFixture::CheckRequired("{}");',
|
|
'}',
|
|
),
|
|
});
|
|
try {
|
|
const target = callable(cg, 'RequiredFieldsTest::CheckRequired');
|
|
expect(callsFrom(cg, 'upb/util/required_fields_test.cc')).toEqual([`CheckRequired @ ${target.filePath}:${target.startLine}`]);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('the one method of a name in the project', () => {
|
|
it('is still taken, as before: protobuf\'s MapTestPeer, declared in a `.inc` file that is not read as C++', async () => {
|
|
const cg = await indexed({
|
|
'src/google/protobuf/map.h': lines(
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'namespace internal {',
|
|
'class KeyMapBase {',
|
|
' public:',
|
|
' static unsigned long CalculateHiCutoff(unsigned long num_buckets) { return num_buckets * 3 / 4; }',
|
|
'};',
|
|
'} // namespace internal',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
// `map_test.inc` declares MapTestPeer, whose CalculateHiCutoff hands on to KeyMapBase's.
|
|
'src/google/protobuf/map_test.cc': lines(
|
|
'#include "google/protobuf/map.h"',
|
|
'#include "google/protobuf/map_test.inc"',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'namespace internal {',
|
|
'unsigned long HiCutoff(unsigned long capacity) {',
|
|
' return MapTestPeer::CalculateHiCutoff(capacity);',
|
|
'}',
|
|
'} // namespace internal',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
});
|
|
try {
|
|
const edges = cg.getCallees(callable(cg, 'google::protobuf::internal::HiCutoff').id).filter((r) => r.edge.kind === 'calls');
|
|
expect(edges.map((r) => r.node.qualifiedName)).toEqual(['google::protobuf::internal::KeyMapBase::CalculateHiCutoff']);
|
|
expect(edges[0]!.edge.metadata?.resolvedBy).toBe('instance-method');
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
});
|