Files
codegraph/__tests__/cpp-macro-defined-methods.test.ts
Colby Mchenry f7951a13db fix(cpp): a method a macro of its file defines, like protobuf's PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, number, int), is indexed where the macro is invoked (#2605)
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>
2026-10-16 00:53:01 +00:00

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);
});
});