Files
codegraph/__tests__/cpp-macro-and-constructor-calls.test.ts
Colby Mchenry 15a5d1d0cd fix(c): keep an #undef under an undecidable #if unknown; a macro call never binds to a type (#2069, #2070) (#2081)
Two follow-ups to the unreleased macro-visibility work (#1838/#1839):

- #2069: `#undef FLAG` under an `#if` nothing in the source decides made a
  never-seen FLAG certainly undefined, so `#ifdef FLAG` was read as false
  and the functions it guards lost their callers (CPython's
  `Py_GIL_DISABLED`). A name no directive has touched is now unknown; only
  an #undef on a certain line clears it.
- #2070: dropping the macro constant from a call's candidates let the next
  same-named node win, so expat's `PREFIX(scanRef)(...)` instantiated an
  unrelated `struct PREFIX`. When the name is a function-like macro in the
  project, only a function or method is an eligible callee.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:46:41 +00:00

620 lines
26 KiB
TypeScript

/**
* C/C++ call-graph fidelity for two call-less shapes (#1838, #1839):
*
* - a function-like macro invocation (`TRACE_POINT(1)`) whose macro is
* visible in the translation unit — its own `#define`s or an in-repo
* include — never binds to a same-named free function elsewhere, and the
* macro itself is never a callee;
* - local object initialization (`T obj;` / `T obj(args)` / `T obj{args}`)
* calls the constructor method, chosen in the lexical namespace of the
* site and, among overloads, by arity when exactly one admits the
* argument count — never the class node, and nothing for an aggregate.
*
* Both issues' reproductions are indexed verbatim; the negative controls pin
* that real calls and declarations that construct nothing are untouched.
*/
import { execFileSync } from 'child_process';
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 { ToolHandler } from '../src/mcp/tools';
import { extractFromSource } from '../src/extraction';
import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
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-calls-'));
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 fn(cg: CodeGraph, name: string): Node {
const node = cg.getNodesByKind('function').find((n) => n.name === name);
if (!node) throw new Error(`no function ${name}`);
return node;
}
/** `calls` callees of a function, as `kind qualifiedName (file)`. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(fn(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => `${r.node.kind} ${r.node.qualifiedName} (${r.node.filePath})`)
.sort();
}
describe('#1838 — a macro invocation is not a call to a same-named function', () => {
it('reproduction: the macro from an included header does not bind to the decoy function', async () => {
const cg = await indexed({
'marker.hpp': '#define TRACE_POINT(value) ((void)(value))\n',
'exercise.cpp': '#include "marker.hpp"\n\nvoid exercise() {\n TRACE_POINT(1);\n}\n',
'macro_decoy.cpp': 'void TRACE_POINT(int value) {}\n',
});
try {
expect(calls(cg, 'exercise')).toEqual([]);
const decoy = fn(cg, 'TRACE_POINT');
expect(cg.getCallers(decoy.id)).toEqual([]);
} finally {
cg.close();
}
});
it.each(['c', 'cpp'] as const)(
'suppresses macros visible through nested includes, a sibling header, a local #define; honors #undef (%s)',
async (language) => {
const cg = await indexed({
'src/pg/pinio.h': '#define HEADER_TRACE(v) ((void)(v))\n',
'src/drivers/pinio.h': '// unrelated header with the same basename\n',
[`src/pg/pinio.${language}`]: '#include "pinio.h"\nvoid sibling_header_use() { HEADER_TRACE(1); }\n',
'inner.h': '#define TRACE_POINT(value) ((void)(value))\n',
'outer.h': '#include "inner.h"\n',
[`exercise.${language}`]: [
'#include "outer.h"',
'void macro_use() { TRACE_POINT(1); }',
'void local_macro() {',
'#define INNER_TRACE(v) ((void)(v))',
'INNER_TRACE(1);',
'}',
'#undef TRACE_POINT',
'void after_undef() { TRACE_POINT(1); }',
'',
].join('\n'),
[`decoy.${language}`]: [
'void HEADER_TRACE(int value) {}',
'void INNER_TRACE(int value) {}',
'void TRACE_POINT(int value) {}',
'void unrelated_use() { TRACE_POINT(1); }',
'',
].join('\n'),
});
try {
expect(calls(cg, 'macro_use')).toEqual([]);
expect(calls(cg, 'sibling_header_use')).toEqual([]);
expect(calls(cg, 'local_macro')).toEqual([]);
// After `#undef`, and in a file that never sees the macro, the call is real.
expect(calls(cg, 'after_undef')).toEqual([`function TRACE_POINT (decoy.${language})`]);
expect(calls(cg, 'unrelated_use')).toEqual([`function TRACE_POINT (decoy.${language})`]);
} finally {
cg.close();
}
}
);
it.each(['c', 'cpp'] as const)('replays unguarded includes and respects guards, pragma once and changed flags (%s)', async (language) => {
const cg = await indexed({
'unguarded.h': '#define TRACE(v) ((void)(v))\n',
'guarded.h': '#ifndef GUARDED_H\n#define GUARDED_H\n#define GUARDED(v) ((void)(v))\n#endif\n',
'once.h': '#pragma once\n#define ONCE(v) ((void)(v))\n',
'conditional.h': '#if ENABLE_TRACE\n#define CONDITIONAL(v) ((void)(v))\n#endif\n',
[`unit.${language}`]: [
'#include "unguarded.h"', '#undef TRACE',
'void between() { TRACE(1); }',
'#include "unguarded.h"', 'void repeated() { TRACE(1); }',
'#include "guarded.h"', '#undef GUARDED', '#include "guarded.h"',
'void guarded() { GUARDED(1); }',
'#undef GUARDED_H', '#include "guarded.h"', 'void reset_guard() { GUARDED(1); }',
'#include "once.h"', '#undef ONCE', '#include "once.h"', 'void once() { ONCE(1); }',
'#define ENABLE_TRACE 0', '#include "conditional.h"',
'#undef ENABLE_TRACE', '#define ENABLE_TRACE 1', '#include "conditional.h"',
'void changed_flag() { CONDITIONAL(1); }', '',
].join('\n'),
[`decoy.${language}`]: 'void TRACE(int x) {}\nvoid GUARDED(int x) {}\nvoid ONCE(int x) {}\nvoid CONDITIONAL(int x) {}\n',
});
try {
expect(calls(cg, 'between')).toEqual([`function TRACE (decoy.${language})`]);
expect(calls(cg, 'repeated')).toEqual([]);
expect(calls(cg, 'guarded')).toEqual([`function GUARDED (decoy.${language})`]);
expect(calls(cg, 'reset_guard')).toEqual([]);
expect(calls(cg, 'once')).toEqual([`function ONCE (decoy.${language})`]);
expect(calls(cg, 'changed_flag')).toEqual([]);
} finally { cg.close(); }
});
it('a diamond-shaped, cyclic include graph still finds the macro on an unconditional path', async () => {
// top.h includes left.h and right.h; both include shared.h (no include
// guard), which includes top.h again. The active include stack breaks the
// cycle while shared.h is replayed on the unconditional path.
const cg = await indexed({
'top.h': '#include "left.h"\n#include "right.h"\n',
'left.h': '#ifdef USE_LEFT\n#include "shared.h"\n#endif\n',
'right.h': '#include "shared.h"\n',
'shared.h': '#include "top.h"\n#define SHARED_TRACE(v) ((void)(v))\n',
'unit.cpp': '#include "top.h"\nvoid unit() { SHARED_TRACE(1); }\n',
'decoy.cpp': 'void SHARED_TRACE(int value) {}\n',
});
try {
expect(calls(cg, 'unit')).toEqual([]);
} finally {
cg.close();
}
});
it('control: a #define inside a block comment, string or raw literal is not a macro', async () => {
const cg = await indexed({
'doc.hpp': '/*\n * Example:\n * #define helper(x) ((x) + 1)\n */\nconst char *usage = "/* #define helper(x) */";\nconst char *raw = R"doc(\n#define helper(x) ((x) + 9)\n)doc";\n',
'lib.cpp': '#include "doc.hpp"\nint helper(int x) { return x + 1; }\nint run() { return helper(1); }\n',
'other.cpp': '#define helper(x) ((x) + 2)\n',
});
try {
expect(calls(cg, 'run')).toEqual(['function helper (lib.cpp)']);
} finally {
cg.close();
}
});
it('control: a wrapper macro whose body calls the same-named function keeps that call', async () => {
const cg = await indexed({
'vec.c': [
'static void vec_splice(char **data, int start, int count) {}',
'#define vec_splice(v, start, count)\\',
' ( vec_splice((char **)(v), start, count),\\',
' (v)->length -= (count) )',
'struct buf { char *data; int length; };',
'void flush(struct buf *b) { vec_splice(b, 0, 1); }',
'',
].join('\n'),
});
try {
expect(calls(cg, 'flush')).toEqual(['function vec_splice (vec.c)']);
} finally {
cg.close();
}
});
it('control: a macro defined only in an unrelated file neither suppresses nor receives a real call', async () => {
const cg = await indexed({
'unrelated.hpp': '#define helper(x) ((x) + 1)\n',
'lib.cpp': 'int helper(int x) { return x + 1; }\n',
'main.cpp': 'int helper(int x);\nint run() { return helper(1); }\n',
});
try {
expect(calls(cg, 'run')).toEqual(['function helper (lib.cpp)']);
} finally {
cg.close();
}
});
it('control: a call above the #define, or under a known-false branch, is still a call', async () => {
const cg = await indexed({
'maths.h': [
'#define FAST_MATH',
'#if defined(FAST_MATH)',
'float sin_approx(float x);',
'#else',
'#define sin_approx(x) external_sin(x)',
'#endif',
'',
].join('\n'),
'maths.cpp': [
'#include "maths.h"',
'float sin_approx(float x) { return x; }',
'void real_fn(int value) {}',
'float invoke_real(float value) { real_fn(1); return sin_approx(value); }',
'#define real_fn(x) ((void)(x))',
'',
].join('\n'),
});
try {
expect(calls(cg, 'invoke_real')).toEqual([
'function real_fn (maths.cpp)',
'function sin_approx (maths.cpp)',
]);
} finally {
cg.close();
}
});
it.each(['c', 'cpp'] as const)('extraction: a function-like macro is a constant carrying its directive (%s)', async (language) => {
await initGrammars();
await loadGrammarsForLanguages([language]);
const result = extractFromSource(
`marker.${language}`,
'#define TRACE_POINT(value) ((void)(value))\n#define VERSION 1\nvoid f() { TRACE_POINT(VERSION); }\n',
language
);
const constants = result.nodes.filter((n) => n.kind === 'constant');
expect(constants.map((n) => [n.name, n.signature])).toEqual([
['TRACE_POINT', '#define TRACE_POINT(value) ((void)(value))'],
]);
// The call ref is still recorded (the resolver decides), the macro is not a function.
expect(result.nodes.some((n) => n.kind === 'function' && n.name === 'TRACE_POINT')).toBe(false);
expect(result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName)).toEqual(['TRACE_POINT']);
});
});
describe('#1839 — local object initialization calls the constructor, not the type', () => {
const REPRODUCTION = [
'struct Aggregate {',
' int value;',
'};',
'',
'class WithConstructor {',
'public:',
' WithConstructor();',
' explicit WithConstructor(int value);',
'};',
'',
'WithConstructor::WithConstructor() {}',
'WithConstructor::WithConstructor(int value) {}',
'',
'void aggregate_initialization() {',
' Aggregate item{};',
'}',
'',
'void constructor_default() {',
' WithConstructor item;',
'}',
'',
'void constructor_braced() {',
' WithConstructor item{};',
'}',
'',
'void constructor_value() {',
' WithConstructor item(1);',
'}',
'',
'void constructor_temporary() {',
' WithConstructor();',
'}',
'',
].join('\n');
it('reproduction: every initialization form reaches the indexed constructor; the aggregate reaches none', async () => {
const cg = await indexed({ 'case.cpp': REPRODUCTION });
try {
expect(calls(cg, 'aggregate_initialization')).toEqual([]);
const ctorNodes = cg.getNodesByKind('method').filter((n) => n.qualifiedName === 'WithConstructor::WithConstructor');
expect(ctorNodes.map((n) => n.signature).sort()).toEqual(['()', '();', '(int value)', '(int value);']);
const bySignature = (caller: string) =>
cg
.getCallees(fn(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => `${r.node.kind} ${r.node.qualifiedName}${r.node.signature}`);
expect(bySignature('constructor_default')).toEqual(['method WithConstructor::WithConstructor()']);
expect(bySignature('constructor_braced')).toEqual(['method WithConstructor::WithConstructor()']);
expect(bySignature('constructor_value')).toEqual(['method WithConstructor::WithConstructor(int value)']);
// The temporary already resolved to a constructor before (#1839); which
// overload a plain `T()` call picks is the generic name matcher's choice.
expect(bySignature('constructor_temporary').every((c) => c.startsWith('method WithConstructor::WithConstructor('))).toBe(true);
// No `calls` edge ever targets the class or struct node.
for (const caller of ['aggregate_initialization', 'constructor_default', 'constructor_braced', 'constructor_value', 'constructor_temporary']) {
const kinds = cg
.getCallees(fn(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => r.node.kind);
expect(kinds, caller).not.toContain('class');
expect(kinds, caller).not.toContain('struct');
}
} finally {
cg.close();
}
});
it('picks the constructor in the lexical namespace of the site, then an enclosing/global type', async () => {
const cg = await indexed({
'ns.cpp': [
'struct Global { Global() {} };',
'union Value { Value() {} int x; };',
'void union_use() { Value v; }',
'namespace first { struct Widget { Widget() {} }; }',
'namespace second {',
' struct Widget { Widget() {} };',
' void local_use() { Widget w; }',
' void global_use() { Global g; }',
'}',
'void explicit_use() { first::Widget w; }',
'',
].join('\n'),
});
try {
expect(calls(cg, 'union_use')).toEqual(['method Value::Value (ns.cpp)']);
expect(calls(cg, 'local_use')).toEqual(['method second::Widget::Widget (ns.cpp)']);
expect(calls(cg, 'global_use')).toEqual(['method Global::Global (ns.cpp)']);
expect(calls(cg, 'explicit_use')).toEqual(['method first::Widget::Widget (ns.cpp)']);
} finally {
cg.close();
}
});
it('chooses among overloads by arity only when exactly one admits the argument count', async () => {
const cg = await indexed({
'overloads.cpp': [
// (One constructor per line: same-line overloads share a node ID.)
'struct Widget {',
' Widget() {}',
' Widget(int value) {}',
' Widget(int a, int b = 2) {}',
'};',
'struct Ambiguous {',
' Ambiguous(int) {}',
' Ambiguous(double) {}',
'};',
'void default_use() { Widget w; }',
'void two_use() { Widget w(1, 2); }',
'void one_use() { Widget w(1); }',
'void ambiguous_use(int value) { Ambiguous w(value); }',
'',
].join('\n'),
});
try {
const signatures = (caller: string) =>
cg
.getCallees(fn(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => r.node.signature);
expect(signatures('default_use')).toEqual(['()']);
expect(signatures('two_use')).toEqual(['(int a, int b = 2)']);
// `Widget(int)` and `Widget(int, int = 2)` both admit one argument.
expect(signatures('one_use')).toEqual([]);
expect(signatures('ambiguous_use')).toEqual([]);
} finally {
cg.close();
}
});
it('controls: pointers, references, prototypes and extern declarations construct no object; arrays construct elements', async () => {
const cg = await indexed({
'controls.cpp': [
'struct Widget {',
' Widget() {}',
' Widget(int) {}',
'};',
'void pointers() { Widget *p{}; Widget *q(nullptr); Widget *arr[2]{}; Widget (*fn)(){}; }',
'void reference_bind(Widget &other) { Widget &r{other}; Widget &s(other); }',
'void prototype() { Widget most_vexing(); extern Widget external; }',
'void array() { Widget items[2]{}; }',
'void actual() { Widget object{}; }',
'',
].join('\n'),
});
try {
expect(calls(cg, 'pointers')).toEqual([]);
expect(calls(cg, 'reference_bind')).toEqual([]);
expect(calls(cg, 'prototype')).toEqual([]);
expect(calls(cg, 'array')).toEqual(['method Widget::Widget (controls.cpp)']);
expect(calls(cg, 'actual')).toEqual(['method Widget::Widget (controls.cpp)']);
} finally {
cg.close();
}
});
it('merges separately declared defaults with definitions without confusing same-arity overloads', async () => {
const cg = await indexed({
'widget.hpp': [
'namespace app {', 'struct Widget {',
' Widget(int value = 7);', ' Widget(double value);', '};',
'struct Pair { Pair(int first, int second = 2); };', '}', '',
].join('\n'),
'widget.cpp': '#include "widget.hpp"\napp::Widget::Widget(int renamed) {}\napp::Widget::Widget(double renamed) {}\napp::Pair::Pair(int a, int b) {}\n',
'use.cpp': '#include "widget.hpp"\nint argument(int value) { return value; }\nvoid defaults() { app::Widget item; }\nvoid nested() { app::Pair item(argument(1)); }\nvoid ambiguous() { app::Widget item(1); }\n',
});
try {
expect(calls(cg, 'defaults')).toEqual(['method app::Widget::Widget (widget.cpp)']);
expect(cg.getCallees(fn(cg, 'defaults').id).find((r) => r.edge.kind === 'calls')?.node.signature).toBe('(int renamed)');
expect(calls(cg, 'nested')).toEqual(['function argument (use.cpp)', 'method app::Pair::Pair (widget.cpp)']);
expect(calls(cg, 'ambiguous')).toEqual([]);
const ctor = cg.getCallees(fn(cg, 'defaults').id).find((r) => r.edge.kind === 'calls')!.node;
expect(cg.getCallers(ctor.id).map((r) => r.node.name)).toContain('defaults');
} finally { cg.close(); }
});
it('constructs default and explicitly braced array elements, retaining nested argument calls', async () => {
const cg = await indexed({ 'arrays.cpp': [
'struct Widget {', ' Widget() {}', ' Widget(int value) {}', '};',
'int argument() { return 1; }',
'void plain() { Widget items[2]; }',
'void empty() { Widget items[2]{}; }',
'void elements() { Widget items[3]{{argument()}, {2}}; }',
'void grid() { Widget items[2][2]{{{1}, {2}}, {{3}}}; }',
'void scalar_elements() { Widget items[2]{1, 2}; }', '',
].join('\n') });
try {
for (const name of ['plain', 'empty']) {
expect(cg.getCallees(fn(cg, name).id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.signature)).toEqual(['()']);
}
expect(cg.getCallees(fn(cg, 'grid').id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.signature).sort()).toEqual(['()', '(int value)']);
expect(cg.getCallees(fn(cg, 'scalar_elements').id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.signature)).toEqual(['(int value)']);
expect(cg.getCallees(fn(cg, 'elements').id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.signature ?? r.node.name).sort())
.toEqual(['()', '(int value)', 'argument']);
} finally { cg.close(); }
});
it('MCP preserves constructor retrieval and labels the retained type dependency', async () => {
const cg = await indexed({ 'case.cpp': REPRODUCTION });
try {
const handler = new ToolHandler(cg);
const result = await handler.execute('codegraph_callees', { symbol: 'constructor_braced' });
expect(result.isError).not.toBe(true);
const text = result.content?.[0]?.text ?? '';
expect(text).toContain('WithConstructor (method)');
expect(text).toContain('via instantiation');
const owner = cg.getNodesByKind('class').find((n) => n.name === 'WithConstructor')!;
expect([...cg.getImpactRadius(owner.id, 2).nodes.values()].map((n) => n.name)).toContain('constructor_braced');
const ctor = cg.getCallees(fn(cg, 'constructor_braced').id).find((r) => r.edge.kind === 'calls')!.node;
expect([...cg.getImpactRadius(ctor.id, 2).nodes.values()].map((n) => n.name)).toContain('constructor_braced');
} finally { cg.close(); }
});
it('CLI labels type dependencies separately from executable constructor calls', async () => {
const cg = await indexed({ 'case.cpp': REPRODUCTION });
const root = cg.getProjectRoot();
cg.close();
const cli = (symbol: string, json = false) => execFileSync(process.execPath,
[path.resolve(__dirname, '../dist/bin/codegraph.js'), 'callees', symbol, '-p', root, ...(json ? ['-j'] : [])],
{ encoding: 'utf8', env: { ...process.env, CODEGRAPH_NO_WATCH: '1', CODEGRAPH_NO_DAEMON: '1', NO_COLOR: '1' } });
const aggregate = JSON.parse(cli('aggregate_initialization', true));
expect(aggregate.callees).toEqual([expect.objectContaining({ kind: 'struct', relationships: ['instantiates'] })]);
const constructed = JSON.parse(cli('constructor_braced', true));
expect(constructed.callees).toEqual(expect.arrayContaining([
expect.objectContaining({ kind: 'class', relationships: ['instantiates'] }),
expect.objectContaining({ kind: 'method', relationships: ['calls'] }),
]));
expect(cli('constructor_braced')).toContain('WithConstructor [instantiates]');
});
it('extraction: one constructor ref per declarator with its own arity; the #1035 instantiates ref is kept', async () => {
await initGrammars();
await loadGrammarsForLanguages(['cpp']);
const result = extractFromSource(
'case.cpp',
'struct Widget { Widget() {} Widget(int value) {} };\nvoid run() { Widget a, b(1), c{}; ns::Other d{1, 2}; Widget *p{}; }\n',
'cpp'
);
expect(result.nodes.filter((n) => n.kind === 'method').map((n) => n.signature)).toEqual(['()', '(int value)']);
const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls' || r.referenceKind === 'instantiates');
expect(refs.map((r) => `${r.referenceKind} ${r.referenceName}`)).toEqual([
'instantiates Widget',
'calls Widget::Widget/0',
'calls Widget::Widget/1',
'calls Widget::Widget/0',
'instantiates Other',
'calls ns::Other::Other/2',
]);
});
});
/** Every outgoing edge of a function, as `edgeKind kind qualifiedName (file)`. */
function edges(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(fn(cg, caller).id)
.map((r) => `${r.edge.kind} ${r.node.kind} ${r.node.qualifiedName} (${r.node.filePath})`)
.sort();
}
describe('#2069 — an #undef under an undecidable #if leaves a never-seen flag unknown', () => {
it.each(['c', 'cpp'] as const)('reproduction: the #ifdef branch after it stays possible (%s)', async (language) => {
const cg = await indexed({
'config.h': '#if FREE_THREADED == 0\n#undef FREE_THREADED\n#endif\n',
[`unit.${language}`]: [
'#include "config.h"',
'#ifdef FREE_THREADED',
'static int world_stopped(void) { return 0; }',
'#else',
'#define world_stopped() 1',
'#endif',
'int check(void) { return world_stopped(); }',
'',
].join('\n'),
});
try {
expect(calls(cg, 'check')).toEqual([`function world_stopped (unit.${language})`]);
} finally {
cg.close();
}
});
it.each(['c', 'cpp'] as const)('control: an #undef on a certain line still makes the flag certainly undefined (%s)', async (language) => {
const cg = await indexed({
'config.h': '#undef TOP_LEVEL\n#if 1\n#undef TRUE_BRANCH\n#endif\n',
[`unit.${language}`]: [
'#include "config.h"',
'#ifdef TOP_LEVEL',
'#else',
'#define top_hook() 1',
'#endif',
'#ifdef TRUE_BRANCH',
'#else',
'#define branch_hook() 1',
'#endif',
'int use_top(void) { return top_hook(); }',
'int use_branch(void) { return branch_hook(); }',
'',
].join('\n'),
[`decoy.${language}`]: 'int top_hook(void) { return 0; }\nint branch_hook(void) { return 0; }\n',
});
try {
expect(calls(cg, 'use_top')).toEqual([]);
expect(calls(cg, 'use_branch')).toEqual([]);
} finally {
cg.close();
}
});
});
describe('#2070 — a call through a function-like macro never binds to a same-named type', () => {
it.each(['c', 'cpp'] as const)('reproduction: PREFIX(scan)(x) reaches no struct PREFIX, in another file or an included header (%s)', async (language) => {
const cg = await indexed({
[`unit.${language}`]: [
'#ifdef TOK_IMPL',
'#define PREFIX(ident) ident',
'#endif',
'int tok(int x) { return PREFIX(scan)(x); }',
'',
].join('\n'),
[`parser.${language}`]: 'struct PREFIX { int x; };\n',
'types.h': 'struct WRAP { int x; };\n',
[`wrapped.${language}`]: [
'#include "types.h"',
'#ifdef WRAP_IMPL',
'#define WRAP(ident) ident',
'#endif',
'int wrapped(int x) { return WRAP(scan)(x); }',
'',
].join('\n'),
});
try {
expect(edges(cg, 'tok').filter((e) => e.includes('struct PREFIX'))).toEqual([]);
expect(edges(cg, 'wrapped').filter((e) => e.includes('struct WRAP'))).toEqual([]);
} finally {
cg.close();
}
});
it.each(['c', 'cpp'] as const)('control: a same-named function is still the callee when the macro may be off (%s)', async (language) => {
const cg = await indexed({
[`unit.${language}`]: '#ifdef FAST\n#define helper(x) (x)\n#endif\nint run(int x) { return helper(x); }\n',
[`lib.${language}`]: 'int helper(int x) { return x; }\n',
[`types.${language}`]: 'struct helper { int x; };\n',
});
try {
expect(edges(cg, 'run')).toEqual([`calls function helper (lib.${language})`]);
} finally {
cg.close();
}
});
it('control: a C++ type whose name is no macro is still constructed', async () => {
const cg = await indexed({
'widget.hpp': 'struct Widget { int x; };\n',
'use.cpp': '#include "widget.hpp"\nvoid make() { auto w = Widget(1); }\n',
'other.cpp': '#define OTHER(x) (x)\n',
});
try {
expect(edges(cg, 'make')).toEqual(['instantiates struct Widget (widget.hpp)']);
} finally {
cg.close();
}
});
});