fix(extraction): record instantiates for C++ stack/brace construction (#1035) (#1049)

`instantiates` edges came only from heap `new Calculator(0)` (a
new_expression) and copy-init `Calculator c = Calculator(0)` (a
call_expression). Stack direct-init `Calculator calc(0)` and brace-init
`Widget w{1, 2}` parse as a `declaration` whose constructor arguments hang
directly off the declarator as an argument_list / initializer_list — there
is no call/new node — so the function-body walker saw no constructor
invocation and emitted no edge. A function that built objects with the
ordinary stack syntax looked like it didn't construct them, and the
dependency was missing from impact / callers.

In the body walker, a C++ `declaration` that is a stack/brace construction
now reuses extractInstantiation (a declaration's `type` field IS the
constructed class name, and extractInstantiation already strips template
args / namespace and emits the `instantiates` ref). Gated by
isCppStackConstruction, which requires BOTH a class-like type
(type_identifier / template_type / qualified_identifier — so `int x(0)`
and `auto z = …` are excluded) AND a declarator carrying args
(argument_list / initializer_list — so default `Calculator c;` and the
most-vexing-parse `Calculator c();` are excluded). The edge targets the
class node, not the same-named constructor method.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-28 21:12:53 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent f4e03e9cdc
commit 2176a7a439
4 changed files with 128 additions and 0 deletions
+36
View File
@@ -2763,6 +2763,42 @@ class Both : public Base<char>, public Plain {};
});
});
describe('C++ stack-allocation construction (#1035)', () => {
// `Calculator calc(0)` (direct-init) and `Widget w{1, 2}` (brace-init) carry
// the constructor args directly on the declarator — no call/new node — so
// they emitted no constructor reference, unlike heap `new Calculator(0)`. An
// `instantiates` ref to the constructed type is now emitted for both.
const instNames = (code: string) =>
extractFromSource('f.cpp', `void run() {\n${code}\n}`)
.unresolvedReferences.filter((r) => r.referenceKind === 'instantiates')
.map((r) => r.referenceName);
it('emits an instantiates ref for direct-init and brace-init', () => {
expect(instNames('Calculator calc(0);')).toEqual(['Calculator']);
expect(instNames('Widget w{1, 2};')).toEqual(['Widget']);
});
it('strips template args and namespace to the bare class name', () => {
// `std::vector<int> v(10)` → `vector`; `ns::Widget w(0)` → `Widget`.
expect(instNames('std::vector<int> v(10);')).toEqual(['vector']);
expect(instNames('ns::Widget w(0);')).toEqual(['Widget']);
});
it('does not emit for primitives, default construction, or the most-vexing parse', () => {
expect(instNames('int x(5);')).toEqual([]); // primitive direct-init
expect(instNames('int y{6};')).toEqual([]); // primitive brace-init
expect(instNames('auto z = make();')).toEqual([]); // auto + call (handled elsewhere)
expect(instNames('Calculator deferred;')).toEqual([]); // default construction, no args
expect(instNames('Calculator calc();')).toEqual([]); // function declaration (most-vexing parse)
});
it('emits a single instantiates ref for a multi-declarator statement', () => {
// `Calculator a(1), b(2);` shares one `type` field; both construct a
// Calculator, so one ref suffices (it dedups to one edge regardless).
expect(instNames('Calculator a(1), b(2);')).toEqual(['Calculator']);
});
});
describe('C/C++ imports', () => {
it('should extract system include', () => {
const code = `#include <iostream>`;
+37
View File
@@ -930,6 +930,43 @@ def bootstrap():
expect(callsToUserService).toHaveLength(0);
});
it('records instantiates for C++ stack/brace construction, targeting the class (#1035)', async () => {
// `Calculator calc(0)` (direct-init) and `Widget w{1, 2}` (brace-init)
// carry the constructor args directly on the declarator — there's no
// call/new node — so they recorded no `instantiates` edge, while heap
// `new Calculator(0)` did. Both stack forms now do.
fs.writeFileSync(
path.join(tempDir, 'm.cpp'),
`class Calculator { public: Calculator(int seed) {} int add(int a, int b){ return a+b; } };
class Widget { public: Widget(int a, int b) {} };
int runStack(int a, int b) { Calculator calc(0); return calc.add(a, b); }
int runBrace() { Widget w{1, 2}; return 0; }
int runHeap(int a, int b) { Calculator* c = new Calculator(0); return c->add(a, b); }
void noise() { int x(5); int y{6}; Calculator deferred; }
`
);
cg = await CodeGraph.init(tempDir, { index: true });
const fn = (name: string) => cg.getNodesByKind('function').find((n) => n.name === name)!;
const instTargets = (name: string) =>
cg
.getOutgoingEdges(fn(name).id)
.filter((e) => e.kind === 'instantiates')
.map((e) => cg.getNode(e.target)!);
// Direct-init (the issue) and brace-init both instantiate, targeting the
// CLASS node — not the same-named constructor method.
const stack = instTargets('runStack');
expect(stack.map((n) => `${n.kind}:${n.name}`)).toContain('class:Calculator');
expect(instTargets('runBrace').map((n) => `${n.kind}:${n.name}`)).toContain('class:Widget');
// Heap still works (regression guard).
expect(instTargets('runHeap').map((n) => `${n.kind}:${n.name}`)).toContain('class:Calculator');
// Primitives (`int x(0)`/`int y{6}`) and bare default construction
// (`Calculator deferred;`) must NOT mint an instantiates edge.
expect(instTargets('noise')).toHaveLength(0);
});
it('resolves a cross-file static method call to the method, not the class (#825)', async () => {
// `Foo.bar()` where `Foo` is an imported class must link to the static
// method `Foo::bar`, NOT to the class `Foo`. Previously the import