mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
f7951a13db
protobuf declares its descriptor accessors in their classes and defines
them below with macros, like `PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor,
number, int)`; its reflection pastes `Reflection::Get##TYPENAME` in
another. The parser never sees those definitions (the preParse blanks the
invocations) and a declaration mints no node, so the methods had none and
every call to them linked to nothing.
- After either extractor (the kernel or the wasm walker), a step over the
original source expands the file's own function-like macros whose body
defines `Class::method(...) ... {`, at each invocation at namespace
scope, and mints a method node per definition: named and qualified like
an out-of-line definition, contained by the file and the same-file
class, with the expanded head as its signature. Constructors,
destructors and variadic macros are left out.
- The parse worker leaves the raw kernel fast path only for a file that
may define one (a cheap text test), so the step sees it.
- The C++ resolver reads such a method's return type and parameters from
its signature, and never takes it for a deleted overload.
A/B vs main on the kernel path, every changed site triaged: leveldb and
redis identical; fmt +13 nodes and rocksdb +11 (gtest's TestEventRepeater
overrides); protobuf +257 nodes, 6,203 calls and 2 references added, 252
calls retargeted, 11 metadata-only, none removed. Kernel and wasm dumps
are byte-identical, protobuf's one pre-existing Swift line aside.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
351 lines
14 KiB
TypeScript
351 lines
14 KiB
TypeScript
/**
|
|
* A C++ method a function-like macro defines, like protobuf's
|
|
* `PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, number, int)` expanding to
|
|
* `inline int FieldDescriptor::number() const { … }`, is a method node where
|
|
* the file invokes the macro. Its class only declares it (`int number()
|
|
* const;`), which mints no node, so it had none at all and no call to it
|
|
* linked.
|
|
*/
|
|
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 { extractFromSource } from '../src/extraction/tree-sitter';
|
|
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-macro-methods-'));
|
|
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@file:line`. */
|
|
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}@${r.node.filePath}:${r.node.startLine}`)
|
|
.sort();
|
|
}
|
|
|
|
/** The methods of a file, as `qualifiedName:line`. */
|
|
function methodsIn(cg: CodeGraph, file: string): string[] {
|
|
return cg.getNodesByKind('method').filter((n) => n.filePath === file).map((n) => `${n.qualifiedName}:${n.startLine}`).sort();
|
|
}
|
|
|
|
const lines = (...l: string[]): string => [...l, ''].join('\n');
|
|
|
|
/** protobuf's descriptor.h, in small: accessors declared in the class, defined by macros below it. */
|
|
const DESCRIPTOR_H = lines(
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'class FieldDescriptor;',
|
|
'class Descriptor {',
|
|
' public:',
|
|
' absl::string_view name() const;',
|
|
' absl::string_view full_name() const;',
|
|
' int field_count() const;',
|
|
' const FieldDescriptor* field(int index) const;',
|
|
' const Descriptor* containing_type() const;',
|
|
' private:',
|
|
' int field_count_;',
|
|
' const FieldDescriptor* fields_;',
|
|
' const Descriptor* containing_type_;',
|
|
'};',
|
|
'class FieldDescriptor {',
|
|
' public:',
|
|
' int number() const;',
|
|
' const Descriptor* containing_type() const;',
|
|
' private:',
|
|
' int number_;',
|
|
' const Descriptor* containing_type_;',
|
|
'};',
|
|
'',
|
|
'#define PROTOBUF_DEFINE_ACCESSOR(CLASS, FIELD, TYPE) \\',
|
|
' inline TYPE CLASS::FIELD() const { return FIELD##_; }',
|
|
'',
|
|
'// Name and full name are stored in a single array to save space.',
|
|
'#define PROTOBUF_DEFINE_NAME_ACCESSOR(CLASS) \\',
|
|
' inline absl::string_view CLASS::name() const { return all_names_.name(); } \\',
|
|
' inline absl::string_view CLASS::full_name() const { \\',
|
|
' return all_names_.full_name(); \\',
|
|
' }',
|
|
'',
|
|
'#define PROTOBUF_DEFINE_ARRAY_ACCESSOR(CLASS, FIELD, TYPE) \\',
|
|
' inline TYPE CLASS::FIELD(int index) const { \\',
|
|
' return FIELD##s_ + index; \\',
|
|
' }',
|
|
'',
|
|
'PROTOBUF_DEFINE_NAME_ACCESSOR(Descriptor)',
|
|
'PROTOBUF_DEFINE_ACCESSOR(Descriptor, field_count, int)',
|
|
'PROTOBUF_DEFINE_ARRAY_ACCESSOR(Descriptor, field, const FieldDescriptor*)',
|
|
'PROTOBUF_DEFINE_ACCESSOR(Descriptor, containing_type,',
|
|
' const Descriptor*)',
|
|
'PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, number, int)',
|
|
'PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, containing_type, const Descriptor*)',
|
|
'',
|
|
'#undef PROTOBUF_DEFINE_ACCESSOR',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
);
|
|
|
|
describe('a C++ method a macro defines', () => {
|
|
it('reproduction: protobuf descriptor accessors, and calls through what they return', async () => {
|
|
const cg = await indexed({
|
|
// A namesake of the class an accessor returns: the type is read as written.
|
|
'src/legacy.h': lines(
|
|
'namespace legacy {',
|
|
'class Descriptor {',
|
|
' public:',
|
|
' absl::string_view full_name() const { return ""; }',
|
|
'};',
|
|
'} // namespace legacy',
|
|
),
|
|
'src/descriptor.h': DESCRIPTOR_H,
|
|
'src/printer.cc': lines(
|
|
'#include "src/legacy.h"',
|
|
'#include "src/descriptor.h"',
|
|
'namespace google {',
|
|
'namespace protobuf {',
|
|
'int Number(const FieldDescriptor* field) { return field->number(); }',
|
|
'int Count(const Descriptor& d) { return d.field_count(); }',
|
|
'absl::string_view Owner(const FieldDescriptor* field) { return field->containing_type()->full_name(); }',
|
|
'int First(const Descriptor* d) { return d->field(0)->number(); }',
|
|
'absl::string_view Parent(const FieldDescriptor* field) {',
|
|
' auto parent = field->containing_type();',
|
|
' return parent->full_name();',
|
|
'}',
|
|
'} // namespace protobuf',
|
|
'} // namespace google',
|
|
),
|
|
});
|
|
try {
|
|
expect(methodsIn(cg, 'src/descriptor.h')).toEqual([
|
|
'google::protobuf::Descriptor::containing_type:43',
|
|
'google::protobuf::Descriptor::field:42',
|
|
'google::protobuf::Descriptor::field_count:41',
|
|
'google::protobuf::Descriptor::full_name:40',
|
|
'google::protobuf::Descriptor::name:40',
|
|
'google::protobuf::FieldDescriptor::containing_type:46',
|
|
'google::protobuf::FieldDescriptor::number:45',
|
|
]);
|
|
expect(calls(cg, 'google::protobuf::Number')).toEqual(['google::protobuf::FieldDescriptor::number@src/descriptor.h:45']);
|
|
expect(calls(cg, 'google::protobuf::Count')).toEqual(['google::protobuf::Descriptor::field_count@src/descriptor.h:41']);
|
|
expect(calls(cg, 'google::protobuf::Owner')).toEqual([
|
|
'google::protobuf::Descriptor::full_name@src/descriptor.h:40',
|
|
'google::protobuf::FieldDescriptor::containing_type@src/descriptor.h:46',
|
|
]);
|
|
expect(calls(cg, 'google::protobuf::First')).toEqual([
|
|
'google::protobuf::Descriptor::field@src/descriptor.h:42',
|
|
'google::protobuf::FieldDescriptor::number@src/descriptor.h:45',
|
|
]);
|
|
expect(calls(cg, 'google::protobuf::Parent')).toEqual([
|
|
'google::protobuf::Descriptor::full_name@src/descriptor.h:40',
|
|
'google::protobuf::FieldDescriptor::containing_type@src/descriptor.h:46',
|
|
]);
|
|
const number = callable(cg, 'google::protobuf::FieldDescriptor::number');
|
|
expect(number.signature).toBe('inline int FieldDescriptor::number() const');
|
|
expect(number.returnType).toBeUndefined();
|
|
expect(callable(cg, 'google::protobuf::FieldDescriptor::containing_type').returnType).toBe('Descriptor');
|
|
const holders = cg.getIncomingEdgesTo([number.id], ['contains']);
|
|
expect(holders.map((e) => cg.getNode(e.source)?.qualifiedName).sort()).toEqual(['google::protobuf::FieldDescriptor', 'src/descriptor.h']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('a macro of a source file that pastes the method name, for a class its header declares', async () => {
|
|
const cg = await indexed({
|
|
'src/reflection.h': lines(
|
|
'class Reflection {',
|
|
' public:',
|
|
' int GetInt32(int index) const;',
|
|
' void SetInt32(int index, int value) const;',
|
|
' bool GetBool(int index) const;',
|
|
' void SetBool(int index, bool value) const;',
|
|
'};',
|
|
),
|
|
'src/reflection.cc': lines(
|
|
'#include "src/reflection.h"',
|
|
'#define DEFINE_PRIMITIVE_ACCESSORS(TYPENAME, TYPE) \\',
|
|
' TYPE Reflection::Get##TYPENAME(int index) const { \\',
|
|
' return TYPE(); \\',
|
|
' } \\',
|
|
' void Reflection::Set##TYPENAME(int index, TYPE value) const {}',
|
|
'',
|
|
'DEFINE_PRIMITIVE_ACCESSORS(Int32, int)',
|
|
'DEFINE_PRIMITIVE_ACCESSORS(Bool, bool)',
|
|
'#undef DEFINE_PRIMITIVE_ACCESSORS',
|
|
'namespace {',
|
|
'class Local {',
|
|
' public:',
|
|
' int Get() const;',
|
|
'};',
|
|
'#define DEFINE_LOCAL_GET(C) inline int C::Get() const { return 1; }',
|
|
'DEFINE_LOCAL_GET(Local)',
|
|
'} // namespace',
|
|
),
|
|
'src/use.cc': lines(
|
|
'#include "src/reflection.h"',
|
|
'int Read(const Reflection& r) { r.SetBool(0, true); return r.GetInt32(1); }',
|
|
),
|
|
});
|
|
try {
|
|
// An anonymous namespace qualifies nothing, as for a definition written out.
|
|
expect(methodsIn(cg, 'src/reflection.cc')).toEqual([
|
|
'Local::Get:17',
|
|
'Reflection::GetBool:9',
|
|
'Reflection::GetInt32:8',
|
|
'Reflection::SetBool:9',
|
|
'Reflection::SetInt32:8',
|
|
]);
|
|
expect(calls(cg, 'Read')).toEqual(['Reflection::GetInt32@src/reflection.cc:8', 'Reflection::SetBool@src/reflection.cc:9']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('a macro-defined overload is picked by the parameters its expansion writes, and is never a deleted one', async () => {
|
|
const cg = await indexed({
|
|
'src/getter.h': lines(
|
|
'class Getter {',
|
|
' public:',
|
|
' int Get(int index) const;',
|
|
' int Get(const char* key) const;',
|
|
'};',
|
|
'#define DEFINE_GET(PARAM) \\',
|
|
' inline int Getter::Get(PARAM) const { return 0; }',
|
|
'DEFINE_GET(int index)',
|
|
'// Between two invocations, another class deletes a method of the same name.',
|
|
'class Blocked {',
|
|
' public:',
|
|
' int Get(double value) = delete;',
|
|
'};',
|
|
'DEFINE_GET(const char* key)',
|
|
),
|
|
'src/use.cc': lines(
|
|
'#include "src/getter.h"',
|
|
'int ByKey(const Getter& g) { return g.Get("key"); }',
|
|
'int ByIndex(const Getter& g) { return g.Get(1); }',
|
|
),
|
|
});
|
|
try {
|
|
expect(calls(cg, 'ByKey')).toEqual(['Getter::Get@src/getter.h:14']);
|
|
expect(calls(cg, 'ByIndex')).toEqual(['Getter::Get@src/getter.h:8']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('an explicit specialization a macro defines is named without its template arguments', async () => {
|
|
const cg = await indexed({
|
|
'src/wire.h': lines(
|
|
'class Wire {',
|
|
' public:',
|
|
' template <typename T, int Kind> static bool Read(T* value);',
|
|
'};',
|
|
'#define READ_PRIMITIVE(CPPTYPE, KIND) \\',
|
|
' template <> inline bool Wire::Read<CPPTYPE, KIND>(CPPTYPE* value) { return true; }',
|
|
'READ_PRIMITIVE(int, 1)',
|
|
'READ_PRIMITIVE(double, 2)',
|
|
),
|
|
'src/use.cc': lines(
|
|
'#include "src/wire.h"',
|
|
'bool Use(int* value) { return Wire::Read<int, 1>(value); }',
|
|
),
|
|
});
|
|
try {
|
|
expect(methodsIn(cg, 'src/wire.h')).toEqual(['Wire::Read:7', 'Wire::Read:8']);
|
|
expect(calls(cg, 'Use')).toHaveLength(1);
|
|
expect(calls(cg, 'Use')[0]).toMatch(/^Wire::Read@src\/wire\.h:[78]$/);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('what is not a method a macro defines', async () => {
|
|
const cg = await indexed({
|
|
'src/other.h': lines(
|
|
'class Widget {',
|
|
' public:',
|
|
' Widget();',
|
|
' void Draw() const;',
|
|
' void Paint() const;',
|
|
' int Size() const;',
|
|
' static void Log(int x);',
|
|
'};',
|
|
'/*',
|
|
'#define COMMENTED(C) inline void C::Draw() const {}',
|
|
'*/',
|
|
'#define DEFINE_DRAW(C) inline void C::Draw() const {}',
|
|
'#define DEFINE_CTOR(C) inline C::C() {}',
|
|
'#define DEFINE_FREE(NAME) inline void NAME##_fn() {}',
|
|
'#define CALL_LOG(x) Widget::Log(x);',
|
|
'#define DECLARE_SIZE(C) int C::Size() const;',
|
|
'COMMENTED(Widget)',
|
|
'DEFINE_CTOR(Widget)',
|
|
'DEFINE_FREE(widget)',
|
|
'DECLARE_SIZE(Widget)',
|
|
'inline void Use() {',
|
|
' DEFINE_DRAW(Widget)',
|
|
' CALL_LOG(1)',
|
|
'}',
|
|
'struct Holder {',
|
|
' DEFINE_DRAW(Widget)',
|
|
'};',
|
|
'#undef DEFINE_DRAW',
|
|
'DEFINE_DRAW(Widget)',
|
|
'#define DEFINE_DRAW(C) inline void C::Paint() const {}',
|
|
'DEFINE_DRAW(Widget)',
|
|
),
|
|
});
|
|
try {
|
|
// Only the invocation of the redefined macro, at namespace scope, defines one.
|
|
expect(methodsIn(cg, 'src/other.h')).toEqual(['Widget::Paint:31', 'Widget::Widget:3']);
|
|
} finally {
|
|
cg.close();
|
|
}
|
|
});
|
|
|
|
it('is minted alike whether the kernel or the wasm extractor reads the file', () => {
|
|
const minted = (kernel: boolean): string[] => {
|
|
const saved = process.env.CODEGRAPH_KERNEL;
|
|
if (kernel) delete process.env.CODEGRAPH_KERNEL;
|
|
else process.env.CODEGRAPH_KERNEL = '0';
|
|
try {
|
|
const result = extractFromSource('src/descriptor.h', DESCRIPTOR_H, 'cpp');
|
|
const byId = new Map(result.nodes.map((n) => [n.id, n]));
|
|
return result.nodes
|
|
.filter((n) => n.kind === 'method' && n.signature?.includes('::'))
|
|
.map((n) => {
|
|
const holders = result.edges.filter((e) => e.kind === 'contains' && e.target === n.id).map((e) => byId.get(e.source)?.qualifiedName);
|
|
return `${n.id} ${n.qualifiedName} ${n.startLine}:${n.startColumn}-${n.endLine}:${n.endColumn} ${n.returnType ?? '-'} ${n.signature} <- ${holders.sort().join(',')}`;
|
|
})
|
|
.sort();
|
|
} finally {
|
|
if (saved === undefined) delete process.env.CODEGRAPH_KERNEL;
|
|
else process.env.CODEGRAPH_KERNEL = saved;
|
|
}
|
|
};
|
|
const wasm = minted(false);
|
|
expect(wasm).toHaveLength(7);
|
|
expect(minted(true)).toEqual(wasm);
|
|
});
|
|
});
|