GitNexus/gitnexus/test/integration/resolvers/cpp.test.ts
Copilot bb68cc1eb0
Extract resolveMemberCall from resolveCallTarget (SM-11) (#744)
* Initial plan

* feat(SM-11): extract resolveMemberCall from resolveCallTarget

- Create resolveMemberCall(ownerType, methodName, currentFile, ctx, heritageMap?)
  that uses owner-scoped + MRO resolution only (no fuzzy lookup)
- resolveCallTarget delegates member calls (D0 path) to resolveMemberCall
- walkMixedChain uses resolveMemberCall for owner-scoped member-call resolution
- Add 7 unit tests for resolveMemberCall covering direct, inherited, MRO,
  null cases, and confidence tier assertions
- Export resolveMemberCall for external use

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/3b7889a9-5f2f-4572-8904-45084210f10d

* fix(SM-11): address PR #744 review

Blocking fixes:

- B1: Revert unrelated package-lock.json gitnexus-shared addition

- B2: Document confidence-tier semantic change on resolveMemberCall

Performance / coupling fixes:

- S1: walkMixedChain now calls resolveMethodByOwner directly (hot path) to avoid throwaway ResolveResult allocation per chain step

- S2: Thread tier from resolveMethodByOwner via { def, tier } tuple; eliminates double ctx.resolve

Alignment with semantic-model plan (Phase 3 target):

- resolveMethodByOwner now iterates ALL class-like candidates from ctx.resolve, deduplicating matches by nodeId. Absorbs D4's ownerId-filtering into the owner-scoped path.

- Handles homonym classes (two Users in different files) without falling through to D1-D4 fuzzy widening

- Shared-ancestor MRO walks automatically dedup (both homonyms walk to same base method)

- Unified direct-vs-MRO lookup under a single canWalkMRO check

Tests added:

- T1: Three D0 skip-condition tests via new _resolveCallTargetForTesting internal export (overloadHints, preComputedArgTypes, hasActiveModuleAlias)

- T2: Rust qualified-syntax null test (trait-inherited method) + direct impl control

- T3: C++ leftmost-base diamond inheritance test

- B2 lock-in: cross-file class tier assertion

- Homonym disambiguation: only-one-owns-method, both-own-method ambiguity, shared-ancestor MRO convergence

Verification:

- tsc --noEmit: clean

- vitest run test/unit/: 3014 passed

- vitest run test/integration/resolvers/: 1746 passed

* test(SM-11): address second PR #744 review round + per-language integration tests

Review fixes (https://github.com/abhigyanpatwari/GitNexus/pull/744#issuecomment-4211877593):

P1 (Performance): Replace Map allocation in resolveMethodByOwner with a firstDef+ambiguous flag pattern. Zero allocation for the common single-candidate case on the hot path — the previous Map approach allocated on every member call regardless of whether deduplication was needed.

P2 (Test gap): Strengthen the module-alias D0 skip test with a homonym fixture (two Users in different files). Previously the test passed whether or not D0 was actually bypassed; the new version proves D0 must be skipped by showing that resolveMemberCall directly returns null (ambiguous) but D1-D4 with alias narrowing picks the right one. Also fixes the underlying D2-vs-alias widening interaction: when filteredCandidates was narrowed by module-alias disambiguation, D2 no longer widens back to the full fuzzy pool (introduces aliasNarrowed boolean flag).

L1 (Language coverage): Add C# and Kotlin implements-split tests at the resolveMemberCall layer.

L2 (Maintainability): Export OverloadHints as @internal so the test can use a direct cast instead of fragile Parameters<...> type inference.

Per-language integration tests:

- rust-child-extends-parent: Direct impl method resolution via D0 (with honest documentation of the trait-method-as-Function gap that is Phase 5 / SM-16 scope)

- java-interface-default-method: User implements Validator with default method resolved via implements-split MRO

- csharp-interface-default-method: Same pattern for C# 8.0+ default interface methods

- kotlin-interface-default-method: Same pattern for Kotlin interfaces with default implementations

- python-multi-level-mro: 3-level C3 linearization (Grandparent ← Parent ← Child)

- cpp-diamond-inheritance: Classic diamond (Base ← A, B ← Derived) via leftmost-base MRO

Verification:

- tsc --noEmit: clean

- vitest run test/unit/: 3015 passed

- vitest run test/integration/resolvers/: 1763 passed (+17 new per-language tests)

* fix(SM-11): Codex adversarial review corrections + deeper D0 fixes

Addresses the three high-severity findings from the Codex adversarial review of PR #744 (https://github.com/abhigyanpatwari/GitNexus/pull/744#issuecomment-4212075120), plus four deeper fixes discovered during regression triage. All discovered issues are now addressed end-to-end rather than papered over with tail-return fallbacks.

Codex review findings:

R1 (C++ diamond): The cpp-diamond-inheritance fixture used non-virtual inheritance, which is genuinely ambiguous in real C++ (two Base subobjects). Changed A and B to use 'virtual public Base' so there's a single shared Base subobject and d.method() is an unambiguous call that the leftmost-base MRO walk correctly resolves.

R2 (C# default-interface): The csharp-interface-default-method fixture called user.Validate() via a User-typed variable, but C# does not inherit default interface methods as callable class members — the call is only valid through an interface-typed variable. Changed App.cs to 'IValidator user = new User(...)' which is the idiomatic dispatch pattern.

R3 (resolveCallTarget tail-return): When D1-D4 receiver filtering produced zero file-matched and zero owner-matched candidates for a member call, the function fell through to the permissive single-candidate tail return — silently emitting CALLS edges for methods that don't belong to the receiver. Added an explicit null-route inside the D1-D4 block that fires only when both filters yielded 0.

R4 (Rust negative assertion): Added the c.trait_only() negative integration test in rust.test.ts demonstrating that direct member calls on Rust structs do not walk trait ancestry. The test now passes because of R3 (previously fell through to the tail return).

Regression triage discoveries:

1. D0 was dead code on the sequential pipeline. The sequential path sets overloadHints for every call regardless of whether the method is overloaded, and the original D0 skip condition '!overloadHints && !preComputedArgTypes' was therefore always false. The Java/C#/C++ SM-9/SM-10 inheritance tests were passing ONLY via the tail-return fallback. Fix: narrow the skip to 'overloadHints && filteredCandidates.length > 1' — skip D0 only when there are actually multiple candidates that need overload disambiguation.

2. lookupMethodByOwner couldn't disambiguate arity-differing overloads (e.g. C++ greet() vs greet(string)). With D0 now firing on the sequential path, same-name/different-arity overloads would collapse to an arbitrary first pick. Fix: added an optional argCount parameter to lookupMethodByOwner + lookupMethodByOwnerWithMRO that filters the overload set by parameterCount/requiredParameterCount before the returnType dedup.

3. Python and Rust class methods are captured as Function nodes (not Method) with ownerId set to the class. The methodByOwner index only accepted 'Method' and 'Constructor' types, so Python class methods and Rust trait methods were invisible to D0. Fix: extended the methodByOwner indexing condition to include 'Function' when ownerId is set. This also unlocks the Rust trait-method negative assertion by ensuring the qualified-syntax MRO strategy has something to return null for.

4. D0 was being skipped when a local variable shadowed an imported module name (Python 'from models.c import C; c = C()' creates both a module alias 'c → models/c.py' AND a typed local 'c'). Fix: the D0 skip now gates on 'aliasNarrowed' (a new boolean tracking whether the alias block actually narrowed filteredCandidates) instead of 'hasActiveModuleAlias'. If the method isn't in the aliased module, the receiver is a typed local variable and D0 should run.

5. PHP trait walk missed the HasTimestamps trait because lookupClassByName did not include 'Trait' type. buildHeritageMap uses lookupClassByName to resolve parent names, so 'BaseModel use HasTimestamps' was failing to register an ancestor edge for BaseModel → HasTimestamps. Fix: added 'Trait' to CLASS_TYPES. The trait is now a valid class-like type for heritage resolution (PHP use, Rust impl Trait for Struct, Scala traits).

Test updates:

- Updated the 'no heritageMap' unit test in call-processor.test.ts to assert the correct null-route behavior instead of the old tail-return fallback.

- Added a new unit test asserting Trait inclusion in the class set.

- Updated the 'does NOT include other type-like labels' test to remove Trait from its rejection set.

Verification:

- tsc --noEmit: clean

- vitest run test/unit/: 3016 passed (+1 new Trait inclusion test)

- vitest run test/integration/resolvers/: 1764 passed (+1 new Rust negative assertion)

- Zero regressions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergo Magyar <magyargergo@users.noreply.github.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
2026-04-09 09:52:12 +01:00

1584 lines
62 KiB
TypeScript

/**
* C++: diamond inheritance + include-based imports + ambiguous #include disambiguation
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';
import {
FIXTURES,
CROSS_FILE_FIXTURES,
getRelationships,
getNodesByLabel,
getNodesByLabelFull,
edgeSet,
runPipelineFromRepo,
type PipelineResult,
} from './helpers.js';
// ---------------------------------------------------------------------------
// Heritage: diamond inheritance + include-based imports
// ---------------------------------------------------------------------------
describe('C++ diamond inheritance', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-diamond'), () => {});
}, 60000);
it('detects exactly 4 classes in diamond hierarchy', () => {
expect(getNodesByLabel(result, 'Class')).toEqual(['Animal', 'Duck', 'Flyer', 'Swimmer']);
});
it('emits exactly 4 EXTENDS edges for full diamond', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(extends_.length).toBe(4);
expect(edgeSet(extends_)).toEqual([
'Duck → Flyer',
'Duck → Swimmer',
'Flyer → Animal',
'Swimmer → Animal',
]);
});
it('resolves all 5 #include imports between header/source files', () => {
const imports = getRelationships(result, 'IMPORTS');
expect(imports.length).toBe(5);
expect(edgeSet(imports)).toEqual([
'duck.cpp → duck.h',
'duck.h → flyer.h',
'duck.h → swimmer.h',
'flyer.h → animal.h',
'swimmer.h → animal.h',
]);
});
it('captures speak as Method nodes (declaration in headers + definition in .cpp)', () => {
const methods = getNodesByLabel(result, 'Method');
expect(methods).toContain('speak');
// speak appears in animal.h (virtual declaration), duck.h (override declaration),
// and duck.cpp (out-of-line definition) — all captured as Method nodes
expect(methods.filter((m) => m === 'speak').length).toBeGreaterThanOrEqual(1);
});
it('no OVERRIDES edges target Property nodes', () => {
const overrides = getRelationships(result, 'METHOD_OVERRIDES');
for (const edge of overrides) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
expect(target!.label).not.toBe('Property');
}
});
});
// ---------------------------------------------------------------------------
// Ambiguous: two headers with same class name, #include disambiguates
// ---------------------------------------------------------------------------
describe('C++ ambiguous symbol resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-ambiguous'), () => {});
}, 60000);
it('detects 2 Handler classes', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes.filter((n) => n === 'Handler').length).toBe(2);
expect(classes).toContain('Processor');
});
it('resolves EXTENDS to handler_a.h (not handler_b.h)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(extends_.length).toBe(1);
expect(extends_[0].source).toBe('Processor');
expect(extends_[0].target).toBe('Handler');
expect(extends_[0].targetFilePath).toBe('handler_a.h');
});
it('#include resolves to handler_a.h', () => {
const imports = getRelationships(result, 'IMPORTS');
expect(imports.length).toBe(1);
expect(imports[0].targetFilePath).toBe('handler_a.h');
});
it('all heritage edges point to real graph nodes', () => {
for (const edge of getRelationships(result, 'EXTENDS')) {
const target = result.graph.getNode(edge.rel.targetId);
expect(target).toBeDefined();
expect(target!.properties.name).toBe(edge.target);
}
});
});
describe('C++ call resolution with arity filtering', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-calls'), () => {});
}, 60000);
it('resolves run → write_audit to one.h via arity narrowing', () => {
const calls = getRelationships(result, 'CALLS');
expect(calls.length).toBe(1);
expect(calls[0].source).toBe('run');
expect(calls[0].target).toBe('write_audit');
expect(calls[0].targetFilePath).toBe('one.h');
expect(calls[0].rel.reason).toBe('import-resolved');
});
});
// ---------------------------------------------------------------------------
// Member-call resolution: obj.method() resolves through pipeline
// ---------------------------------------------------------------------------
describe('C++ member-call resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-member-calls'), () => {});
}, 60000);
it('resolves processUser → save as a member call on User', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save');
expect(saveCall).toBeDefined();
expect(saveCall!.source).toBe('processUser');
expect(saveCall!.targetFilePath).toBe('user.h');
});
it('detects User class and save method', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Method')).toContain('save');
});
it('emits HAS_METHOD edge from User to save', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const edge = hasMethod.find((e) => e.source === 'User' && e.target === 'save');
expect(edge).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Constructor resolution: new Foo() resolves to Class
// ---------------------------------------------------------------------------
describe('C++ constructor-call resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-constructor-calls'), () => {});
}, 60000);
it('resolves new User() as a CALLS edge to the User class', () => {
const calls = getRelationships(result, 'CALLS');
const ctorCall = calls.find((c) => c.target === 'User');
expect(ctorCall).toBeDefined();
expect(ctorCall!.source).toBe('processUser');
expect(ctorCall!.targetLabel).toBe('Class');
expect(ctorCall!.targetFilePath).toBe('user.h');
expect(ctorCall!.rel.reason).toBe('import-resolved');
});
it('detects User class and save method', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Method')).toContain('save');
});
it('resolves #include import', () => {
const imports = getRelationships(result, 'IMPORTS');
expect(imports.length).toBe(1);
expect(imports[0].targetFilePath).toBe('user.h');
});
});
// ---------------------------------------------------------------------------
// Receiver-constrained resolution: typed variables disambiguate same-named methods
// ---------------------------------------------------------------------------
describe('C++ receiver-constrained resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-receiver-resolution'), () => {});
}, 60000);
it('detects User and Repo classes, both with save methods', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'save');
expect(saveMethods.length).toBe(2);
});
it('resolves user.save() to User.save and repo.save() to Repo.save via receiver typing', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((c) => c.target === 'save');
expect(saveCalls.length).toBe(2);
const userSave = saveCalls.find((c) => c.targetFilePath === 'user.h');
const repoSave = saveCalls.find((c) => c.targetFilePath === 'repo.h');
expect(userSave).toBeDefined();
expect(repoSave).toBeDefined();
expect(userSave!.source).toBe('processEntities');
expect(repoSave!.source).toBe('processEntities');
});
});
// ---------------------------------------------------------------------------
// Constructor-inferred type resolution: auto user = User(); user.save() → User.save
// Cross-file SymbolTable verification (no explicit type annotations)
// ---------------------------------------------------------------------------
describe('C++ constructor-inferred type resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-constructor-type-inference'),
() => {},
);
}, 60000);
it('detects User and Repo classes, both with save methods', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'save');
expect(saveMethods.length).toBe(2);
});
it('resolves user.save() to models/User.h via constructor-inferred type', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find((c) => c.target === 'save' && c.targetFilePath === 'models/User.h');
expect(userSave).toBeDefined();
expect(userSave!.source).toBe('processEntities');
});
it('resolves repo.save() to models/Repo.h via constructor-inferred type', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find((c) => c.target === 'save' && c.targetFilePath === 'models/Repo.h');
expect(repoSave).toBeDefined();
expect(repoSave!.source).toBe('processEntities');
});
it('emits exactly 2 save() CALLS edges (one per receiver type)', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((c) => c.target === 'save');
expect(saveCalls.length).toBe(2);
});
});
// ---------------------------------------------------------------------------
// Variadic resolution: C-style variadic (...) doesn't get filtered by arity
// ---------------------------------------------------------------------------
describe('C++ variadic call resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-variadic-resolution'), () => {});
}, 60000);
it('resolves 3-arg call to variadic function log_entry(const char*, ...) in logger.h', () => {
const calls = getRelationships(result, 'CALLS');
const logCall = calls.find((c) => c.target === 'log_entry');
expect(logCall).toBeDefined();
expect(logCall!.source).toBe('main');
expect(logCall!.targetFilePath).toBe('logger.h');
});
});
// ---------------------------------------------------------------------------
// Local shadow: same-file definition takes priority over imported name
// ---------------------------------------------------------------------------
describe('C++ local definition shadows import', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-local-shadow'), () => {});
}, 60000);
it('resolves run → save to same-file definition, not the imported one', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'run');
expect(saveCall).toBeDefined();
expect(saveCall!.targetFilePath).toBe('src/main.cpp');
});
it('does NOT resolve save to utils.h', () => {
const calls = getRelationships(result, 'CALLS');
const saveToUtils = calls.find(
(c) => c.target === 'save' && c.targetFilePath === 'src/utils.h',
);
expect(saveToUtils).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// this->save() resolves to enclosing class's own save method
// ---------------------------------------------------------------------------
describe('C++ this resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-self-this-resolution'), () => {});
}, 60000);
it('detects User and Repo classes, each with a save method', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'save');
expect(saveMethods.length).toBe(2);
});
it('resolves this->save() to User::save in the same file (not Repo::save)', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save');
expect(saveCall).toBeDefined();
expect(saveCall!.targetFilePath).toBe('src/User.cpp');
});
});
// ---------------------------------------------------------------------------
// Parent class resolution: EXTENDS via base_class_clause
// ---------------------------------------------------------------------------
describe('C++ parent resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-parent-resolution'), () => {});
}, 60000);
it('detects BaseModel and User classes', () => {
expect(getNodesByLabel(result, 'Class')).toContain('BaseModel');
expect(getNodesByLabel(result, 'Class')).toContain('User');
});
it('emits EXTENDS edge: User → BaseModel (base_class_clause)', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(extends_.length).toBe(1);
expect(extends_[0].source).toBe('User');
expect(extends_[0].target).toBe('BaseModel');
});
});
// ---------------------------------------------------------------------------
// Brace-init constructor inference: auto x = User{}; x.save() → User.save
// ---------------------------------------------------------------------------
describe('C++ brace-init constructor inference', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-brace-init-inference'), () => {});
}, 60000);
it('detects User and Repo classes, both with save methods', () => {
expect(getNodesByLabel(result, 'Class')).toEqual(['Repo', 'User']);
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'save');
expect(saveMethods.length).toBe(2);
});
it('resolves user.save() to User.save via brace-init', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find((c) => c.target === 'save' && c.targetFilePath === 'models/User.h');
expect(userSave).toBeDefined();
});
it('resolves repo.save() to Repo.save via brace-init', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find((c) => c.target === 'save' && c.targetFilePath === 'models/Repo.h');
expect(repoSave).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// C++ scoped brace-init: auto x = ns::HttpClient{}
// ---------------------------------------------------------------------------
describe('C++ scoped brace-init resolution (ns::Type{})', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-scoped-brace-init'), () => {});
}, 60000);
it('resolves client.connect() via ns::HttpClient{} scoped brace-init', () => {
const calls = getRelationships(result, 'CALLS');
const connectCall = calls.find(
(c) => c.target === 'connect' && c.targetFilePath === 'models.h',
);
expect(connectCall).toBeDefined();
expect(connectCall!.source).toBe('run');
});
it('resolves client.send() via ns::HttpClient{} scoped brace-init', () => {
const calls = getRelationships(result, 'CALLS');
const sendCall = calls.find((c) => c.target === 'send' && c.targetFilePath === 'models.h');
expect(sendCall).toBeDefined();
expect(sendCall!.source).toBe('run');
});
});
// ---------------------------------------------------------------------------
// C++ range-based for: for (auto& user : users) — Tier 1c
// ---------------------------------------------------------------------------
describe('C++ range-based for loop resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-range-for'), () => {});
}, 60000);
it('detects User and Repo classes with save methods', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
});
it('resolves user.save() in range-for to User#save', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('User'),
);
expect(userSave).toBeDefined();
});
it('resolves repo.save() in const auto& range-for to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'processRepos' && c.targetFilePath?.includes('Repo'),
);
expect(repoSave).toBeDefined();
});
it('does NOT cross-resolve user.save() to Repo#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('Repo'),
);
expect(wrongSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Return type inference: auto user = getUser("alice"); user.save()
// C++'s CONSTRUCTOR_BINDING_SCANNER captures auto declarations with
// call_expression values, enabling return type inference from function results.
// ---------------------------------------------------------------------------
describe('C++ return type inference via auto + function call', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-return-type'), () => {});
}, 60000);
it('detects User class and getUser function', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Function')).toContain('getUser');
});
it('detects save method on User', () => {
const methods = getNodesByLabel(result, 'Method');
expect(methods).toContain('save');
});
it('resolves user.save() to User#save via return type of getUser(): User', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('user.h'),
);
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Return-type inference with competing methods:
// Two classes both have save(), factory functions disambiguate via return type
// ---------------------------------------------------------------------------
describe('C++ return-type inference via function return type', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-return-type-inference'), () => {});
}, 60000);
it('resolves user.save() to User#save via return type of getUser()', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath.includes('user.h'),
);
expect(saveCall).toBeDefined();
});
it('user.save() does NOT resolve to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find((c) => c.target === 'save' && c.source === 'processUser');
// Should resolve to exactly one target — if it resolves at all, check it's the right one
if (wrongSave) {
expect(wrongSave.targetFilePath).toContain('user.h');
}
});
it('resolves repo.save() to Repo#save via return type of getRepo()', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) =>
c.target === 'save' && c.source === 'processRepo' && c.targetFilePath.includes('repo.h'),
);
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Nullable receiver unwrapping: User* pointer type stripped for resolution
// ---------------------------------------------------------------------------
describe('C++ nullable receiver resolution (pointer types)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-nullable-receiver'), () => {});
}, 60000);
it('detects User and Repo classes with competing save methods', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
const saveMethods = getNodesByLabel(result, 'Method').filter((m: string) => m === 'save');
expect(saveMethods.length).toBe(2);
});
it('resolves user->save() to User#save via pointer receiver typing', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'processEntities' &&
c.targetFilePath.includes('User.h'),
);
expect(userSave).toBeDefined();
});
it('resolves repo->save() to Repo#save via pointer receiver typing', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'processEntities' &&
c.targetFilePath.includes('Repo.h'),
);
expect(repoSave).toBeDefined();
});
it('does NOT cross-contaminate (exactly 1 save per receiver file)', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((c) => c.target === 'save' && c.source === 'processEntities');
const userTargeted = saveCalls.filter((c) => c.targetFilePath.includes('User.h'));
const repoTargeted = saveCalls.filter((c) => c.targetFilePath.includes('Repo.h'));
expect(userTargeted.length).toBe(1);
expect(repoTargeted.length).toBe(1);
});
});
// ---------------------------------------------------------------------------
// C++ assignment chain propagation: auto alias = u; alias.save()
// Tests extractPendingAssignment for C++ auto declarations.
// ---------------------------------------------------------------------------
describe('C++ assignment chain propagation (auto alias)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-assignment-chain'), () => {});
}, 60000);
it('detects User and Repo classes each with a save method', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'save');
expect(saveMethods.length).toBe(2);
});
it('resolves alias.save() to User#save via auto assignment chain', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'processEntities' &&
c.targetFilePath?.includes('User.h'),
);
expect(userSave).toBeDefined();
});
it('resolves rAlias.save() to Repo#save via auto assignment chain', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'processEntities' &&
c.targetFilePath?.includes('Repo.h'),
);
expect(repoSave).toBeDefined();
});
it('each alias resolves to its own class, not the other', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((c) => c.target === 'save' && c.source === 'processEntities');
const userTargeted = saveCalls.filter((c) => c.targetFilePath?.includes('User.h'));
const repoTargeted = saveCalls.filter((c) => c.targetFilePath?.includes('Repo.h'));
expect(userTargeted.length).toBe(1);
expect(repoTargeted.length).toBe(1);
});
});
// ---------------------------------------------------------------------------
// Chained method calls: svc.getUser().save()
// Tests that C++ chain call resolution correctly infers the intermediate
// receiver type from getUser()'s return type and resolves save() to User.
// ---------------------------------------------------------------------------
describe('C++ chained method call resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-chain-call'), () => {});
}, 60000);
it('detects User, Repo, and UserService classes', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('User');
expect(classes).toContain('Repo');
expect(classes).toContain('UserService');
});
it('detects getUser and save symbols', () => {
const allSymbols = [
...getNodesByLabel(result, 'Function'),
...getNodesByLabel(result, 'Method'),
];
expect(allSymbols).toContain('getUser');
expect(allSymbols).toContain('save');
});
it('resolves svc.getUser().save() to User#save via chain resolution', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath?.includes('user.h'),
);
expect(userSave).toBeDefined();
});
it('does NOT resolve svc.getUser().save() to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'processUser' && c.targetFilePath?.includes('repo.h'),
);
expect(repoSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// C++ structured binding in range-for: for (auto& [key, user] : userMap)
// ---------------------------------------------------------------------------
describe('C++ structured binding in range-for', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-structured-binding'), () => {});
}, 60000);
it('detects User and Repo classes with save methods', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'save');
expect(saveMethods.length).toBe(2);
});
it('resolves user.save() in structured binding for-loop to User#save', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'processUserMap' &&
c.targetFilePath?.includes('User.h'),
);
expect(userSave).toBeDefined();
});
it('resolves repo.save() in structured binding for-loop to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'processRepoMap' &&
c.targetFilePath?.includes('Repo.h'),
);
expect(repoSave).toBeDefined();
});
it('does NOT cross-resolve user.save() to Repo#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'processUserMap' &&
c.targetFilePath?.includes('Repo.h'),
);
expect(wrongSave).toBeUndefined();
});
it('does NOT cross-resolve repo.save() to User#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' &&
c.source === 'processRepoMap' &&
c.targetFilePath?.includes('User.h'),
);
expect(wrongSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// C++ pointer dereference in range-for: for (auto& user : *ptr)
// ---------------------------------------------------------------------------
describe('C++ pointer dereference in range-for', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-deref-range-for'), () => {});
}, 60000);
it('detects User and Repo classes with save methods', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Class')).toContain('Repo');
});
it('resolves user.save() in *usersPtr range-for to User#save', () => {
const calls = getRelationships(result, 'CALLS');
const userSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('User'),
);
expect(userSave).toBeDefined();
});
it('resolves repo.save() in *reposPtr range-for to Repo#save', () => {
const calls = getRelationships(result, 'CALLS');
const repoSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'processRepos' && c.targetFilePath?.includes('Repo'),
);
expect(repoSave).toBeDefined();
});
it('does NOT cross-resolve user.save() to Repo#save (negative)', () => {
const calls = getRelationships(result, 'CALLS');
const wrongSave = calls.find(
(c) =>
c.target === 'save' && c.source === 'processUsers' && c.targetFilePath?.includes('Repo'),
);
expect(wrongSave).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// Phase 8: Field/property type resolution (1-level)
// ---------------------------------------------------------------------------
describe('Field type resolution (C++)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-field-types'), () => {});
}, 60000);
it('detects classes: Address, User', () => {
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']);
});
it('detects Property nodes for C++ data member fields', () => {
const properties = getNodesByLabel(result, 'Property');
expect(properties).toContain('address');
expect(properties).toContain('name');
expect(properties).toContain('city');
});
it('emits HAS_PROPERTY edges linking fields to classes', () => {
const propEdges = getRelationships(result, 'HAS_PROPERTY');
expect(propEdges.length).toBe(3);
expect(edgeSet(propEdges)).toContain('User → address');
expect(edgeSet(propEdges)).toContain('User → name');
expect(edgeSet(propEdges)).toContain('Address → city');
});
it('resolves user.address.save() → Address#save via field type', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((e) => e.target === 'save');
const addressSave = saveCalls.find(
(e) => e.source === 'processUser' && e.targetFilePath.includes('models'),
);
expect(addressSave).toBeDefined();
});
it('populates field metadata (visibility, declaredType) on Property nodes', () => {
const properties = getNodesByLabelFull(result, 'Property');
const city = properties.find((p) => p.name === 'city');
expect(city).toBeDefined();
expect(city!.properties.visibility).toBe('public');
expect(city!.properties.isStatic).toBe(false);
expect(city!.properties.isReadonly).toBe(false);
const addr = properties.find((p) => p.name === 'address');
expect(addr).toBeDefined();
expect(addr!.properties.visibility).toBe('public');
});
});
// ---------------------------------------------------------------------------
// Phase 8A: Deep field chain resolution (3-level)
// ---------------------------------------------------------------------------
describe('Deep field chain resolution (C++)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-deep-field-chain'), () => {});
}, 60000);
it('detects classes: Address, City, User', () => {
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'City', 'User']);
});
it('detects Property nodes for all typed fields', () => {
const properties = getNodesByLabel(result, 'Property');
expect(properties).toContain('address');
expect(properties).toContain('city');
expect(properties).toContain('zipCode');
});
it('emits HAS_PROPERTY edges for nested type chain', () => {
const propEdges = getRelationships(result, 'HAS_PROPERTY');
expect(edgeSet(propEdges)).toContain('User → address');
expect(edgeSet(propEdges)).toContain('Address → city');
expect(edgeSet(propEdges)).toContain('City → zipCode');
});
it('resolves 2-level chain: user.address.save() → Address#save', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((e) => e.target === 'save' && e.source === 'processUser');
const addressSave = saveCalls.find((e) => e.targetFilePath.includes('models'));
expect(addressSave).toBeDefined();
});
it('resolves 3-level chain: user.address.city.getName() → City#getName', () => {
const calls = getRelationships(result, 'CALLS');
const getNameCalls = calls.filter((e) => e.target === 'getName' && e.source === 'processUser');
const cityGetName = getNameCalls.find((e) => e.targetFilePath.includes('models'));
expect(cityGetName).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Pointer and reference member fields (Address* address; Address& ref_address;)
// ---------------------------------------------------------------------------
describe('C++ pointer/reference member field capture', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-pointer-ref-fields'), () => {});
}, 60000);
it('detects classes: Address, User', () => {
expect(getNodesByLabel(result, 'Class')).toEqual(['Address', 'User']);
});
it('detects Property nodes for pointer and reference member fields', () => {
const properties = getNodesByLabel(result, 'Property');
expect(properties).toContain('address');
expect(properties).toContain('ref_address');
expect(properties).toContain('name');
expect(properties).toContain('city');
});
it('emits HAS_PROPERTY edges for pointer/reference fields', () => {
const propEdges = getRelationships(result, 'HAS_PROPERTY');
expect(edgeSet(propEdges)).toContain('User → address');
expect(edgeSet(propEdges)).toContain('User → ref_address');
expect(edgeSet(propEdges)).toContain('User → name');
});
});
// ACCESSES write edges from assignment expressions
// ---------------------------------------------------------------------------
describe('Write access tracking (C++)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-write-access'), () => {});
}, 60000);
it('emits ACCESSES write edges for field assignments', () => {
const accesses = getRelationships(result, 'ACCESSES');
const writes = accesses.filter((e) => e.rel.reason === 'write');
expect(writes.length).toBe(2);
const fieldNames = writes.map((e) => e.target);
expect(fieldNames).toContain('name');
expect(fieldNames).toContain('address');
const sources = writes.map((e) => e.source);
expect(sources).toContain('updateUser');
});
it('write ACCESSES edges have confidence 1.0', () => {
const accesses = getRelationships(result, 'ACCESSES');
const writes = accesses.filter((e) => e.rel.reason === 'write');
for (const edge of writes) {
expect(edge.rel.confidence).toBe(1.0);
}
});
});
// ---------------------------------------------------------------------------
// Call-result variable binding (Phase 9): auto user = getUser(); user.save()
// ---------------------------------------------------------------------------
describe('C++ call-result variable binding (Tier 2b)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-call-result-binding'), () => {});
}, 60000);
it('resolves user.save() to User#save via call-result binding with auto', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'processUser');
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Method chain binding (Phase 9C): getUser() → .address → .getCity() → .save()
// ---------------------------------------------------------------------------
describe('C++ method chain binding via unified fixpoint (Phase 9C)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-method-chain-binding'), () => {});
}, 60000);
it('resolves city.save() to City#save via method chain with auto', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find((c) => c.target === 'save' && c.source === 'processChain');
expect(saveCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Phase B: Deep MRO — walkParentChain() at depth 2 (C→B→A)
// greet() is defined on A, accessed via C. Tests BFS depth-2 parent traversal.
// ---------------------------------------------------------------------------
describe('C++ grandparent method resolution via MRO (Phase B)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-grandparent-resolution'), () => {});
}, 60000);
it('detects A, B, C, Greeting classes', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('A');
expect(classes).toContain('B');
expect(classes).toContain('C');
expect(classes).toContain('Greeting');
});
it('emits EXTENDS edges: B→A, C→B', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toContain('B → A');
expect(edgeSet(extends_)).toContain('C → B');
});
it('resolves c.greet().save() to Greeting#save via depth-2 MRO lookup', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) => c.target === 'save' && c.targetFilePath.includes('Greeting'),
);
expect(saveCall).toBeDefined();
});
it('resolves c.greet() to A#greet (method found via MRO walk)', () => {
const calls = getRelationships(result, 'CALLS');
const greetCall = calls.find((c) => c.target === 'greet' && c.targetFilePath.includes('A.h'));
expect(greetCall).toBeDefined();
});
});
// ── Phase P: Overload Disambiguation via Parameter Types ─────────────────
describe('C++ overload disambiguation by parameter types', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-overload-param-types'), () => {});
}, 60000);
it('produces distinct graph nodes for same-arity overloads via type-hash suffix', () => {
const methods = getNodesByLabelFull(result, 'Method');
const lookupNodes = methods.filter((m) => m.name === 'lookup');
// Type-hash disambiguation → 2 distinct graph nodes
expect(lookupNodes.length).toBe(2);
const types = lookupNodes.map((n) => n.properties.parameterTypes).sort();
expect(types).toEqual([['int'], ['string']]);
});
it('callById() emits exactly one CALLS edge to lookup(int)', () => {
const calls = getRelationships(result, 'CALLS');
const fromCallById = calls.filter((c) => c.source === 'callById' && c.target === 'lookup');
expect(fromCallById.length).toBe(1);
const targetNode = result.graph.getNode(fromCallById[0].rel.targetId);
expect(targetNode?.properties.parameterTypes).toEqual(['int']);
});
it('callByName() emits exactly one CALLS edge to lookup(string)', () => {
const calls = getRelationships(result, 'CALLS');
const fromCallByName = calls.filter((c) => c.source === 'callByName' && c.target === 'lookup');
expect(fromCallByName.length).toBe(1);
const targetNode = result.graph.getNode(fromCallByName[0].rel.targetId);
expect(targetNode?.properties.parameterTypes).toEqual(['string']);
});
});
// ── Phase P: Same-arity overloads — cross-file + chain resolution ─────────
describe('C++ same-arity overload cross-file and chain resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-same-arity-cross-file'), () => {});
}, 60000);
it('callById() emits exactly one CALLS edge to find(int) in DbLookup', () => {
const calls = getRelationships(result, 'CALLS');
const edges = calls.filter(
(c) =>
c.source === 'callById' && c.target === 'find' && c.targetFilePath.includes('db_lookup'),
);
expect(edges.length).toBe(1);
const targetNode = result.graph.getNode(edges[0].rel.targetId);
expect(targetNode?.properties.parameterTypes).toEqual(['int']);
});
it('callByName() emits exactly one CALLS edge to find(string) in DbLookup', () => {
const calls = getRelationships(result, 'CALLS');
const edges = calls.filter(
(c) =>
c.source === 'callByName' && c.target === 'find' && c.targetFilePath.includes('db_lookup'),
);
expect(edges.length).toBe(1);
const targetNode = result.graph.getNode(edges[0].rel.targetId);
expect(targetNode?.properties.parameterTypes).toEqual(['string']);
});
it('chainIntToFormat() — find(42) → find(int), format(result) → format(string)', () => {
const calls = getRelationships(result, 'CALLS');
const findEdges = calls.filter((c) => c.source === 'chainIntToFormat' && c.target === 'find');
const formatEdges = calls.filter(
(c) => c.source === 'chainIntToFormat' && c.target === 'format',
);
expect(findEdges.length).toBe(1);
const findTarget = result.graph.getNode(findEdges[0].rel.targetId);
expect(findTarget?.properties.parameterTypes).toEqual(['int']);
expect(formatEdges.length).toBe(1);
const formatTarget = result.graph.getNode(formatEdges[0].rel.targetId);
expect(formatTarget?.properties.parameterTypes).toEqual(['string']);
});
it('chainNameToFormat() — find("alice") → find(string), format(result) → format(string)', () => {
const calls = getRelationships(result, 'CALLS');
const findEdges = calls.filter((c) => c.source === 'chainNameToFormat' && c.target === 'find');
const formatEdges = calls.filter(
(c) => c.source === 'chainNameToFormat' && c.target === 'format',
);
expect(findEdges.length).toBe(1);
const findTarget = result.graph.getNode(findEdges[0].rel.targetId);
expect(findTarget?.properties.parameterTypes).toEqual(['string']);
expect(formatEdges.length).toBe(1);
const formatTarget = result.graph.getNode(formatEdges[0].rel.targetId);
expect(formatTarget?.properties.parameterTypes).toEqual(['string']);
});
});
// ---------------------------------------------------------------------------
// C++ smart pointer virtual dispatch via std::make_shared<T>()
// ---------------------------------------------------------------------------
describe('C++ smart pointer virtual dispatch via make_shared', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-smart-ptr-dispatch'), () => {});
}, 60000);
it('detects Dog and Animal classes', () => {
expect(getNodesByLabel(result, 'Class')).toContain('Animal');
expect(getNodesByLabel(result, 'Class')).toContain('Dog');
});
it('emits CALLS edge from process → speak', () => {
const calls = getRelationships(result, 'CALLS');
const speakCall = calls.find((c) => c.source === 'process' && c.target === 'speak');
expect(speakCall).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// C++ default parameter arity resolution
// ---------------------------------------------------------------------------
describe('C++ default parameter arity resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-default-params'), () => {});
}, 60000);
it('resolves greet("Alice") with 1 arg to greet with 2 params (1 default)', () => {
const calls = getRelationships(result, 'CALLS');
const greetCalls = calls.filter((c) => c.source === 'process' && c.target === 'greet');
expect(greetCalls.length).toBe(1);
});
});
// ---------------------------------------------------------------------------
// Phase 14: Cross-file binding propagation (via synthesized wildcard imports)
// models/user.h declares User class with save() and get_name() methods
// models/user_factory.h declares User get_user() free function
// app/main.cpp includes user_factory.h, calls get_user().save()
// → user is typed User via cross-file return type propagation
// ---------------------------------------------------------------------------
describe('C++ cross-file binding propagation', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(CROSS_FILE_FIXTURES, 'cpp-cross-file'), () => {});
}, 60000);
it('detects User class with save and get_name methods', () => {
expect(getNodesByLabel(result, 'Class')).toContain('User');
expect(getNodesByLabel(result, 'Method')).toContain('save');
expect(getNodesByLabel(result, 'Method')).toContain('get_name');
});
it('detects get_user factory function and process consumer', () => {
expect(getNodesByLabel(result, 'Function')).toContain('get_user');
expect(getNodesByLabel(result, 'Function')).toContain('process');
});
it('emits IMPORTS edge from main.cpp to headers', () => {
const imports = getRelationships(result, 'IMPORTS');
const edge = imports.find(
(e) => e.sourceFilePath.includes('main') && e.targetFilePath.includes('models'),
);
expect(edge).toBeDefined();
});
it('resolves user.save() in process() to User#save via cross-file propagation', () => {
const calls = getRelationships(result, 'CALLS');
const saveCall = calls.find(
(c) => c.target === 'save' && c.source === 'process' && c.targetFilePath.includes('models'),
);
expect(saveCall).toBeDefined();
});
it('resolves user.get_name() in process() to User#get_name via cross-file propagation', () => {
const calls = getRelationships(result, 'CALLS');
const getNameCall = calls.find(
(c) =>
c.target === 'get_name' && c.source === 'process' && c.targetFilePath.includes('models'),
);
expect(getNameCall).toBeDefined();
});
it('emits HAS_METHOD edges linking save and get_name to User (via header declarations)', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const saveEdge = hasMethod.find((e) => e.source === 'User' && e.target === 'save');
const getNameEdge = hasMethod.find((e) => e.source === 'User' && e.target === 'get_name');
expect(saveEdge).toBeDefined();
expect(getNameEdge).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// Method enrichment: pure virtual, static, concrete methods + EXTENDS
// ---------------------------------------------------------------------------
describe('C++ method enrichment', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-method-enrichment'), () => {});
}, 60000);
it('detects Animal and Dog classes', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('Animal');
expect(classes).toContain('Dog');
});
it('emits HAS_METHOD edges for Animal', () => {
const hasMethod = getRelationships(result, 'HAS_METHOD');
const animalMethods = hasMethod.filter((e) => e.source === 'Animal').map((e) => e.target);
expect(animalMethods).toContain('speak');
expect(animalMethods).toContain('classify');
expect(animalMethods).toContain('breathe');
});
it('marks pure virtual speak as isAbstract (conditional)', () => {
const methods = getNodesByLabelFull(result, 'Function');
const speak = methods.find((n) => n.name === 'speak' && n.properties.filePath === 'animal.hpp');
if (speak?.properties.isAbstract !== undefined) {
expect(speak.properties.isAbstract).toBe(true);
}
});
it('marks classify as isStatic (conditional)', () => {
const methods = getNodesByLabelFull(result, 'Function');
const classify = methods.find((n) => n.name === 'classify');
if (classify?.properties.isStatic !== undefined) {
expect(classify.properties.isStatic).toBe(true);
}
});
it('populates parameterTypes for classify (conditional)', () => {
const methods = getNodesByLabelFull(result, 'Function');
const classify = methods.find((n) => n.name === 'classify');
if (classify?.properties.parameterTypes !== undefined) {
expect(classify.properties.parameterTypes.length).toBeGreaterThan(0);
}
});
});
// ── Phase P: C++ const-qualified method overload disambiguation ───────────
describe('C++ const-qualified method overload disambiguation', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-const-overload'), () => {});
}, 60000);
it('produces distinct nodes for begin() and begin() const', () => {
const methods = getNodesByLabelFull(result, 'Method');
const beginNodes = methods.filter((m) => m.name === 'begin');
expect(beginNodes.length).toBe(2);
const constFlags = beginNodes.map((n) => !!n.properties.isConst).sort();
expect(constFlags).toEqual([false, true]);
});
it('produces distinct nodes for end() and end() const', () => {
const methods = getNodesByLabelFull(result, 'Method');
const endNodes = methods.filter((m) => m.name === 'end');
expect(endNodes.length).toBe(2);
const constFlags = endNodes.map((n) => !!n.properties.isConst).sort();
expect(constFlags).toEqual([false, true]);
});
it('single const method (size) has isConst but no $const suffix (no collision)', () => {
const methods = getNodesByLabelFull(result, 'Method');
const sizeNodes = methods.filter((m) => m.name === 'size');
expect(sizeNodes.length).toBe(1);
expect(sizeNodes[0].properties.isConst).toBe(true);
});
it('callNonConst has isConst falsy, callConst has isConst true', () => {
const methods = getNodesByLabelFull(result, 'Method');
const callNonConst = methods.find((m) => m.name === 'callNonConst');
const callConst = methods.find((m) => m.name === 'callConst');
expect(callNonConst).toBeDefined();
expect(callConst).toBeDefined();
expect(callNonConst!.properties.isConst).toBeFalsy();
expect(callConst!.properties.isConst).toBe(true);
});
});
// ── Phase P: C++ const-qualified cross-file + chain resolution ────────────
describe('C++ const-qualified cross-file and chain resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-const-cross-file'), () => {});
}, 60000);
// -- Cross-file: const vs non-const get() called from App --
it('Container.get has distinct const and non-const nodes', () => {
const methods = getNodesByLabelFull(result, 'Method');
const getNodes = methods.filter(
(m) => m.name === 'get' && m.properties.filePath?.includes('container'),
);
expect(getNodes.length).toBe(2);
const constFlags = getNodes.map((n) => !!n.properties.isConst).sort();
expect(constFlags).toEqual([false, true]);
});
it('Container.size has distinct const and non-const nodes', () => {
const methods = getNodesByLabelFull(result, 'Method');
const sizeNodes = methods.filter(
(m) => m.name === 'size' && m.properties.filePath?.includes('container'),
);
expect(sizeNodes.length).toBe(2);
const constFlags = sizeNodes.map((n) => !!n.properties.isConst).sort();
expect(constFlags).toEqual([false, true]);
});
// -- Chain: format() calls resolve cross-file via receiver-type propagation --
it('chainMutableGet() calls format cross-file via string receiver type', () => {
const calls = getRelationships(result, 'CALLS');
const fmtEdges = calls.filter((c) => c.source === 'chainMutableGet' && c.target === 'format');
expect(fmtEdges.length).toBe(1);
const fmtTarget = result.graph.getNode(fmtEdges[0].rel.targetId);
expect(fmtTarget?.properties.parameterTypes).toEqual(['string']);
});
it('chainConstSize() calls format cross-file via int receiver type', () => {
const calls = getRelationships(result, 'CALLS');
const fmtEdges = calls.filter((c) => c.source === 'chainConstSize' && c.target === 'format');
expect(fmtEdges.length).toBe(1);
const fmtTarget = result.graph.getNode(fmtEdges[0].rel.targetId);
expect(fmtTarget?.properties.parameterTypes).toEqual(['int']);
});
});
// ── Phase P: C++ template overload disambiguation ─────────────────────────
describe('C++ template overload disambiguation (vector<int> vs vector<string>)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-template-overload'), () => {});
}, 60000);
it('produces distinct nodes for process(vector<int>) and process(vector<string>)', () => {
const methods = getNodesByLabelFull(result, 'Method');
const processNodes = methods.filter((m) => m.name === 'process');
expect(processNodes.length).toBe(2);
});
it('each process() node has distinct parameterTypes (simplified)', () => {
const methods = getNodesByLabelFull(result, 'Method');
const processNodes = methods.filter((m) => m.name === 'process');
// Both have type 'vector' after extractSimpleTypeName, but distinct node IDs
// from rawType-based type-hash (~vector<int> vs ~vector<std::string>)
const types = processNodes.map((n) => n.properties.parameterTypes);
// Both have simplified 'vector' as parameterTypes[0], but they're separate nodes
expect(types.length).toBe(2);
});
it('the two process() nodes have different graph IDs', () => {
const ids: string[] = [];
result.graph.forEachNode((n) => {
if (n.properties.name === 'process' && n.label === 'Method') {
ids.push(n.id);
}
});
expect(ids.length).toBe(2);
expect(ids[0]).not.toBe(ids[1]);
});
});
// ── Phase P: C++ template overload cross-file + chain resolution ──────────
describe('C++ template overload cross-file and chain resolution', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-template-cross-file'), () => {});
}, 60000);
// -- Cross-file: template-overloaded process() defined in processor.h, called from app.cpp --
it('Processor.process has distinct nodes for vector<int> and vector<string>', () => {
const methods = getNodesByLabelFull(result, 'Method');
const processNodes = methods.filter(
(m) => m.name === 'process' && m.properties.filePath?.includes('processor'),
);
expect(processNodes.length).toBe(2);
// Verify they have different startLine (proof of distinct nodes, not ID collision)
const lines = processNodes.map((n) => n.properties.startLine).sort();
expect(lines[0]).not.toBe(lines[1]);
});
// -- Chain: format(int) and format(string) called cross-file from App --
it('chainIntToFormat() emits exactly one CALLS edge to format(int)', () => {
const calls = getRelationships(result, 'CALLS');
const edges = calls.filter(
(c) =>
c.source === 'chainIntToFormat' &&
c.target === 'format' &&
c.targetFilePath.includes('formatter'),
);
expect(edges.length).toBe(1);
const targetNode = result.graph.getNode(edges[0].rel.targetId);
expect(targetNode?.properties.parameterTypes).toEqual(['int']);
});
it('chainStringToFormat() emits exactly one CALLS edge to format(string)', () => {
const calls = getRelationships(result, 'CALLS');
const edges = calls.filter(
(c) =>
c.source === 'chainStringToFormat' &&
c.target === 'format' &&
c.targetFilePath.includes('formatter'),
);
expect(edges.length).toBe(1);
const targetNode = result.graph.getNode(edges[0].rel.targetId);
expect(targetNode?.properties.parameterTypes).toEqual(['string']);
});
});
// ── Phase P: C++ out-of-class method definition + overload disambiguation ─
describe('C++ out-of-class method definition with overloaded declarations', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-out-of-class-method'), () => {});
}, 60000);
it('header declarations produce Method nodes for greet() and greet(string)', () => {
const methods = getNodesByLabelFull(result, 'Method');
const greetNodes = methods.filter(
(m) => m.name === 'greet' && m.properties.filePath?.includes('myclass'),
);
// greet() (arity 0) and greet(string) (arity 1) have different arity → distinct IDs
expect(greetNodes.length).toBeGreaterThanOrEqual(2);
});
it('header declarations produce Method nodes for getName() and getName(int)', () => {
const methods = getNodesByLabelFull(result, 'Method');
const getNameNodes = methods.filter(
(m) => m.name === 'getName' && m.properties.filePath?.includes('myclass'),
);
expect(getNameNodes.length).toBeGreaterThanOrEqual(2);
});
it('callGreetDefault() emits exactly one CALLS edge to greet (arity 0)', () => {
const calls = getRelationships(result, 'CALLS');
const edges = calls.filter((c) => c.source === 'callGreetDefault' && c.target === 'greet');
expect(edges.length).toBe(1);
const targetNode = result.graph.getNode(edges[0].rel.targetId);
expect(targetNode?.properties.parameterCount).toBe(0);
});
it('callGreetMsg() emits exactly one CALLS edge to greet(string)', () => {
const calls = getRelationships(result, 'CALLS');
const edges = calls.filter((c) => c.source === 'callGreetMsg' && c.target === 'greet');
expect(edges.length).toBe(1);
const targetNode = result.graph.getNode(edges[0].rel.targetId);
expect(targetNode?.properties.parameterTypes).toEqual(['string']);
});
it('callGetNameDefault() emits exactly one CALLS edge to getName (arity 0)', () => {
const calls = getRelationships(result, 'CALLS');
const edges = calls.filter((c) => c.source === 'callGetNameDefault' && c.target === 'getName');
expect(edges.length).toBe(1);
const targetNode = result.graph.getNode(edges[0].rel.targetId);
expect(targetNode?.properties.parameterCount).toBe(0);
});
it('callGetNameById() emits exactly one CALLS edge to getName(int)', () => {
const calls = getRelationships(result, 'CALLS');
const edges = calls.filter((c) => c.source === 'callGetNameById' && c.target === 'getName');
expect(edges.length).toBe(1);
const targetNode = result.graph.getNode(edges[0].rel.targetId);
expect(targetNode?.properties.parameterTypes).toEqual(['int']);
});
});
// ---------------------------------------------------------------------------
// SM-9: lookupMethodByOwnerWithMRO — c.parentMethod() via leftmost-base walk
// ---------------------------------------------------------------------------
describe('C++ Child extends Parent — inherited method resolution (SM-9)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-child-extends-parent'), () => {});
}, 60000);
it('detects Parent and Child classes', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('Parent');
expect(classes).toContain('Child');
});
it('emits EXTENDS edge: Child → Parent', () => {
const extends_ = getRelationships(result, 'EXTENDS');
expect(edgeSet(extends_)).toContain('Child → Parent');
});
it('resolves c.parentMethod() to Parent.parentMethod via leftmost-base MRO walk', () => {
const calls = getRelationships(result, 'CALLS');
const parentMethodCall = calls.find(
(c) => c.target === 'parentMethod' && c.targetFilePath.includes('Parent.h'),
);
expect(parentMethodCall).toBeDefined();
expect(parentMethodCall!.source).toBe('run');
});
});
describe('C++ Derived : A, B — diamond inheritance via leftmost-base MRO (SM-11)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-diamond-inheritance'), () => {});
}, 60000);
it('detects Base, A, B, and Derived classes', () => {
const classes = getNodesByLabel(result, 'Class');
expect(classes).toContain('Base');
expect(classes).toContain('A');
expect(classes).toContain('B');
expect(classes).toContain('Derived');
});
it('emits EXTENDS edges for both branches: A → Base, B → Base, Derived → A, Derived → B', () => {
const extends_ = getRelationships(result, 'EXTENDS');
const edges = edgeSet(extends_);
expect(edges).toContain('A → Base');
expect(edges).toContain('B → Base');
expect(edges).toContain('Derived → A');
expect(edges).toContain('Derived → B');
});
it('resolves d.method() to Base::method via leftmost-base MRO walk', () => {
const calls = getRelationships(result, 'CALLS');
const methodCall = calls.find(
(c) => c.target === 'method' && c.targetFilePath.includes('Base.h'),
);
expect(methodCall).toBeDefined();
expect(methodCall!.source).toBe('run');
});
});