diff --git a/__tests__/cpp-macro-and-constructor-calls.test.ts b/__tests__/cpp-macro-and-constructor-calls.test.ts new file mode 100644 index 000000000..803b7c24d --- /dev/null +++ b/__tests__/cpp-macro-and-constructor-calls.test.ts @@ -0,0 +1,505 @@ +/** + * 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): Promise { + 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', + ]); + }); +}); diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index c88a7b0c7..3a8f0345e 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -4982,6 +4982,13 @@ CPPType* ApiHelper { + const result = extractFromSource('f.cpp', 'struct Widget { Widget(int x = 1); };\nvoid run() { Widget a[2]; Widget b[3]{{2}, {3}}; }'); + expect(result.nodes.find((n) => n.kind === 'method')?.signature).toBe('(int x = 1);'); + expect(result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName)) + .toEqual(['Widget::Widget/0', 'Widget::Widget/1', 'Widget::Widget/1', 'Widget::Widget/0']); + }); + it('strips template args and namespace to the bare class name', () => { // `std::vector v(10)` → `vector`; `ns::Widget w(0)` → `Widget`. expect(instNames('std::vector v(10);')).toEqual(['vector']); diff --git a/__tests__/fixtures/kernel-parity/torture-macros-ctors.cpp b/__tests__/fixtures/kernel-parity/torture-macros-ctors.cpp new file mode 100644 index 000000000..ed0763803 --- /dev/null +++ b/__tests__/fixtures/kernel-parity/torture-macros-ctors.cpp @@ -0,0 +1,66 @@ +/// Parity fixture for #1838 / #1839: function-like macros as `constant` +/// nodes (file scope, inside a namespace, inside a body), constructor +/// signatures, and the per-declarator constructor refs of local object +/// initialization — default, direct, brace, multi-declarator, qualified and +/// template types — beside the declarators that construct nothing (pointer, +/// reference, most-vexing-parse, extern). Must parse ERROR-FREE. +#define TRACE_POINT(value) ((void)(value)) +#define VERSION 7 + +namespace app { +#define APP_LOG(fmt, ...) log_impl(fmt, __VA_ARGS__) + +struct Aggregate { + int value; +}; + +class Widget { +public: + Widget(); + explicit Widget(int value); + Widget(int a, int b = 2) : first(a), second(b) {} + +private: + int first; + int second; +}; + +Widget::Widget() : first(0), second(0) {} +Widget::Widget(int value) : first(value), second(0) {} + +template +struct Box { + Box(T inner) : value(inner) {} + T value; +}; + +void constructions() { + Aggregate item{}; + Widget by_default; + Widget braced{}; + Widget direct(1); + Widget two(1, 2); + Widget a, b(1), c{1, 2}; + app::Widget qualified(3); + Box boxed(4); + Widget(); + TRACE_POINT(VERSION); + APP_LOG("x", 1); +#define LOCAL_TRACE(v) ((void)(v)) + LOCAL_TRACE(1); +} + +void non_constructions(Widget &other) { + Widget *pointer{}; + Widget *null_pointer(nullptr); + Widget &reference{other}; + Widget &bound(other); + Widget most_vexing(); + Widget items[2]{}; // array elements do construct objects + Widget *table[2]{}; + Widget (*callback)(){}; + extern Widget external; + int primitive(5); + int braced_primitive{6}; +} +} // namespace app diff --git a/__tests__/fixtures/kernel-parity/torture-macros.c b/__tests__/fixtures/kernel-parity/torture-macros.c new file mode 100644 index 000000000..9d2e5cdb2 --- /dev/null +++ b/__tests__/fixtures/kernel-parity/torture-macros.c @@ -0,0 +1,15 @@ +/* Parity fixture for #1838 in C: function-like macros as `constant` nodes + * at file scope and inside a body, an object-like define that is NOT a node, + * and the call refs the macro invocations still record. */ +#define TRACE_POINT(value) ((void)(value)) +#define VERSION 7 +#define MAX(a, b) ((a) > (b) ? (a) : (b)) + +static int helper(int x) { return x; } + +int exercise(int value) { + TRACE_POINT(value); +#define LOCAL_TRACE(v) ((void)(v)) + LOCAL_TRACE(1); + return MAX(helper(value), VERSION); +} diff --git a/__tests__/kernel-ccpp-parity.test.ts b/__tests__/kernel-ccpp-parity.test.ts index c05a15d66..720ffb623 100644 --- a/__tests__/kernel-ccpp-parity.test.ts +++ b/__tests__/kernel-ccpp-parity.test.ts @@ -151,6 +151,61 @@ describe.skipIf(!kernelBuilt)('kernel C/C++ extraction parity', () => { assertParity('fixtures/torture.hpp', fs.readFileSync(file, 'utf8'), 'cpp'); }); + it('macro + constructor fixture (cpp): #define constants, constructor signatures, per-declarator ctor refs (#1838/#1839)', () => { + const file = path.join(FIXTURE_DIR, 'torture-macros-ctors.cpp'); + const source = fs.readFileSync(file, 'utf8'); + assertParity('fixtures/torture-macros-ctors.cpp', source, 'cpp'); + // Pin the shapes the resolver relies on, so parity is never empty-vs-empty. + process.env.CODEGRAPH_KERNEL = '0'; + const result = extractFromSource('fixtures/torture-macros-ctors.cpp', source, 'cpp'); + delete process.env.CODEGRAPH_KERNEL; + expect(result.nodes.filter((n) => n.kind === 'constant').map((n) => n.qualifiedName).sort()).toEqual([ + 'TRACE_POINT', + 'app::APP_LOG', + 'app::constructions::LOCAL_TRACE', + ]); + expect( + result.nodes.filter((n) => n.kind === 'method' && n.name === 'Widget').map((n) => n.signature).sort() + ).toEqual(['()', '();', '(int a, int b = 2)', '(int value)', '(int value);']); + const ctorRefs = result.unresolvedReferences + .filter((r) => r.referenceKind === 'calls' && r.referenceName.includes('/')) + .map((r) => r.referenceName); + expect(ctorRefs).toEqual([ + 'Aggregate::Aggregate/0', + 'Widget::Widget/0', + 'Widget::Widget/0', + 'Widget::Widget/1', + 'Widget::Widget/2', + 'Widget::Widget/0', + 'Widget::Widget/1', + 'Widget::Widget/2', + 'app::Widget::Widget/1', + 'Box::Box/1', + 'Widget::Widget/0', + ]); + }); + + it.each(['\n', '\r\n'])('constructor prototypes and array elements remain in parity (%j)', (eol) => { + const source = ['struct Widget {', ' Widget(int value = 1);', '};', + 'Widget::Widget(int renamed) {}', 'int argument() { return 2; }', + 'void run() { Widget plain[2]; Widget empty[2]{}; Widget items[3]{{argument()}, {2}}; Widget grid[2][2]{{{1}, {2}}, {{3}}}; Widget scalars[2]{1, 2}; Widget hex[0x2]{{1}}; Widget sized[2u]{{1}}; }', ''].join(eol); + assertParity('arrays.cpp', source, 'cpp'); + }); + + it('macro fixture (c): #define constants at file scope and inside a body (#1838)', () => { + const file = path.join(FIXTURE_DIR, 'torture-macros.c'); + const source = fs.readFileSync(file, 'utf8'); + assertParity('fixtures/torture-macros.c', source, 'c'); + process.env.CODEGRAPH_KERNEL = '0'; + const result = extractFromSource('fixtures/torture-macros.c', source, 'c'); + delete process.env.CODEGRAPH_KERNEL; + expect(result.nodes.filter((n) => n.kind === 'constant').map((n) => n.name).sort()).toEqual([ + 'LOCAL_TRACE', + 'MAX', + 'TRACE_POINT', + ]); + }); + // Metal rides the cpp route: `.metal` maps to language 'cpp' and the // extension-gated `[[attribute]]` blank must reach the kernel arm through // the route-point preParse hoist (filePath rides along for the gate). @@ -185,6 +240,8 @@ describe.skipIf(!kernelBuilt)('kernel C/C++ extraction parity', () => { ['torture.c', 'c'], ['torture.cpp', 'cpp'], ['torture.hpp', 'cpp'], + ['torture-macros-ctors.cpp', 'cpp'], + ['torture-macros.c', 'c'], ] as const)('torture fixture CRLF parity: %s', (name, lang) => { const file = path.join(FIXTURE_DIR, name); const crlf = fs.readFileSync(file, 'utf8').replace(/(? Walker<'t> { let kind = node.kind(); let mut skip_children = false; + // C/C++ function-like macros become `constant` nodes carrying the + // directive as their signature — a value, never a callee (#1838). + // Mirrors tree-sitter.ts visitNode. + if kind == "preproc_function_def" { + if let Some(name_node) = node.child_by_field_name("name") { + let name = self.text(name_node).to_string(); + let signature = Some(self.text(node).trim().to_string()); + self.create_node("constant", &name, node, Extra { signature, ..Extra::default() }); + } + return; + } + // C++ namespace blocks: prefix-only, no node (#1291/#1093). Anonymous // namespaces fall through to the generic walk. if self.variant == Variant::Cpp && kind == "namespace_definition" { @@ -929,7 +941,10 @@ impl<'t> Walker<'t> { self.maybe_capture_fn_refs(node); - if kind == "function_definition" { + if self.is_cpp_constructor_declaration(node) { + self.extract_method(node); + skip_children = true; + } else if kind == "function_definition" { // functionTypes for both; cpp's methodTypes also lists it, so // inside a class-like scope it extracts as a method. if self.inside_class_like() && self.variant == Variant::Cpp { @@ -1016,6 +1031,7 @@ impl<'t> Walker<'t> { let extra = Extra { docstring: preceding_docstring(node, self.src), + signature: self.constructor_signature(node), visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None }, return_type: self.return_type_of(node), ..Extra::default() @@ -1052,6 +1068,7 @@ impl<'t> Walker<'t> { let extra = Extra { docstring: preceding_docstring(node, self.src), + signature: self.constructor_signature(node), visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None }, is_abstract: if self.variant == Variant::Cpp && self.is_cpp_pure_virtual_method_decl(node) { Some(true) @@ -1544,27 +1561,117 @@ impl<'t> Walker<'t> { } } - /// isCppStackConstruction (#1035). - fn is_cpp_stack_construction(&self, node: Node) -> bool { - let Some(type_node) = node.child_by_field_name("type") else { return false }; + fn is_cpp_constructor_declaration(&self, node: Node<'t>) -> bool { + if self.variant != Variant::Cpp || node.kind() != "declaration" || node.child_by_field_name("type").is_some() { + return false; + } + let Some(owner) = node.parent().and_then(|p| p.parent()) else { return false }; + if !matches!(owner.kind(), "class_specifier" | "struct_specifier" | "union_specifier") { return false; } + let Some(decl) = node.child_by_field_name("declarator") else { return false }; + decl.kind() == "function_declarator" && decl.child_by_field_name("declarator").map(|n| self.text(n)) + == owner.child_by_field_name("name").map(|n| self.text(n)) + } + + /// cppExtractor.getSignature (languages/c-cpp.ts): a C++ constructor + /// definition or class-body prototype carries its parameter + /// list as the signature so a local `T obj(args)` can pick the overload + /// by arity (#1839); a trailing semicolon marks a prototype. Macro-shaped definitions whose real name was + /// recovered from an argument are excluded. + fn constructor_signature(&self, node: Node<'t>) -> Option { + if self.variant != Variant::Cpp || (node.kind() != "function_definition" && !self.is_cpp_constructor_declaration(node)) { + return None; + } + if node.child_by_field_name("type").is_some() || self.recover_cpp_macro_defined_name(node).is_some() { + return None; + } + let params = node.child_by_field_name("declarator")?.child_by_field_name("parameters")?; + Some(format!("{}{}", self.text(params), if node.kind() == "declaration" { ";" } else { "" })) + } + + /// cppStackConstructions (tree-sitter.ts, #1035 / #1839): whether the + /// declaration constructs with arguments (→ `instantiates`), and one + /// arity per constructed object (→ `calls T::T/arity`). `extern`, + /// pointer / reference / function declarators construct nothing; an + /// array's braces hold elements, not constructor arguments. + fn cpp_stack_constructions(&self, node: Node<'t>) -> (bool, Vec) { + let none = (false, Vec::new()); + let Some(type_node) = node.child_by_field_name("type") else { return none }; if !matches!( type_node.kind(), "type_identifier" | "template_type" | "qualified_identifier" ) { - return false; + return none; } + let mut instantiates = false; + let mut arities = Vec::new(); for i in 0..node.named_child_count() { let Some(child) = node.named_child(i) else { continue }; + if child.kind() == "storage_class_specifier" && self.text(child) == "extern" { + return none; + } + if matches!(child.kind(), "identifier" | "array_declarator") { + if child.kind() == "array_declarator" && !self.cpp_object_array(child) { continue; } + arities.push(0); + continue; + } if child.kind() != "init_declarator" { continue; } - if let Some(value) = child.child_by_field_name("value") { - if matches!(value.kind(), "argument_list" | "initializer_list") { - return true; + let Some(declarator) = child.child_by_field_name("declarator") else { continue }; + if !matches!(declarator.kind(), "identifier" | "array_declarator") { + continue; + } + let Some(value) = child.child_by_field_name("value") else { continue }; + if !matches!(value.kind(), "argument_list" | "initializer_list") { + continue; + } + instantiates = true; + if declarator.kind() == "identifier" { + let count = (0..value.named_child_count()) + .filter(|&j| value.named_child(j).map(|n| n.kind() != "comment").unwrap_or(false)) + .count(); + arities.push(count); + } else if self.cpp_object_array(declarator) { + let mut dimensions = Vec::new(); + let mut array = Some(declarator); + while let Some(n) = array.filter(|n| n.kind() == "array_declarator") { + dimensions.insert(0, n.child_by_field_name("size").map(|s| self.text(s)) + .filter(|s| s.bytes().all(|b| b.is_ascii_digit())) + .and_then(|s| s.parse::().ok()).filter(|&n| n <= 9_007_199_254_740_991)); + array = n.child_by_field_name("declarator"); } + Self::cpp_array_arities(value, &dimensions, &mut arities); } } - false + (instantiates, arities) + } + + fn cpp_array_arities(list: Node<'t>, dimensions: &[Option], arities: &mut Vec) { + let entries: Vec<_> = (0..list.named_child_count()).filter_map(|j| list.named_child(j)) + .filter(|n| n.kind() != "comment").collect(); + let mut elided = false; + for entry in &entries { + if dimensions.len() > 1 { + if entry.kind() == "initializer_list" { Self::cpp_array_arities(*entry, &dimensions[1..], arities); } + else { elided = true; } + } else { + arities.push(if entry.kind() == "initializer_list" { + (0..entry.named_child_count()).filter_map(|j| entry.named_child(j)) + .filter(|n| n.kind() != "comment").count() + } else { 1 }); + } + } + if !elided && (entries.is_empty() || dimensions[0].map(|n| n > entries.len()).unwrap_or(false)) { + arities.push(0); + } + } + + fn cpp_object_array(&self, node: Node<'t>) -> bool { + let mut element = node.child_by_field_name("declarator"); + while element.map(|n| n.kind() == "array_declarator").unwrap_or(false) { + element = element.and_then(|n| n.child_by_field_name("declarator")); + } + element.map(|n| n.kind() == "identifier").unwrap_or(false) } /// recordCppFnPtrBinding (tree-sitter.ts:5089). @@ -1660,6 +1767,11 @@ impl<'t> Walker<'t> { fn visit_for_calls_and_structure(&mut self, node: Node<'t>) { stack_guard!(); let kind = node.kind(); + // A function-like macro defined inside a body is still a macro (#1838). + if kind == "preproc_function_def" { + self.visit_node(node); + return; + } self.maybe_capture_fn_refs(node); if kind == "call_expression" { @@ -1668,12 +1780,25 @@ impl<'t> Walker<'t> { self.extract_instantiation(node); } - // C++ stack construction `Calculator calc(0)` / `Widget w{1,2}` (#1035). - if kind == "declaration" - && self.variant == Variant::Cpp - && self.is_cpp_stack_construction(node) - { - self.extract_instantiation(node); + // C++ stack construction `Calculator calc(0)` / `Widget w{1,2}` (#1035), + // plus one constructor ref `ns::T::T/arity` per constructed object (#1839). + if kind == "declaration" && self.variant == Variant::Cpp { + let (instantiates, arities) = self.cpp_stack_constructions(node); + if instantiates { + self.extract_instantiation(node); + } + if !arities.is_empty() && !self.stack.is_empty() { + if let Some(type_node) = node.child_by_field_name("type") { + let from = self.top_row(); + let class_name = strip_cpp_template_args(self.text(type_node)); + if let Some(name) = class_name.split("::").filter(|s| !s.is_empty()).last() { + let calls = edge_kind_index("calls").unwrap(); + for arity in arities { + self.push_ref_at(from, &format!("{class_name}::{name}/{arity}"), calls, node); + } + } + } + } } // C++ local fn-pointer bindings: declarations and branch reassignments. diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 8aaca0e2c..82341a127 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -2250,13 +2250,16 @@ for (const direction of ['callers', 'callees'] as const) { return { group, nodes: [...nodes.values()], edges: [...edges.values()] }; }); + const relationships = (node: Node, edges: Edge[]) => [...new Set(edges + .filter((edge) => (direction === 'callers' ? edge.source : edge.target) === node.id) + .map((edge) => edge.kind))]; if (options.json) { const definitions = collected.map(({ group, nodes, edges }) => { const limited = nodes.slice(0, limit); const shown = new Set(limited.map((node) => node.id)); return { ...cliDefinition(group), - [direction]: limited.map((node) => ({ id: node.id, ...cliNode(node) })), + [direction]: limited.map((node) => ({ id: node.id, ...cliNode(node), relationships: relationships(node, edges) })), edges: edges.filter((edge) => shown.has(direction === 'callers' ? edge.source : edge.target)), total: nodes.length, limit, @@ -2277,7 +2280,8 @@ for (const direction of ['callers', 'callees'] as const) { filteredOut, note, definitions, - [direction]: [...union.values()].slice(0, limit).map(cliNode), + [direction]: [...union.values()].slice(0, limit).map((node) => ({ ...cliNode(node), + relationships: relationships(node, collected.flatMap((entry) => entry.edges)) })), total, limit, truncated: total > limit, @@ -2287,7 +2291,7 @@ for (const direction of ['callers', 'callees'] as const) { if (ambiguous) { console.log(chalk.bold(`\n${title} of "${symbol}" — ${groups.length} distinct definitions (narrow with --file):`)); } - for (const { group, nodes } of collected) { + for (const { group, nodes, edges } of collected) { const limited = nodes.slice(0, limit); const total = nodes.length; const truncated = total > limit; @@ -2304,7 +2308,9 @@ for (const direction of ['callers', 'callees'] as const) { } for (const node of limited) { const loc = node.startLine ? `:${node.startLine}` : ''; - console.log(chalk.cyan(node.kind.padEnd(12)) + chalk.white(node.name)); + const kinds = relationships(node, edges).filter((kind) => kind !== 'calls'); + const relation = kinds.length ? ` [${kinds.join(', ')}]` : ''; + console.log(chalk.cyan(node.kind.padEnd(12)) + chalk.white(node.name) + chalk.dim(relation)); console.log(chalk.dim(` ${node.filePath}${loc}`)); console.log(); } diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index bd7c8dbdf..c4aa31a4f 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -539,7 +539,7 @@ export function blankMetalAttributes(source: string): string { * removed in full. The pipeline masks once, and individual paren blankers also * use this helper so they are safe when called directly. */ -function maskCppRawStrings(source: string): { source: string; restore: (blanked: string) => string } { +export function maskCppRawStrings(source: string): { source: string; restore: (blanked: string) => string } { const unchanged = { source, restore: (blanked: string): string => blanked }; if (source.indexOf('R"') === -1) return unchanged; // Skip comments and ordinary literals before looking for a raw opener. The @@ -1763,6 +1763,16 @@ function preParseCSource(source: string): string { return rawStrings.restore(blankCNamedVariadicDefineDots(restoreDirectiveLines(source, blanked))); } +/** A constructor prototype carries defaults even when its body is in another file. */ +export function isCppConstructorDeclaration(node: SyntaxNode): boolean { + if (node.type !== 'declaration' || getChildByField(node, 'type')) return false; + const owner = node.parent?.parent; + if (!owner || !['class_specifier', 'struct_specifier', 'union_specifier'].includes(owner.type)) return false; + const declarator = getChildByField(node, 'declarator'); + return declarator?.type === 'function_declarator' + && getChildByField(declarator, 'declarator')?.text === getChildByField(owner, 'name')?.text; +} + export const cppExtractor: LanguageExtractor = { // Recover macro-annotated class/struct definitions (`class MYMODULE_API Foo : Base`, // #1061/#946) and macro-prefixed functions (`FORCEINLINE FString Foo()`, #1093 @@ -1806,6 +1816,18 @@ export const cppExtractor: LanguageExtractor = { resolveName: extractCppQualifiedMethodName, getReceiverType: extractCppReceiverType, getReturnType: extractCppReturnType, + // Constructors (definitions and class-body declarations) carry their + // parameter list as the signature; a trailing semicolon marks a prototype, so a local `T obj(args)` can be matched + // to the one overload with a compatible arity (#1839). Macro-shaped + // definitions whose real name was recovered from an argument are excluded: + // their "parameters" are macro arguments. Mirrored in the kernel. + getSignature: (node, source) => { + if ((node.type !== 'function_definition' && !isCppConstructorDeclaration(node)) || getChildByField(node, 'type')) return undefined; + if (recoverCppMacroDefinedName(node, source)) return undefined; + const declarator = getChildByField(node, 'declarator'); + const parameters = declarator && getChildByField(declarator, 'parameters'); + return parameters ? getNodeText(parameters, source) + (node.type === 'declaration' ? ';' : '') : undefined; + }, getVisibility: (node) => { // Check for access specifier in parent const parent = node.parent; diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 5304a534d..19c16471a 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -21,7 +21,7 @@ import { FN_REF_SPECS, captureFnRefCandidates, type FnRefSpec, type FnRefCandida import { isGeneratedFile } from './generated-detection'; import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types'; import { EXTRACTORS } from './languages'; -import { stripCppTemplateArgs } from './languages/c-cpp'; +import { stripCppTemplateArgs, isCppConstructorDeclaration } from './languages/c-cpp'; import { rustImplTypeName } from './languages/rust'; import { LiquidExtractor } from './liquid-extractor'; import { RazorExtractor } from './razor-extractor'; @@ -1092,6 +1092,27 @@ export class TreeSitterExtractor { if (skipChildren) return; } + if (this.language === 'cpp' && isCppConstructorDeclaration(node)) { + this.extractMethod(node); + return; + } + + // C/C++ function-like macros (`#define TRACE(x) ...`) become `constant` + // nodes carrying the directive as their signature. A macro is a value, + // never an executable callee: the resolver reads these to recognize a + // call whose name is a macro visible in the translation unit and refuses + // to bind it to a same-named function elsewhere (#1838). Mirrored in the + // kernel (ccpp/mod.rs visit_node). + if ((this.language === 'c' || this.language === 'cpp') && nodeType === 'preproc_function_def') { + const name = getChildByField(node, 'name'); + if (name) { + this.createNode('constant', getNodeText(name, this.source), node, { + signature: getNodeText(node, this.source).trim(), + }); + } + return; + } + // C++ namespace blocks: carry the namespace name as a qualifiedName prefix // while walking the body, so `namespace flash { void compute_attn(); }` // indexes compute_attn with qualifiedName `flash::compute_attn` and a @@ -5192,26 +5213,34 @@ export class TreeSitterExtractor { } /** - * Is this C++ `declaration` a stack/direct-initialization object construction - * that invokes a constructor — `Calculator calc(0)` (direct-init) or - * `Widget w{1, 2}` (brace-init) — as opposed to a plain variable or a - * function declaration? Used to emit an `instantiates` edge for the + * C++ stack construction — `Calculator calc(0)` / `Widget w{1, 2}` — is * call-less construction syntax (#1035); heap `new T(...)` is handled * separately by INSTANTIATION_KINDS. * - * Two signals, both required: - * - the `type` field is a class-like NAMED type (`type_identifier`, - * `template_type`, or `qualified_identifier`). Primitives (`int x(0)`), - * `auto` (`placeholder_type_specifier` — that form always carries a real - * `call_expression`, already handled), and sized specifiers are excluded — - * they construct no class; and - * - a declarator carries constructor arguments: an `init_declarator` whose - * `value` is an `argument_list` (`(args)`) or `initializer_list` (`{args}`). - * This skips default construction `Calculator c;` (no value) and the - * most-vexing-parse `Calculator c();` (a bodyless `function_declarator`, - * a function decl — not a construction). + * The `type` field must be a class-like NAMED type (`type_identifier`, + * `template_type`, or `qualified_identifier`). Primitives (`int x(0)`), + * `auto` (`placeholder_type_specifier` — that form always carries a real + * `call_expression`, already handled), and sized specifiers are excluded — + * they construct no class. `extern T x;` declares, it constructs nothing. + * + * Per declarator: + * - a bare `identifier` is default construction (`T item;`) — arity 0; + * - an `init_declarator` whose `value` is an `argument_list` (`(args)`) or + * `initializer_list` (`{args}`) carries constructor arguments — arity is + * the argument count. An array declarator's braces hold ELEMENTS, not + * constructor arguments, so each braced element has its own arity; + * - pointer / reference / function declarators construct nothing: + * `T* p{}` is a null pointer, `T& r{x}` binds a reference, and the + * most-vexing-parse `T c();` is a function declaration. + * + * `instantiates` (the type) is retained for initialized declarations, + * preserving the #1035 dependency edges. `calls` (`T::T/arity`, resolved to + * the constructor by src/resolution/cpp-constructor.ts, #1839) is emitted + * for every declarator with an arity, default construction included. + * Mirrored in the kernel (ccpp/mod.rs cpp_stack_constructions). */ - private isCppStackConstruction(node: SyntaxNode): boolean { + private cppStackConstructions(node: SyntaxNode): { instantiates: boolean; arities: number[] } { + const none = { instantiates: false, arities: [] }; const typeNode = getChildByField(node, 'type'); if ( !typeNode || @@ -5219,17 +5248,59 @@ export class TreeSitterExtractor { typeNode.type !== 'template_type' && typeNode.type !== 'qualified_identifier') ) { - return false; + return none; } + let instantiates = false; + const arities: number[] = []; for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); - if (child?.type !== 'init_declarator') continue; + if (!child) continue; + if (child.type === 'storage_class_specifier' && getNodeText(child, this.source) === 'extern') return none; + if (child.type === 'identifier' || child.type === 'array_declarator') { + if (child.type === 'array_declarator' && !this.cppObjectArray(child)) continue; + arities.push(0); + continue; + } + if (child.type !== 'init_declarator') continue; + const declarator = getChildByField(child, 'declarator'); + if (declarator?.type !== 'identifier' && declarator?.type !== 'array_declarator') continue; const value = getChildByField(child, 'value'); - if (value && (value.type === 'argument_list' || value.type === 'initializer_list')) { - return true; + if (!value || (value.type !== 'argument_list' && value.type !== 'initializer_list')) continue; + instantiates = true; + if (declarator.type === 'identifier') { + arities.push(value.namedChildren.filter((c) => c.type !== 'comment').length); + } else if (this.cppObjectArray(declarator)) { + const dimensions: number[] = []; + let array: SyntaxNode | null = declarator; + while (array?.type === 'array_declarator') { + const size = getChildByField(array, 'size')?.text ?? ''; + dimensions.unshift(/^\d+$/.test(size) && Number.isSafeInteger(Number(size)) ? Number(size) : NaN); + array = getChildByField(array, 'declarator'); + } + const elements = (list: SyntaxNode, depth: number): void => { + const entries = list.namedChildren.filter((c) => c.type !== 'comment'); + let elided = false; + for (const entry of entries) { + if (depth + 1 < dimensions.length) { + if (entry.type === 'initializer_list') elements(entry, depth + 1); + else elided = true; // Unbraced multidimensional layout needs type information. + } else { + arities.push(entry.type === 'initializer_list' + ? entry.namedChildren.filter((c) => c.type !== 'comment').length : 1); + } + } + if (!elided && (entries.length === 0 || dimensions[depth]! > entries.length)) arities.push(0); + }; + elements(value, 0); } } - return false; + return { instantiates, arities }; + } + + private cppObjectArray(node: SyntaxNode): boolean { + let element = getChildByField(node, 'declarator'); + while (element?.type === 'array_declarator') element = getChildByField(element, 'declarator'); + return element?.type === 'identifier'; } /** @@ -5786,6 +5857,12 @@ export class TreeSitterExtractor { const visitForCallsAndStructure = (node: SyntaxNode): void => { const nodeType = node.type; + // A function-like macro defined inside a body is still a macro (#1838). + if ((this.language === 'c' || this.language === 'cpp') && nodeType === 'preproc_function_def') { + this.visitNode(node); + return; + } + // Function-as-value capture (#756) — function bodies are walked here, // not in visitNode, so the capture hook must fire in both walkers. this.maybeCaptureFnRefs(node, nodeType); @@ -5835,8 +5912,29 @@ export class TreeSitterExtractor { // (which strips template args / namespace and emits the `instantiates` // ref). Children still recurse below, so a nested ctor-arg call // (`Calculator calc(make())`) keeps its own `calls` ref. - if (nodeType === 'declaration' && this.language === 'cpp' && this.isCppStackConstruction(node)) { - this.extractInstantiation(node); + if (nodeType === 'declaration' && this.language === 'cpp') { + const { instantiates, arities } = this.cppStackConstructions(node); + if (instantiates) this.extractInstantiation(node); + // One `calls` ref per constructed object, naming the constructor and + // its argument count (`ns::T::T/1`) so the resolver can pick the + // overload — a type is not a callee (#1839). + const callerId = this.nodeStack[this.nodeStack.length - 1]; + const typeNode = getChildByField(node, 'type'); + if (callerId && typeNode && arities.length) { + const className = stripCppTemplateArgs(getNodeText(typeNode, this.source)); + const name = className.split('::').filter(Boolean).pop(); + if (name) { + for (const arity of arities) { + this.unresolvedReferences.push({ + fromNodeId: callerId, + referenceName: `${className}::${name}/${arity}`, + referenceKind: 'calls', + line: node.startPosition.row + 1, + column: node.startPosition.column, + }); + } + } + } } // C++ local function-pointer bindings (see cppLocalFnPtrs): record diff --git a/src/resolution/cpp-constructor.ts b/src/resolution/cpp-constructor.ts new file mode 100644 index 000000000..92d2f4d7a --- /dev/null +++ b/src/resolution/cpp-constructor.ts @@ -0,0 +1,126 @@ +/** + * Local C++ object construction → the constructor it runs (#1839). + * + * `T obj;` / `T obj(args);` / `T obj{args};` carry no call node, so + * extraction records a dedicated `calls` ref shaped `ns::T::T/` + * (tree-sitter.ts cppStackConstructions). This module resolves it: + * - the type is looked up in the LEXICAL namespaces of the call site — for + * `second::use()` writing `Widget w;`, `second::Widget` before a global + * `Widget`; a `::T` spelling is global only; + * - among that type's constructors, the single overload whose parameter + * count admits the argument count wins (defaults and `...` widen a range); + * two admitting overloads (`T(int)` / `T(double)` for `T w(x)`) resolve to + * nothing rather than a guess; + * - a type with no indexed constructor (an aggregate) yields no edge — a + * type declaration is not a callee. + * These refs never fall through to the generic name strategies. + */ +import type { Node } from '../types'; +import type { ResolvedRef, ResolutionContext, UnresolvedRef } from './types'; + +const CONSTRUCTOR_REF = /^(.*)::([^:]+)\/(\d+)$/; + +export function isCppConstructorRef(ref: UnresolvedRef): boolean { + return ref.language === 'cpp' && ref.referenceKind === 'calls' && /::[^:]+\/\d+$/.test(ref.referenceName); +} + +/** Admissible argument counts of a `(params)` signature; null when it can't be read. */ +function constructorShape(signature: string | undefined): { min: number; max: number; key: string } | null { + signature = signature?.replace(/;$/, ''); + if (!signature?.startsWith('(') || !signature.endsWith(')')) return null; + const text = signature.slice(1, -1).trim(); + if (!text || text === 'void') return { min: 0, max: 0, key: '' }; + // Split on top-level commas only: `std::map`, `int (*cb)(int, int)` + // and `T x = f(a, b)` all nest their commas. + const parts: string[] = []; + let start = 0; + let depth = 0; + let quote = ''; + for (let i = 0; i < text.length; i++) { + const c = text[i]!; + if (quote) { + if (c === '\\') i++; + else if (c === quote) quote = ''; + continue; + } + if (c === '"' || c === "'") { + quote = c; + continue; + } + if ('(<[{'.includes(c)) depth++; + if (')>]}'.includes(c)) depth--; + if (c === ',' && depth === 0) { + parts.push(text.slice(start, i)); + start = i + 1; + } + } + if (depth !== 0 || quote) return null; + parts.push(text.slice(start)); + // A comparison in a default argument would be mistaken for a `=` default + // or a template bracket — leave those to a compiler. + if (parts.some((p) => /[<>]=|==|!=/.test(p))) return null; + const variadic = parts.some((p) => p.includes('...')); + const types = parts.map((p) => { + let type = p.split('=')[0]!.trim(); + // Strip an optional parameter name, retaining unnamed built-in types + // (`unsigned int`, `long long`) and qualifiers. Complex declarators stay + // distinct unless their spelling matches; uncertainty must not merge overloads. + type = type.replace(/(.*[\s*&>])([A-Za-z_]\w*)$/, (whole, prefix: string, name: string) => + /^(?:void|bool|char|short|int|long|float|double|signed|unsigned|const|volatile)$/.test(name) + || /^(?:const|volatile|struct|class|enum)\s*$/.test(prefix) ? whole : prefix); + return type.replace(/\s+/g, ''); + }); + return { + key: types.join(','), + min: parts.filter((p) => !p.includes('=') && !p.includes('...')).length, + max: variadic ? Infinity : parts.length, + }; +} + +export function matchCppConstructor(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + const match = ref.referenceName.match(CONSTRUCTOR_REF); + if (!match) return null; + const [, rawType, name, count] = match; + const type = rawType!.replace(/^::/, ''); + if (type.split('::').pop() !== name) return null; + const argc = Number(count); + + // Innermost lexical namespace first, then outward, then global. + const caller = context.getNodeById?.(ref.fromNodeId); + const scopes = rawType!.startsWith('::') ? [] : (caller?.qualifiedName.split('::') ?? []); + const candidates: string[] = []; + for (let i = scopes.length; i > 0; i--) candidates.push(`${scopes.slice(0, i).join('::')}::${type}`); + candidates.push(type); + + for (const qualified of candidates) { + const owners = context + .getNodesByQualifiedName(qualified) + .filter((n) => n.language === 'cpp' && (n.kind === 'class' || n.kind === 'struct' || n.kind === 'union')); + if (owners.length === 0) continue; + const constructors = context + .getNodesByName(name!) + .filter((n) => n.language === 'cpp' && n.kind === 'method' && n.qualifiedName === `${qualified}::${name}`); + // Brace-init prefers an initializer_list overload over arity — that + // choice needs the argument types, so decline. + if (constructors.some((n) => /\binitializer_list\b/.test(n.signature ?? ''))) return null; + // A prototype and its out-of-line definition describe one overload. + // Merge their admissible ranges, then prefer the executable definition. + const overloads = new Map(); + for (const node of constructors) { + const shape = constructorShape(node.signature); + if (!shape) return null; + const prior = overloads.get(shape.key); + if (prior) { + prior.nodes.push(node); + prior.min = Math.min(prior.min, shape.min); + } else overloads.set(shape.key, { nodes: [node], min: shape.min, max: shape.max }); + } + const admitting = [...overloads.values()].filter((o) => o.min <= argc && argc <= o.max); + if (admitting.length !== 1) return null; + const definitions = admitting[0]!.nodes.filter((n) => !n.signature?.endsWith(';')); + const targets = definitions.length > 0 ? definitions : admitting[0]!.nodes; + if (targets.length !== 1) return null; + return { original: ref, targetNodeId: targets[0]!.id, confidence: 0.9, resolvedBy: 'qualified-name' }; + } + return null; +} diff --git a/src/resolution/cpp-macro-visibility.ts b/src/resolution/cpp-macro-visibility.ts new file mode 100644 index 000000000..f9946c1cc --- /dev/null +++ b/src/resolution/cpp-macro-visibility.ts @@ -0,0 +1,326 @@ +/** + * Is a C/C++ call's name a function-like macro visible at the call site? + * + * `TRACE_POINT(1)` parses as a call_expression, so extraction records a + * `calls` ref named `TRACE_POINT`. If the translation unit defines + * `#define TRACE_POINT(value) ...` (in the file itself or through an + * include), the "call" is a macro expansion — and binding it to a + * same-spelled free function in an unrelated file fabricates a caller and a + * callee that never existed (#1838). The resolver asks this before trying + * any strategy and drops such a ref. + * + * Directive summaries are cached per file, but evaluated in translation-unit + * order on every inclusion. Only #pragma once and actual guard state suppress + * reinclusion; an active recursion stack breaks include cycles. Unknown build + * flags remain possible, so only definite macro visibility suppresses a call. + * Object-like definitions participate in conditions but never enter the cached + * call-site timelines (vendor headers can contain tens of thousands of them). + */ +import * as path from 'path'; +import { maskCppRawStrings } from '../extraction/languages/c-cpp'; +import { CPP_DEFINE_SIGNATURE, type ResolutionContext, type UnresolvedRef } from './types'; +import { resolveImportPath } from './import-resolver'; + +/** Three-valued: true / false / undefined = depends on an unknown build flag. */ +type Truth = boolean | undefined; +type FileEvent = + | { kind: 'define' | 'undef'; name: string; line: number; functionLike: boolean; value: string; wrapsItself: boolean } + | { kind: 'include'; quote: string; spec: string; line: number } + | { kind: 'branch'; op: string; expression: string; guard: boolean; line: number } + | { kind: 'once'; line: number }; +type Event = { line: number; defined: Truth }; +type Cache = { + summaries: Map; + includes: Map; + /** Indexed files by basename, for `#include "dir/name.h"` that no include root explains. */ + byBasename: Map | null; + /** Per root file: macro name → define/undef events in root-file line order. */ + roots: Map>; +}; + +const memo = new WeakMap(); +const ROOT_TIMELINE_CAP = 32; + +const and = (a: Truth, b: Truth): Truth => + a === false || b === false ? false : a === true && b === true ? true : undefined; +const or = (a: Truth, b: Truth): Truth => + a === true || b === true ? true : a === false && b === false ? false : undefined; +const not = (a: Truth): Truth => (a === undefined ? undefined : !a); + +export function clearCppMacroVisibility(context: ResolutionContext): void { + memo.delete(context); +} + +const isDefine = (n: { kind: string; signature?: string }): boolean => + n.kind === 'constant' && CPP_DEFINE_SIGNATURE.test(n.signature ?? ''); + +/** + * Is this C/C++ `calls` ref a macro expansion rather than a call? True when + * the index knows the name as a function-like macro (extraction mints a + * `constant` per `preproc_function_def`) and either nothing but macros bears + * the name — there is no function to call, and a case-insensitive fuzzy + * match (`SWAP` → `swap`) must not invent one — or the macro is definitely + * visible at the call site. + */ +export function isVisibleCppMacro(ref: UnresolvedRef, context: ResolutionContext): boolean { + if (ref.language !== 'c' && ref.language !== 'cpp') return false; + if (ref.referenceKind !== 'calls' || !/^\w+$/.test(ref.referenceName)) return false; + // The name cache is the index's truth, invalidated with the resolver's other + // caches; a per-context name set would go stale between indexing batches. + const sameName = context.getNodesByName(ref.referenceName); + if (!sameName.some(isDefine)) return false; + if (!sameName.some((n) => !isDefine(n))) return true; + const cache = cacheFor(context); + + const rootKey = `${ref.language}\0${ref.filePath}`; + let timeline = cache.roots.get(rootKey); + if (!timeline) { + timeline = walkTranslationUnit(ref.filePath, ref.language, context, cache); + if (cache.roots.size >= ROOT_TIMELINE_CAP) cache.roots.delete(cache.roots.keys().next().value!); + cache.roots.set(rootKey, timeline); + } + const before = (timeline.get(ref.referenceName) ?? []).filter((e) => e.line <= ref.line); + return before.length > 0 && before[before.length - 1]!.defined === true; +} + +function cacheFor(context: ResolutionContext): Cache { + let cache = memo.get(context); + if (!cache) { + cache = { summaries: new Map(), includes: new Map(), byBasename: null, roots: new Map() }; + memo.set(context, cache); + } + return cache; +} + +/** Cache syntax, never conditional truth: an included file can change its flags. */ +function summarize(file: string, context: ResolutionContext, cache: Cache): FileEvent[] { + const cached = cache.summaries.get(file); + if (cached) return cached; + const events: FileEvent[] = []; + const lines = directiveLines(context.readFile(file) ?? ''); + for (let i = 0; i < lines.length; i++) { + const text = lines[i]!; + const branch = text.match(/^\s*#\s*(ifdef|ifndef|if|elif|else|endif)\b(.*)$/); + if (branch) { + events.push({ kind: 'branch', op: branch[1]!, expression: branch[2]!, + guard: guardsItself(lines, i, branch[1]!, branch[2]!), line: i + 1 }); + continue; + } + const directive = text.match(/^\s*#\s*(define|undef)\s+(\w+)(\(?)/); + if (directive) { + events.push({ kind: directive[1] as 'define' | 'undef', name: directive[2]!, line: i + 1, + functionLike: directive[3] === '(', value: text.slice(directive[0].length), + wrapsItself: directive[3] === '(' && callsItself(lines, i, directive[2]!) }); + continue; + } + const include = text.match(/^\s*#\s*include\s*([<"])([^>"]+)[>"]/); + if (include) events.push({ kind: 'include', quote: include[1]!, spec: include[2]!, line: i + 1 }); + if (/^\s*#\s*pragma\s+once\b/.test(text)) events.push({ kind: 'once', line: i + 1 }); + } + cache.summaries.set(file, events); + return events; +} + +/** + * The file's lines with comments removed — only as far as the preprocessor + * needs: a line inside a block comment is blank, a directive line loses its + * trailing `//` / `/* … *\/`, and every other line is kept verbatim (its + * content is never read, only whether a block comment opens on it). The + * generic comment stripper is a regex pass over the whole file and this walk + * touches every header of a translation unit, vendor trees included. + */ +function directiveLines(source: string): string[] { + const out: string[] = []; + let inBlock = false; + for (const raw of maskCppRawStrings(source).source.split(/\r?\n/)) { + let text = raw; + if (inBlock) { + const end = text.indexOf('*/'); + if (end < 0) { + out.push(''); + continue; + } + text = text.slice(end + 2); + inBlock = false; + } + const directive = /^\s*#/.test(text); + let kept = ''; + let quote = ''; + for (let i = 0; i < text.length; i++) { + const c = text[i]!; + if (quote) { + if (c === '\\') i++; + else if (c === quote) quote = ''; + continue; + } + if (c === '"' || c === "'") { + quote = c; + continue; + } + if (c === '/' && text[i + 1] === '/') { + kept = text.slice(0, i); + break; + } + if (c === '/' && text[i + 1] === '*') { + const end = text.indexOf('*/', i + 2); + if (end < 0) { + inBlock = true; + kept = text.slice(0, i); + break; + } + text = text.slice(0, i) + ' ' + text.slice(end + 2); + i--; + continue; + } + } + out.push(directive ? kept || text : raw); + } + return out; +} + +/** Does the body of the `#define NAME(` at `index` (continuation lines included) call `NAME`? */ +function callsItself(lines: string[], index: number, name: string): boolean { + let text = lines[index]!; + for (let j = index; /\\\s*$/.test(lines[j]!) && j + 1 < lines.length; j++) text += ' ' + lines[j + 1]!; + const body = text.slice(text.indexOf('(') + 1); + return new RegExp(`(?:\\b${name}\\s*\\(|\\(\\s*${name}\\s*\\)\\s*\\()`).test(body); +} + +/** + * The include-guard idiom: `#ifndef X_H` (or `#if !defined(X_H)`) whose next + * directive is `#define X_H`. Nothing in the file defines the guard before + * the test, so this is the first inclusion and the guarded body is active. + * The same shape around a fallback function-like macro (`#ifndef MIN` / + * `#define MIN(a, b) …`) is read the same way. A default VALUE + * (`#ifndef ENABLE_X` / `#define ENABLE_X 0`) is not: that is the flag a + * build overrides on the command line, so it stays unknown. + */ +function guardsItself(lines: string[], index: number, op: string, expression: string): boolean { + const name = + op === 'ifndef' + ? expression.trim() + : expression.match(/^\s*!\s*defined\s*(?:\(\s*(\w+)\s*\)|(\w+))\s*$/)?.slice(1).find(Boolean); + if (!name || !/^\w+$/.test(name)) return false; + for (let j = index + 1; j < lines.length; j++) { + const text = lines[j]!; + if (!/^\s*#/.test(text)) continue; + return new RegExp(`^\\s*#\\s*define\\s+${name}(?:\\s*$|\\()`).test(text); + } + return false; +} + +function resolveInclude( + file: string, + quote: string, + spec: string, + language: UnresolvedRef['language'], + context: ResolutionContext, + cache: Cache +): string | null { + const key = `${language}\0${file}\0${quote}${spec}`; + const cached = cache.includes.get(key); + if (cached !== undefined) return cached; + const local = path.posix.normalize(path.posix.join(path.posix.dirname(file), spec.replace(/\\/g, '/'))); + let target = + quote === '"' && !local.startsWith('../') && !path.posix.isAbsolute(local) && context.fileExists(local) + ? local + : resolveImportPath(spec, file, language, context); + if (!target) { + if (!cache.byBasename) { + cache.byBasename = new Map(); + for (const f of context.getAllFiles()) { + const base = f.slice(f.lastIndexOf('/') + 1); + const list = cache.byBasename.get(base); + if (list) list.push(f); + else cache.byBasename.set(base, [f]); + } + } + const normalized = spec.replace(/\\/g, '/'); + const matches = (cache.byBasename.get(normalized.slice(normalized.lastIndexOf('/') + 1)) ?? []).filter( + (f) => f === normalized || f.endsWith('/' + normalized) + ); + if (matches.length === 1) target = matches[0]!; + } + cache.includes.set(key, target); + return target; +} + +function walkTranslationUnit( + rootFile: string, + language: UnresolvedRef['language'], + context: ResolutionContext, + cache: Cache +): Map { + const timeline = new Map(); + const definitions = new Map(); + const scanning = new Set(); + const macroNames = new Set(); + const once = new Map(); + const condition = (expression: string): Truth => { + const text = expression.trim(); + if (/^(?:0x[\da-f]+|\d+)[uUlL]*$/i.test(text)) return Number(text.replace(/[uUlL]+$/, '')) !== 0; + const def = text.match(/^(!)?\s*defined\s*(?:\(\s*(\w+)\s*\)|(\w+))$/); + if (def) { + const known = definitions.get(def[2] ?? def[3]!)?.defined; + return def[1] ? not(known) : known; + } + return /^\w+$/.test(text) ? definitions.get(text)?.value : undefined; + }; + const scan = (file: string, inherited: Truth, includeLine?: number): void => { + if (inherited === false || scanning.has(file) || once.get(file) === true) return; + scanning.add(file); + let active: Truth = inherited; + const frames: Array<{ parent: Truth; taken: Truth }> = []; + for (const ev of summarize(file, context, cache)) { + const line = includeLine ?? ev.line; + if (ev.kind === 'branch') { + if (ev.op === 'if' || ev.op === 'ifdef' || ev.op === 'ifndef') { + const known = definitions.get(ev.expression.trim())?.defined; + let selected = ev.op === 'if' ? condition(ev.expression) : ev.op === 'ifndef' ? not(known) : known; + if (selected === undefined && ev.guard) selected = true; + frames.push({ parent: active, taken: selected }); + active = and(active, selected); + } else if (ev.op === 'endif') { + active = frames.pop()?.parent ?? inherited; + } else { + const frame = frames[frames.length - 1]; + if (frame) { + const test = ev.op === 'else' ? true : condition(ev.expression); + active = and(frame.parent, and(not(frame.taken), test)); + frame.taken = or(frame.taken, test); + } + } + continue; + } + if (active === false) continue; + if (ev.kind === 'once') { + once.set(file, or(once.get(file) ?? false, active)); + continue; + } + if (ev.kind === 'include') { + const target = resolveInclude(file, ev.quote, ev.spec, language, context, cache); + if (target) scan(target, active, line); + continue; + } + const prior = definitions.get(ev.name); + const defining = ev.kind === 'define'; + const macro = defining && ev.functionLike && !ev.wrapsItself; + const now = active === true ? macro : prior?.macro === macro ? macro : undefined; + definitions.set(ev.name, { + defined: defining ? or(prior?.defined ?? false, active) : and(prior?.defined ?? false, not(active)), + value: defining && active === true ? condition(ev.value) : undefined, + macro: now, + }); + if (ev.functionLike) macroNames.add(ev.name); + if (macroNames.has(ev.name)) { + const events = timeline.get(ev.name) ?? []; + events.push({ line, defined: now }); + timeline.set(ev.name, events); + } + } + scanning.delete(file); + }; + + scan(rootFile, true); + return timeline; +} diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 43c2fd260..0d84ed46e 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -19,8 +19,11 @@ import { isSupertypeTarget, isInheritanceRef, isImportableKind, + CPP_DEFINE_SIGNATURE, } from './types'; import { matchJsStoreBindingCall, isUnresolvedJsMemberCall, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos } from './name-matcher'; +import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility'; +import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; import { resolveViaImport, resolvePhpImportedStaticCall, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath } from './import-resolver'; import { ResolverPool, minRefsForPool } from './resolver-pool'; import { resolveAliasBinding } from './alias-binding'; @@ -414,6 +417,7 @@ export class ReferenceResolver { if (this.context) { clearImportResolverMemos(this.context); clearNameMatcherMemos(this.context); + clearCppMacroVisibility(this.context); } } @@ -889,6 +893,10 @@ export class ReferenceResolver { * the alias names (see ./alias-binding), regardless of the strategy. */ resolveOne(ref: UnresolvedRef): ResolvedRef | null { + // A C/C++ "call" whose name is a function-like macro visible in this + // translation unit is a macro expansion, not a call — it must never bind + // to a same-named function in another file (#1838). + if (isVisibleCppMacro(ref, this.context)) return null; const candidate = this.gateTargetKind(this.resolveOneInner(ref), ref); const resolved = candidate?.resolvedBy === 'framework' ? this.gateFrameworkLanguage(candidate, ref) @@ -911,6 +919,10 @@ export class ReferenceResolver { } private resolveOneInner(ref: UnresolvedRef): ResolvedRef | null { + // A local C++ object construction (`T obj(args)`, ref `ns::T::T/1`) + // resolves ONLY to a constructor of the lexically nearest `T` (#1839). + if (isCppConstructorRef(ref)) return matchCppConstructor(ref, this.context); + // Skip built-in/external references if (this.isBuiltInOrExternal(ref)) { return null; @@ -2670,6 +2682,13 @@ export class ReferenceResolver { private gateTargetKind(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null { if (!result) return result; + // A `#define` is a value, never a callee (#1838): a macro defined only in + // an unrelated file is not what `NAME(x)` here expands to either. + if (ref.referenceKind === 'calls') { + const target = this.queries.getNodeById(result.targetNodeId); + if (target?.kind === 'constant' && CPP_DEFINE_SIGNATURE.test(target.signature ?? '')) return null; + } + // An `imports` reference names something importable — never a member that // only exists inside a type. if (ref.referenceKind === 'imports') { diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 04f3a3707..ac2470b17 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import { Language, Node } from '../types'; -import { UnresolvedRef, ResolvedRef, ResolutionContext, isSupertypeTarget, isInheritanceRef, isImportableKind } from './types'; +import { UnresolvedRef, ResolvedRef, ResolutionContext, isSupertypeTarget, CPP_DEFINE_SIGNATURE, isInheritanceRef, isImportableKind } from './types'; import { blankStringContents, stripCommentsForRegex } from './strip-comments'; import { JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins'; /** @@ -981,6 +981,11 @@ export function matchByExactName( if (storeAction) return storeAction; } const candidates = context.getNodesByName(ref.referenceName) + // Macro constants are not callees and must not consume the same-name + // ceiling (#1839). Keep upstream's language gate on the chosen result. + .filter((n) => !(ref.referenceKind === 'calls' && + (ref.language === 'c' || ref.language === 'cpp') && + n.kind === 'constant' && CPP_DEFINE_SIGNATURE.test(n.signature ?? ''))) // Type/value references retain same-family eligibility: a native namesake // must not hide the actual web type. Calls still gate only the winner. .filter((n) => (ref.referenceKind !== 'references' && ref.referenceKind !== 'function_ref') || diff --git a/src/resolution/types.ts b/src/resolution/types.ts index 42f689a56..75d7ab5eb 100644 --- a/src/resolution/types.ts +++ b/src/resolution/types.ts @@ -356,3 +356,12 @@ const NON_IMPORTABLE_KINDS = new Set([ export function isImportableKind(kind: Node['kind']): boolean { return !NON_IMPORTABLE_KINDS.has(kind); } + +/** + * The signature extraction gives a C/C++ `constant` minted from a + * function-like `preproc_function_def` (`#define NAME(args) …`, #1838). A + * macro is a value: it is never a `calls` target, and its presence in a + * translation unit is what makes `NAME(x)` a macro expansion rather than a + * call. + */ +export const CPP_DEFINE_SIGNATURE = /^\s*#\s*define\b/;