fix(graph): treat class instantiation as a caller/callee edge (#774) (#804)

`callers <Class>` returned "No callers found" (or only the importing
file) even when a class's constructor was called from many sites, and
the instantiation sites were invisible — the opposite of what "what
breaks if I change this class?" should answer.

The `instantiates` edges already existed in the graph, correctly
attributed to the constructing function; they were simply excluded from
the caller/callee traversal, which queried only calls/references/imports.
Constructing a class is calling its constructor, so add `instantiates`
to the edge-kind set in both getCallers and getCallees (kept symmetric so
they stay inverses and `trace` can cross the instantiation boundary,
function -> class -> its methods). impact already traversed all edge
kinds, so it was unaffected.

Query-layer only — existing indexes benefit on upgrade with no re-index.
Verified on a Python fixture: `callers Supervisor` now returns the
construction sites (main/work/test_it), and a new graph test asserts
main() <-> DerivedClass via the instantiation. Full suite green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-11 11:25:13 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 9a0f144770
commit d0e649969a
3 changed files with 31 additions and 2 deletions
+19
View File
@@ -293,6 +293,25 @@ export { main };
expect(Array.isArray(callees)).toBe(true);
});
it('treats class instantiation as a caller/callee of the class (#774)', () => {
// main() does `new DerivedClass(10, 'test')`. Constructing a class is
// calling its constructor, so main is a caller of DerivedClass and
// DerivedClass is a callee of main. Before #774 the `instantiates` edge
// was excluded from the caller/callee traversal, so `callers <Class>`
// returned the importing file (or nothing) and missed every
// construction site.
const derived = cg.getNodesByKind('class').find((n) => n.name === 'DerivedClass');
const main = cg.getNodesByKind('function').find((n) => n.name === 'main');
expect(derived).toBeDefined();
expect(main).toBeDefined();
const callerNames = cg.getCallers(derived!.id).map((c) => c.node.name);
expect(callerNames).toContain('main');
const calleeNames = cg.getCallees(main!.id).map((c) => c.node.name);
expect(calleeNames).toContain('DerivedClass');
});
});
describe('getImpactRadius()', () => {