GitNexus/gitnexus/test/integration/resolvers/cpp.test.ts
WENJIE HUANG e01f0912bc
feat(cpp): migrate C++ to scope-based resolution model (#938) (#1520)
* fix(cpp): complete scope-resolution parity

* fix(ci): resolve formatting, lint errors for PR #1520

- prettier: format arity-metadata.ts, captures.ts, index.ts
- eslint: rename unused HEADER_GLOB to _HEADER_GLOB
- eslint: replace unsafe parser.parse() with parseSourceSafe()
- eslint: suppress intentional console.warn/log in sync.ts
- eslint: remove unused _it import alias in cpp.test.ts

* fix(ci): complete formatting, lint, and typecheck fixes

- prettier: format call-processor.ts, imported-return-types.ts,
  include-extractor.test.ts, cpp-captures.test.ts, cpp-imports.test.ts
- eslint: suppress intentional console.warn in manifest-extractor.ts
- typecheck: restore 'thrift' in ContractType union (was accidentally
  removed) and add thrift case to exhaustive switch in manifest-extractor

* fix(ci): revert unintended group module changes that broke tests

Restore types.ts, config-parser.ts, matching.ts, sync.ts, and
manifest-extractor.ts to upstream/main versions. The original commit
accidentally removed fields (thrift, workspace_deps, exclude_links_paths,
exclude_links_param_only_paths) from DetectConfig/MatchingConfig/ContractType
which are still referenced by matching.test.ts, config-parser.test.ts,
sync.test.ts and other integration tests.

This PR's scope is C++ scope-resolution parity only — group module
type definitions and logic should remain unchanged.

* fix(codeql): address security and quality alerts

- arity-metadata.ts, interpret.ts: replace single-pass template strip
  regex (/<[^>]*>/g) with a while-loop to fully handle nested templates
  like Map<List<int>> — resolves 'Incomplete multi-character sanitization'
- cpp.test.ts: remove unused vitest 'it' import since the file defines
  its own 'it' via createResolverParityIt — resolves 'Assignment to constant'
- include-extractor.test.ts: use fs.mkdtempSync() instead of predictable
  os.tmpdir()+Date.now() paths — resolves 'Insecure temporary file'
- interpret.ts: remove redundant 'name !== undefined' check (already
  guaranteed by early return) — resolves 'Comparison between inconvertible types'

* review: address Claude review findings on PR #1520

- Findings 1-3 (BLOCKERS): restore include-extractor.ts and its test to
  the main baseline. Block-comment fallback regression, suffix-resolve
  false-positive suppression, and the four deleted regression tests
  (#3-#6) are now back. These changes were unrelated to C++ scope
  parity and should not have been in this PR.

- Finding 4 (MAJOR, partial): revert COMPOUND_RECEIVER_MAX_DEPTH 6 to
  4. No C++ test exercises depth > 4 (cpp-chain-call uses a 2-hop
  chain), so the bump risked silent regressions on other migrated
  languages without justification. The wildcard-origin propagation in
  imported-return-types.ts is retained — C++ #include and using
  namespace both emit wildcard-origin bindings (cpp/import-decomposer
  .ts:40,90), so wildcard propagation is causal to C++ parity.

- Finding 6: tighten write-access dedup test with exact per-field
  counts (nameWrites = 2, addrWrites = 1) instead of total-count + sub
  string containment, so a regression in one of the two name writes
  can no longer be masked.

- Finding 8: skipped. Box-drawing characters in cpp/query.ts comments
  match the established convention used in csharp/java/php query
  files.

Finding 5 (int/long normalization tie-breaker) left as documented
follow-up — proper fix requires resolver-level tie-breaker logic and
risks regressing other arity-matching tests.

* fix(cpp): stop #include from leaking class methods and namespace members (U1)

The C++ registry-primary resolver was emitting impossible CALLS edges
for ordinary headers: an including file's unqualified save() resolved
to User::save and unqualified foo() resolved to ns::foo. Two leak
paths converged on localDefs:

1. expandCppWildcardNames (file-local-linkage.ts) iterated the
   flattened localDefs and exported every simple tail, including
   class-owned methods and namespace-contained symbols. Replaced with
   a scope-aware filter: build nodeId -> owning Scope from
   Scope.ownedDefs and skip defs whose owning scope is Namespace or
   Class.

2. The shared global free-call fallback's pickUniqueGlobalCallable
   walks the workspace registry by simple name and would still hit
   class methods / namespace members even with wildcard expansion
   fixed. Plugged the gap via the existing isFileLocalDef hook —
   semantically 'logically invisible cross-file' — by tracking per-
   file non-globally-visible nodeIds (populateCppNonGloballyVisible,
   called from populateOwners) and adding an ownerId !== undefined
   fast-path for class-owned defs.

Side fix in shared finalize-algorithm.ts: when wildcard expansion
resolves to a real target but produces zero propagating names, the
edge was dropped, taking the file-level IMPORTS edge with it.
Preserve the original wildcard edge so #include dependencies survive
even when the header exposes no unqualified bindings.

Tests: cpp-include-no-class-leak, cpp-include-no-namespace-leak, and
cpp-anon-ns-same-file-visible fixtures. Negative tests mode-gated to
REGISTRY_PRIMARY_CPP=1 via the expected-failures registry — legacy
DAG has no scope-aware filtering on the global fallback; backporting
is out of scope. All 2104 resolver integration tests pass under
registry-primary mode.

* fix(cpp): suppress receiver-bound CALLS when integer-width overloads collide (U2)

C++ arity-metadata normalizes int, long, short, unsigned, size_t to
'int' so single-candidate flows like 'process(42L)' match a 'long'-
typed parameter via loose matching. But when both 'process(int)' and
'process(long)' coexist as method overloads, they both end up with
parameterTypes=['int'] in the registry, and pickOverload's narrowing
returns 2 candidates with no way to disambiguate. The previous code
picked candidates[0] arbitrarily, emitting a CALLS edge to the wrong
overload roughly half the time.

Fix:
- Add isOverloadAmbiguousAfterNormalization in overload-narrowing.ts
  that detects >1 candidate sharing identical parameterTypes sequences.
- Have pickOverload return a new OVERLOAD_AMBIGUOUS sentinel when this
  fires.
- In the receiver-bound-calls loop, when pickOverload signals ambiguity,
  suppress the edge AND add the site to handledSites so the late-stage
  emitReferencesViaLookup pass does not re-emit the pre-resolved
  reference. Without the handled-mark, the reference index still
  carries a toDef and emits the same wrong edge.

Graph schema has no ambiguous-target edge model, so emitting two
edges (one per candidate) would require a separate schema change.
Zero-edge is the only safe outcome.

Other languages: the ambiguity check is a precondition gate, not a
behavior change for normal narrowing. Languages whose normalizers do
not collapse distinct types into a single token (verified by grep
over *-arity-metadata.ts) will never produce >1 candidate with
identical parameterTypes from genuinely distinct declarations, so
the branch is effectively C++-only in practice.

Test: cpp-overload-int-long fixture asserts exactly .toBe(0) CALLS
edges. Count=1 = arbitrary pick (the bug); count>1 = unsupported
ambiguous-edge model. Mode-gated to REGISTRY_PRIMARY_CPP=1 — legacy
DAG has no OVERLOAD_AMBIGUOUS wiring; backporting is out of scope.

All 2105 resolver integration tests pass under registry-primary; all
139 cpp tests pass under both modes (3 negative tests skipped in
legacy as documented).

* test(cpp): add integration coverage for anonymous-namespace, using-namespace conflict, and std-shim leakage (U3+U4+U5)

Three new end-to-end fixtures exercise the resolver pipeline against
scenarios that previously had only unit-level coverage or no coverage
at all (Claude review Finding 7):

U3 — cpp-anon-ns-cross-file:
  helper.cpp declares 'namespace { void worker(); }' and calls it
  internally. caller.cpp declares a separate 'void worker()' and calls
  it. Asserts (a) the cross-file CALLS edge from caller's run() does
  not target helper.cpp's anonymous-namespace worker, and (b) the
  same-file edge from helper_entry() to its own worker still resolves
  (positive guard against a 'no edges at all' regression making the
  negative check vacuously pass). Includes a state-isolation guard
  that re-runs the same fixture and asserts identical results,
  proving clearFileLocalNames() is called by the pipeline entry.

U4 — cpp-using-namespace-conflict:
  Two headers each declaring 'namespace a { foo() }' and
  'namespace b { foo() }' respectively, plus a caller doing
  'using namespace a; using namespace b; foo()'. Asserts exactly
  zero CALLS edges. One edge = arbitrary pick (the bug); two edges
  would require an ambiguous-target edge model GitNexus does not
  have. Depends on U1 — without scope-aware filtering, both foo()s
  would already be in the importer's wildcard binding set as simple
  'foo', so the test would pass for the wrong reason.

U5 — cpp-using-namespace-std-smoke:
  Fixture-local 'namespace std { void cout_write(); void println(); }'
  shim rather than real <iostream> — captures the wildcard-leak
  shape deterministically without depending on system-header modeling
  stability (out of scope per plan). Asserts (a) the project-local
  call resolves correctly, (b) no leak to shim STL symbols, and (c)
  no CALLS/ACCESSES edges from the caller into std-shim.h at all.

Negative tests for U2/U4 mode-gated to REGISTRY_PRIMARY_CPP=1 via
the expected-failures registry; legacy DAG lacks the OVERLOAD_AMBIGUOUS
suppression and the namespace-aware filtering, so the leaks persist
there. All 2112 resolver integration tests pass under registry-primary;
all 146 cpp tests pass under both modes (4 negative tests skipped in
legacy as documented).

* chore(autofix): apply prettier + eslint fixes via /autofix command

* fix(cpp): scope-aware isSuperReceiver classification (U1)

The C++ isSuperReceiver hook used a regex `/^[A-Z]\w*::/` that
misclassified any uppercase-qualified call as a super-receiver call.
Singleton::getInstance(), std::Foo::bar(), and PascalCase namespace
calls all entered the super branch, where the absence of an enclosing
class (or wrong MRO context) dropped the resolution entirely.

Fix:
- New optional ScopeResolver hook isSuperReceiverInContext(text,
  callerScope, scopes). Languages where super classification depends
  on caller context define it; receiver-bound-calls.ts prefers it
  when defined and falls back to the simple isSuperReceiver(text)
  otherwise. Other migrated languages (Python, Java, C#, PHP, Go,
  TypeScript) are unchanged.
- C++ implementation: parse the LHS of '::' from the receiver text,
  resolve via findClassBindingInScope, and return true only when
  the LHS is a class-like def in the caller's enclosing class's MRO.
  Returns false for namespace LHS, unresolved LHS, self-class LHS
  (qualified self-calls aren't super), and any non-'::' form.
- Extended the C++ tree-sitter query to capture the LHS of
  qualified_identifier as @reference.receiver so qualified static
  member calls (Singleton::getInstance()) reach the receiver-bound
  Case 2 (class-name receiver) path. Without the receiver capture,
  qualified calls had no explicit receiver and could not resolve
  through any receiver-bound branch.

Test: cpp-namespace-qualified-not-super fixture. Singleton::getInstance()
from a free function asserts exactly 1 CALLS edge through the
qualified-call path. Passes under both REGISTRY_PRIMARY_CPP=1 and =0.

All 2113 resolver integration tests pass; all 147 cpp tests pass under
both modes.

* fix(cpp): suppress receiver-bound CALLS when default-arg overloads collide (U4)

ISO C++ rejects 's.f(1)' as ambiguous when both 'void f(int)' and
'void f(int, int = 0)' are declared on S. The previous resolver
returned the first viable candidate via pickOverload's fallback.

Extended isOverloadAmbiguousAfterNormalization to take an optional
argCount: when provided, the predicate compares only the first
argCount slots of each candidate's parameterTypes. Candidates whose
declared-prefix matches up to argCount are treated as ambiguous
because default arguments make all of them equally viable for the
call.

Without argCount, behavior is unchanged (the original int/long
normalization-collapse contract, full-length equality required).
pickOverload now passes site.arity so default-arg ambiguity fires.

Test: cpp-overload-default-arg-ambiguous fixture. s.f(1) where S has
f(int) and f(int, int = 0) asserts exactly .toBe(0) CALLS edges.
Passes under both REGISTRY_PRIMARY_CPP=1 and =0.

All 2114 resolver integration tests pass; all 148 cpp tests pass
under both modes.

* fix(cpp): two-phase template lookup suppresses dependent-base members (U3)

ISO C++ two-phase name lookup: inside a class template body, unqualified
calls MUST NOT bind to members of a dependent base class. Only this->name
or Base<T>::name forms make the lookup dependent. GCC and Clang both
reject the unqualified form with 'declaration of f must be available'.

Before this fix, GitNexus's global free-call fallback walked the
workspace registry by simple name and bound unqualified calls inside
template bodies to dependent-base members, producing CALLS edges the
compiler would reject.

Implementation:
- New languages/cpp/two-phase-lookup.ts module: per-pipeline state
  recording (className, dependentBaseName) pairs at capture time and
  resolving them to nodeId sets during populateOwners.
- captures.ts detectCppDependentBases walks the AST once finding every
  template_declaration containing a class/struct definition. For each,
  it collects template-parameter names (typename T, class T, non-type
  int N, template-template parameters) and walks each base in the
  base_class_clause checking whether any inner type_identifier matches
  a template parameter. Conservative bias: typename T::U, decltype,
  and template-template-parameter shapes also classified as dependent.
- Extended scope-resolution contract's isCallableVisibleFromCaller
  hook with optional callerScope and scopes fields. C++ implements
  the hook to consult isCppDependentBaseMember: when the candidate
  is a member of a dependent base of the caller's enclosing class,
  the hook returns false and pickUniqueGlobalCallable skips the
  candidate.
- clearFileLocalNames also clears the dependent-base state per
  pipeline run.

Fixtures:
- cpp-two-phase-dependent-base: Derived<T> deriving from Base<T>,
  unqualified f() and i inside Derived's body. Asserts zero CALLS
  edges and zero ACCESSES edges respectively.
- cpp-two-phase-this-qualified, cpp-two-phase-non-dependent-base,
  cpp-two-phase-namespace-free-call-inside-template: positive
  fixtures left as documented gaps (this-> and qualified-name
  resolution inside template bodies are pre-existing resolver
  weaknesses independent of U3). Tracked separately.

Negative test mode-gated to REGISTRY_PRIMARY_CPP=1 via the expected-
failures registry; legacy DAG has no two-phase lookup.

All 2116 resolver integration tests pass under registry-primary; all
150 cpp tests pass under both modes (5 negative tests skipped in legacy
as documented).

* fix(cpp): implement V1 ADL (Koenig lookup) for free-function calls (U2)

Plan 2026-05-13-001 U2. Adds argument-dependent lookup as a new
candidate-generating tier in `emitFreeCallFallback`: when ordinary
unqualified lookup is empty, ADL surfaces candidates from each
value-class-typed argument's enclosing namespace.

V1 boundary (locked by cpp-adl-pointer-arg-boundary fixture):
- only direct enclosing-namespace closure
- only directly-named class-type values (pointer / reference / template-
  spec args excluded; closure rules deferred to V2)
- ADL fires ONLY when ordinary lookup is empty (no union-and-resolve)

Parenthesized name `(f)(s)` suppresses ADL per ISO C++
[basic.lookup.argdep]/3.1. Multi-candidate ambiguity (e.g. `process(int)`
vs `process(long)` after C++ int-width normalization) returns the
ADL_AMBIGUOUS sentinel — caller suppresses entirely, mirroring the
OVERLOAD_AMBIGUOUS contract from plan 2026-05-12-002 U2.

Implementation:
- `cpp/adl.ts` — new module: per-pipeline argInfoBySite + noAdlSites Maps
  populated at capture time, classToNamespaceQualifiedName Map populated
  during populateOwners; `pickCppAdlCandidates` returns
  SymbolDefinition | ADL_AMBIGUOUS | undefined
- `scope-resolution/contract/scope-resolver.ts` — adds optional
  `resolveAdlCandidates` hook
- `scope-resolution/passes/free-call-fallback.ts` — invokes ADL hook
  between `findCallableBindingInScope` and `pickUniqueGlobalCallable`;
  marks site handled on `'ambiguous'` so emit-references doesn't retry
- `cpp/captures.ts` — detects `parenthesized_expression` function wrap;
  per-arg classification (pointer/reference/value class) preserving the
  shape info the existing arity-narrowing normalizer strips
- `cpp/scope-resolver.ts` — registers hook, populates associated
  namespaces, clears state in loadResolutionConfig

Negative tests (parens, pointer-boundary, ambiguous) gated under
LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp — legacy DAG has no V1/V2
ADL boundary or ADL_AMBIGUOUS suppression.

154/154 cpp integration tests pass under REGISTRY_PRIMARY_CPP=1;
147 pass + 7 skipped under =0 (legacy parity baseline).

* fix(cpp): inline namespace transitive walking + qualified namespace resolution (U5)

Plan 2026-05-13-001 U5. Two ISO C++ inline-namespace semantics:

1. Unqualified-lookup transitive visibility: inline-namespace members
   reach the enclosing namespace's scope as if declared there. The
   `populateCppNonGloballyVisible` exemption keeps them globally visible
   so cross-file unqualified lookup finds them.

2. Qualified-receiver transitive visibility: `outer::foo()` resolves to
   `outer::v1::foo()` when `v1` is inline (and through arbitrarily-deep
   nesting like `outer::v1::experimental::foo`, matching libc++ `__1` /
   libstdc++ `__cxx11`).

The second behavior required a new resolver case in
`receiver-bound-calls.ts` (Case 1.5: language-specific qualified-receiver
member lookup) because C++ qualified-namespace member calls had no prior
resolution path — receiver-bound Case 1 only handled
`ParsedImport.kind === 'namespace'` (Python/JS-style) and Case 2 handles
class receivers, neither of which fired for `outer::foo()`. The new
hook `resolveQualifiedReceiverMember` is opt-in; languages without
C++-style qualified-name semantics omit it.

Implementation:
- `cpp/inline-namespaces.ts` — new module: per-pipeline
  `inlineNamespaceRangesByFile` + `inlineNamespaceScopeIds` Sets;
  `markCppInlineNamespaceRange` at capture time;
  `populateCppInlineNamespaceScopes` resolves ranges → scope IDs;
  `resolveCppQualifiedNamespaceMember` walks namespace scopes by simple
  name and descends transitively through inline children only.
- `scope-resolution/contract/scope-resolver.ts` — adds optional
  `resolveQualifiedReceiverMember` hook to the contract.
- `scope-resolution/passes/receiver-bound-calls.ts` — Case 1.5 invokes
  the hook between Case 1 (namespace imports) and Case 2 (class-name
  receiver). Returns undefined for non-namespace receivers so Case 2
  still resolves class-qualified calls.
- `cpp/captures.ts` — detects `inline` keyword child on
  `namespace_definition`; records 1-based range to match Scope.range.
- `cpp/file-local-linkage.ts` — `populateCppNonGloballyVisible` exempts
  inline-namespace scopes so cross-file unqualified lookup keeps their
  members visible.
- `cpp/scope-resolver.ts` — wires `populateCppInlineNamespaceScopes`
  into populateOwners (BEFORE `populateCppNonGloballyVisible` so the
  exemption sees populated state); registers
  `resolveQualifiedReceiverMember` hook.

4 fixtures: `cpp-inline-namespace-unqualified`, `-versioned`,
`-nested` (two transitive inline hops, STL `__1` shape), and
`-adl-participation` (composes with U2 — ADL surfaces records declared
inside inline child namespaces). All 4 assert exactly 1 CALLS edge with
correct target file.

Versioned fixture gated under LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp
— legacy DAG can't disambiguate two same-name foos without inline
awareness. Other 3 coincidentally resolve in legacy.

158/158 cpp integration tests pass under REGISTRY_PRIMARY_CPP=1;
150 pass + 8 skipped under =0 (legacy parity baseline).

* test(cpp): Phase 5 cross-unit composition tests for U1/U2/U3/U5

Plan 2026-05-13-001 Phase 5. Locks in correct behavior at the
intersections between the previously-shipped scope-resolver units.

Enhancement to U1: `isSuperReceiverInContext` strips template-argument
lists (`Base<T>` → `Base`) and namespace prefixes (`outer::v1::Base` →
`Base`) before resolving the receiver in the caller's scope chain. This
makes the super-receiver classification work for template-class
heritage shapes like `Base<T>::method()` and `outer::v1::Base<T>::f()`.

Three fixtures + four tests:

- `cpp-phase5-u1-u3-qualified-base-call`:
  `template<class T> struct Derived : Base<T>` with
  `Base<T>::method()` inside a template body. Asserts NO mis-routing
  (count = 0) — documents the V1 gap that template-class inheritance
  isn't captured as EXTENDS by the legacy DAG, so MRO walks are empty
  and the super branch can't dispatch. The composition still works
  correctly: U1's template-arg-stripping classifies `Base<T>` as a
  super candidate, but the empty-MRO terminates without false edges.

- `cpp-phase5-u2-u3-adl-from-derived`:
  `Derived : Base<T>` where `Base::record` shadows `audit::record`.
  Unqualified `record(e)` inside the template body should resolve via
  ADL to `audit::record` (because U3 + the `isFileLocalDef` class-
  owned filter suppress `Base::record`). Asserts 1 edge to audit.h
  and 0 edges to base.h.

- `cpp-phase5-u3-u5-inline-base`:
  `template<class T> struct Derived : outer::v1::Base<T>` where `v1`
  is inline. Unqualified `f()` inside `Derived<T>::g()` should NOT
  bind to Base::f (dependent-base suppression even across inline
  namespace prefix). Asserts count = 0.

Phase 5 tests asserting no-false-positives are gated under
LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES.cpp — legacy DAG over-
resolves without the template-arg-stripping qualified-receiver path
and without two-phase dependent-base suppression.

162/162 cpp integration tests pass under REGISTRY_PRIMARY_CPP=1;
152 pass + 10 skipped under =0 (legacy parity baseline).

---------

Co-authored-by: HuangWenjie <zhoudeng.hwj@alibaba-inc.com>
Co-authored-by: Gergo Magyar <gergomagyar@icloud.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-14 09:30:52 +01:00

2185 lines
88 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* C++: diamond inheritance + include-based imports + ambiguous #include disambiguation
*/
import { describe, expect, beforeAll } from 'vitest';
import path from 'path';
import {
FIXTURES,
CROSS_FILE_FIXTURES,
getRelationships,
getNodesByLabel,
getNodesByLabelFull,
edgeSet,
runPipelineFromRepo,
createResolverParityIt,
type PipelineResult,
} from './helpers.js';
const it = createResolverParityIt('cpp');
// ---------------------------------------------------------------------------
// 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(3);
// Per-field exact counts: both `user.name = ...` and `user.name += ...`
// must produce distinct edges (no dedup); single write to `address`.
const nameWrites = writes.filter((e) => e.target === 'name');
expect(nameWrites.length).toBe(2);
const addrWrites = writes.filter((e) => e.target === 'address');
expect(addrWrites.length).toBe(1);
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');
});
});
// ---------------------------------------------------------------------------
// U1: `#include` must not leak class-owned methods as unqualified bindings
// ---------------------------------------------------------------------------
describe('C++ include does not leak class methods', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-include-no-class-leak'), () => {});
}, 60000);
it('does NOT resolve unqualified save() to User::save via #include', () => {
const calls = getRelationships(result, 'CALLS');
const leak = calls.filter((c) => c.source === 'run' && c.target === 'save');
expect(leak.length).toBe(0);
});
it('preserves the file-level #include IMPORTS edge', () => {
const imports = getRelationships(result, 'IMPORTS');
expect(imports.length).toBe(1);
expect(imports[0].targetFilePath).toBe('user.h');
});
});
// ---------------------------------------------------------------------------
// U1: `#include` must not leak namespace-nested symbols as unqualified bindings
// ---------------------------------------------------------------------------
describe('C++ include does not leak namespace members', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-include-no-namespace-leak'),
() => {},
);
}, 60000);
it('does NOT resolve unqualified foo() to ns::foo via #include', () => {
const calls = getRelationships(result, 'CALLS');
const leak = calls.filter((c) => c.source === 'run' && c.target === 'foo');
expect(leak.length).toBe(0);
});
it('preserves the file-level #include IMPORTS edge', () => {
const imports = getRelationships(result, 'IMPORTS');
expect(imports.length).toBe(1);
expect(imports[0].targetFilePath).toBe('lib.h');
});
});
// ---------------------------------------------------------------------------
// U1: anonymous-namespace symbols remain visible within their declaring TU
// (positive companion to the cross-file exclusion test below)
// ---------------------------------------------------------------------------
describe('C++ anonymous namespace symbols visible in same TU', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-anon-ns-same-file-visible'),
() => {},
);
}, 60000);
it('resolves run() -> w() within the same TU', () => {
const calls = getRelationships(result, 'CALLS');
const wCalls = calls.filter((c) => c.source === 'run' && c.target === 'w');
expect(wCalls.length).toBe(1);
});
});
// ---------------------------------------------------------------------------
// U2: integer-width overload ambiguity suppresses CALLS edge entirely
// (PR #1520 review follow-up plan U2; Claude review Finding 5)
// ---------------------------------------------------------------------------
describe('C++ ambiguous integer-width overloads', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-overload-int-long'), () => {});
}, 60000);
it('emits zero CALLS edges when process(int)/process(long) collide after normalization', () => {
const calls = getRelationships(result, 'CALLS');
const processCalls = calls.filter((c) => c.source === 'run' && c.target === 'process');
// Exact .toBe(0): any non-zero count is a regression. count=1 = arbitrary
// pick (the bug U2 fixes); count=2+ would require an ambiguous-edge model
// GitNexus does not have. The resolver must suppress entirely.
expect(processCalls.length).toBe(0);
});
});
// ---------------------------------------------------------------------------
// U3: anonymous-namespace symbols MUST NOT leak across translation units
// (full-pipeline integration test; unit-level coverage exists separately)
// PR #1520 review follow-up plan U3 / Claude review Finding 7
// ---------------------------------------------------------------------------
describe('C++ anonymous namespace cross-file exclusion (integration)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-anon-ns-cross-file'), () => {});
}, 60000);
it('caller.cpp::run -> worker does NOT target helper.cpp anonymous-namespace worker', () => {
const calls = getRelationships(result, 'CALLS');
const crossFileLeak = calls.filter(
(c) =>
c.source === 'run' && c.target === 'worker' && c.targetFilePath?.includes('helper.cpp'),
);
expect(crossFileLeak.length).toBe(0);
});
it('helper.cpp::helper_entry still resolves its OWN anonymous-namespace worker (positive guard)', () => {
const calls = getRelationships(result, 'CALLS');
const sameFileResolve = calls.filter(
(c) =>
c.source === 'helper_entry' &&
c.target === 'worker' &&
c.targetFilePath?.includes('helper.cpp'),
);
// Pairs with the negative test above so a "no edges at all" regression
// doesn't make the cross-file leak check pass vacuously.
expect(sameFileResolve.length).toBe(1);
});
});
// State-isolation guard: re-run the same fixture and assert identical
// results. Proves `clearFileLocalNames()` (called from the cpp resolver's
// `loadResolutionConfig`) is exercised by `runPipelineFromRepo` and
// that module-level `fileLocalNames` state doesn't bleed across runs.
describe('C++ anonymous namespace state-isolation guard', () => {
it('second run of the same fixture produces identical worker-cross-file edge count', async () => {
const fixture = path.join(FIXTURES, 'cpp-anon-ns-cross-file');
const r1 = await runPipelineFromRepo(fixture, () => {});
const r2 = await runPipelineFromRepo(fixture, () => {});
const countLeak = (r: PipelineResult): number =>
getRelationships(r, 'CALLS').filter(
(c) =>
c.source === 'run' && c.target === 'worker' && c.targetFilePath?.includes('helper.cpp'),
).length;
expect(countLeak(r1)).toBe(0);
expect(countLeak(r2)).toBe(0);
}, 120000);
});
// ---------------------------------------------------------------------------
// U4: `using namespace` with conflicting names from two namespaces
// The resolver MUST emit zero CALLS edges — emitting one is arbitrary
// pick; emitting two requires an ambiguous-target edge model GitNexus
// does not have.
// Depends on U1 (without scope-aware filtering, `a::foo` and `b::foo`
// would already be in the importer's wildcard binding set as simple
// `foo` and this test would pass for the wrong reason).
// PR #1520 review follow-up plan U4 / Claude review Finding 7
// ---------------------------------------------------------------------------
describe('C++ using-namespace with conflicting names', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-using-namespace-conflict'),
() => {},
);
}, 60000);
it('emits zero CALLS edges for ambiguous foo() bound via two using-namespace declarations', () => {
const calls = getRelationships(result, 'CALLS');
const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo');
expect(fooCalls.length).toBe(0);
});
});
// ---------------------------------------------------------------------------
// U5: `using namespace std` MUST NOT leak shim STL symbols into unqualified
// bindings. Uses a fixture-local `namespace std { ... }` shim rather than
// real <iostream> — captures the wildcard-leak shape deterministically
// without depending on system-header modeling stability.
// PR #1520 review follow-up plan U5 / Claude review Finding 7
// ---------------------------------------------------------------------------
describe('C++ using-namespace std smoke test', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-using-namespace-std-smoke'),
() => {},
);
}, 60000);
it('resolves the project call (positive guard against vacuous pass)', () => {
const calls = getRelationships(result, 'CALLS');
const projectCalls = calls.filter((c) => c.source === 'run' && c.target === 'project_helper');
expect(projectCalls.length).toBe(1);
});
it('does NOT leak unqualified bindings for shim STL symbols', () => {
const calls = getRelationships(result, 'CALLS');
const stlLeaks = calls.filter(
(c) => c.source === 'run' && (c.target === 'cout_write' || c.target === 'println'),
);
expect(stlLeaks.length).toBe(0);
});
it('emits no CALLS or ACCESSES edges from run() into std-shim.h', () => {
const calls = getRelationships(result, 'CALLS');
const accesses = getRelationships(result, 'ACCESSES');
const intoShim = [...calls, ...accesses].filter(
(e) => e.source === 'run' && e.targetFilePath?.includes('std-shim.h'),
);
expect(intoShim.length).toBe(0);
});
});
// ---------------------------------------------------------------------------
// U1 (follow-up plan 2026-05-13-001): namespace-qualified or class-qualified
// calls from outside that class MUST NOT be classified as super-receiver calls.
// The `isSuperReceiverInContext` hook consults the caller's MRO.
// ---------------------------------------------------------------------------
describe('C++ namespace-qualified call is not a super receiver', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-namespace-qualified-not-super'),
() => {},
);
}, 60000);
it('resolves Singleton::getInstance() from a free function (not as super call)', () => {
const calls = getRelationships(result, 'CALLS');
const getInstanceCalls = calls.filter((c) => c.source === 'run' && c.target === 'getInstance');
// Exactly 1: routed through the normal qualified-call path, NOT the super
// branch. Before the U1 fix the regex `/^[A-Z]\w*::/` matched Singleton::,
// entered the super branch with no enclosing class, and dropped the edge.
expect(getInstanceCalls.length).toBe(1);
expect(getInstanceCalls[0].targetFilePath).toContain('singleton.h');
});
});
// ---------------------------------------------------------------------------
// U4 (follow-up plan 2026-05-13-001): default-argument overload ambiguity.
// `void f(int); void f(int, int = 0); f(1);` is ambiguous per ISO C++. The
// OVERLOAD_AMBIGUOUS sentinel from plan 2026-05-12-002 U2 should detect
// this case via isOverloadAmbiguousAfterNormalization.
// ---------------------------------------------------------------------------
describe('C++ default-argument overload ambiguity', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-overload-default-arg-ambiguous'),
() => {},
);
}, 60000);
it('s.f(1) emits zero CALLS edges when f(int) and f(int, int=0) both match', () => {
const calls = getRelationships(result, 'CALLS');
const fCalls = calls.filter((c) => c.source === 'run' && c.target === 'f');
// Exact .toBe(0): count=1 means arbitrary pick (the bug); count=2+ would
// require an ambiguous-target edge model GitNexus does not have. The
// resolver must suppress entirely. Standard C++ rejects the call as
// ambiguous (GCC/Clang both diagnose).
expect(fCalls.length).toBe(0);
});
});
// ---------------------------------------------------------------------------
// U3 (follow-up plan 2026-05-13-001): two-phase template lookup.
// Inside a class template body, unqualified calls MUST NOT bind to members
// of a dependent base class. Only `this->name()` or `Base<T>::name()` forms
// should resolve.
// ---------------------------------------------------------------------------
describe('C++ two-phase template lookup — dependent base suppression', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-two-phase-dependent-base'),
() => {},
);
}, 60000);
it('Derived<T>::g() -> f() does NOT bind to Base<T>::f (dependent base)', () => {
const calls = getRelationships(result, 'CALLS');
const leaks = calls.filter((c) => c.source === 'g' && c.target === 'f');
expect(leaks.length).toBe(0);
});
it('Derived<T>::h() -> i does NOT bind to Base<T>::i (dependent base)', () => {
const accesses = getRelationships(result, 'ACCESSES');
const leaks = accesses.filter((c) => c.source === 'h' && c.target === 'i');
expect(leaks.length).toBe(0);
});
});
// NOTE: positive guards (this->f() resolves, non-dependent-base unqualified
// f() resolves, namespace-qualified utils::ns_helper() resolves) inside
// template bodies are documented gaps in C++ template-context resolution
// independent of U3's dependent-base suppression. The U3 core asserts only
// the negative behavior (dependent-base members are NOT bound by unqualified
// calls); the positive cases would require additional `this` type-binding
// and template-body member-lookup work tracked separately. See plan
// 2026-05-13-001 follow-ups.
// ---------------------------------------------------------------------------
// U2 (follow-up plan 2026-05-13-001): argument-dependent (Koenig) lookup.
// Free-function calls with class-typed arguments must consider candidates
// declared in the argument's enclosing namespace (associated namespace).
// V1 boundary: only direct enclosing-namespace closure for value class-
// typed args; pointer / reference / template-spec args excluded.
// ---------------------------------------------------------------------------
describe('C++ ADL — basic associated-namespace closure', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-basic'), () => {});
}, 60000);
it('record(e) where e is audit::Event resolves to audit::record via ADL', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
// Exactly 1: ordinary lookup is empty (no `using` statement, no local
// declaration), ADL surfaces audit::record because audit::Event's
// associated namespace is `audit`. The CALLS edge should target the
// declaration in audit.h.
expect(recordCalls.length).toBe(1);
expect(recordCalls[0].targetFilePath).toContain('audit.h');
});
});
describe('C++ ADL — parenthesized name suppresses ADL', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-suppressed-parens'), () => {});
}, 60000);
it('(record)(e) emits zero CALLS edges — ADL is suppressed by parentheses', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
// Exact .toBe(0): ISO C++ [basic.lookup.argdep]/3.1 specifies that the
// parenthesized form `(f)(x)` forces ordinary lookup only — ADL must
// NOT fire. Without ordinary-lookup candidates (no `using`, no local
// declaration), the call goes unresolved.
expect(recordCalls.length).toBe(0);
});
});
describe('C++ ADL — pointer-arg V1 boundary', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-adl-pointer-arg-boundary'),
() => {},
);
}, 60000);
it('record(p) where p is audit::Event* emits zero CALLS — V1 ADL excludes pointer args', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
// Exact .toBe(0): V1 ADL covers only directly-named class-type values
// (per plan 2026-05-13-001 R4). Pointer-typed args fall under
// associated-entity closure rules deferred to V2. This fixture locks
// the boundary in CI so the implementer cannot accidentally extend
// V1 to include pointer types. Real ISO C++ would resolve via V2
// closure; matching that requires the V2 follow-up plan.
expect(recordCalls.length).toBe(0);
});
});
describe('C++ ADL — int/long-collision overloads suppress via OVERLOAD_AMBIGUOUS', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-ambiguous'), () => {});
}, 60000);
it('process(t, 42) emits zero CALLS edges when ADL surfaces process(Token,int)/process(Token,long) (collide after C++ int normalization)', () => {
const calls = getRelationships(result, 'CALLS');
const processCalls = calls.filter((c) => c.source === 'run' && c.target === 'process');
// Exact .toBe(0): both alpha::process(Token, int) and
// alpha::process(Token, long) are surfaced via ADL (alpha::Token's
// associated namespace). C++ arity-metadata normalizes int/long to
// 'int', so both candidates have parameterTypes ['Token', 'int'].
// narrowOverloadCandidates can't disambiguate (arg-types are
// ['', 'int']), and isOverloadAmbiguousAfterNormalization detects
// the collision → ADL_AMBIGUOUS sentinel → caller suppresses.
// count=1 is the bug (arbitrary first-pick); count=2 would require
// an ambiguous-target edge model GitNexus does not have.
expect(processCalls.length).toBe(0);
});
});
// ---------------------------------------------------------------------------
// U5 (follow-up plan 2026-05-13-001): inline namespace transitive walking.
// `inline namespace v1 { ... }` makes its members reachable through the
// enclosing namespace's qualified lookup as if declared directly there
// (ISO C++ `[namespace.def]/p4`). Adds a C++-specific
// `resolveQualifiedReceiverMember` hook on the ScopeResolver contract.
// ---------------------------------------------------------------------------
describe('C++ inline namespace — outer::foo resolves to inline child', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-inline-namespace-unqualified'),
() => {},
);
}, 60000);
it('outer::foo() resolves to outer::v1::foo via inline-namespace transitive walking', () => {
const calls = getRelationships(result, 'CALLS');
const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo');
// Exactly 1: the inline-namespace exemption lets `outer::foo()` reach
// the declaration in `outer::v1::foo()`. Without U5 the call would be
// unresolved (count = 0).
expect(fooCalls.length).toBe(1);
expect(fooCalls[0].targetFilePath).toContain('lib.h');
});
});
describe('C++ inline namespace — versioned (v1 inline, v0 not)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-inline-namespace-versioned'),
() => {},
);
}, 60000);
it('outer::foo() resolves to outer::v1::foo (inline child), NOT outer::v0::foo', () => {
const calls = getRelationships(result, 'CALLS');
const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo');
// Exactly 1: only inline-namespace children are reachable through the
// enclosing namespace's qualified lookup. `v0` is NOT inline so its
// `foo` is NOT visible as `outer::foo`.
expect(fooCalls.length).toBe(1);
expect(fooCalls[0].targetFilePath).toContain('lib.h');
});
});
describe('C++ inline namespace — nested (STL __1-style)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-inline-namespace-nested'),
() => {},
);
}, 60000);
it('outer::foo() resolves through two transitive inline namespaces (v1 then experimental)', () => {
const calls = getRelationships(result, 'CALLS');
const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo');
// Exactly 1: the resolver descends inline namespaces depth-first, so
// `outer::foo` reaches `outer::v1::experimental::foo` through two
// transitive inline-namespace hops. Mirrors libc++ `std::__1::vector`
// / libstdc++ `std::__cxx11` qualified-call shape.
expect(fooCalls.length).toBe(1);
expect(fooCalls[0].targetFilePath).toContain('lib.h');
});
});
describe('C++ inline namespace — ADL participation', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-inline-namespace-adl-participation'),
() => {},
);
}, 60000);
it('ADL surfaces audit::v1::record through inline-namespace transitive walking', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record');
// Exactly 1: `audit::Event e;` resolves Event's enclosing namespace
// to `audit` (the inline child `v1` is transparent — see U2's
// computeNamespaceQName walking through the inline scope). ADL then
// surfaces every callable named `record` in any namespace scope
// matching qname 'audit' across files. Since inline namespaces are
// exempted from the non-globally-visible filter, the `record`
// declared inside `inline namespace v1` is reachable. count=0
// would be the bug — ADL failing to walk inline children.
expect(recordCalls.length).toBe(1);
expect(recordCalls[0].targetFilePath).toContain('audit.h');
});
});
// ---------------------------------------------------------------------------
// Phase 5 (follow-up plan 2026-05-13-001): cross-unit composition tests.
// Lock in correct interaction between U1 (super-receiver context), U2 (ADL),
// U3 (two-phase lookup), and U5 (inline namespaces).
// ---------------------------------------------------------------------------
describe('C++ Phase 5 U1×U3 — qualified Base<T>::method() inside template body (no false positives)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-phase5-u1-u3-qualified-base-call'),
() => {},
);
}, 60000);
it('Base<T>::method() does NOT mis-route to a class method outside the MRO', () => {
const calls = getRelationships(result, 'CALLS');
const methodCalls = calls.filter((c) => c.source === 'g' && c.target === 'method');
// V1 documented gap: cross-file (and same-file) template-class
// inheritance is not captured as an EXTENDS edge by the legacy DAG
// (the cpp captures.ts has no `base_class_clause` heritage emitter
// for template_type bases). Without an EXTENDS edge, MRO is empty
// and the U1 super branch can't dispatch. Result: 0 CALLS edges.
//
// This Phase 5 cross-unit composition test locks in that the
// template-arg-stripping U1 logic produces NO false positives —
// `Base<T>` correctly classifies as a super-receiver candidate but
// (due to empty MRO) doesn't accidentally route to an unrelated
// method named `method` via any other case. count > 0 here would
// indicate the U1 stripped lookup mis-resolved across cases.
expect(methodCalls.length).toBe(0);
});
});
describe('C++ Phase 5 U2×U3 — ADL routes around dependent-base shadow', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-phase5-u2-u3-adl-from-derived'),
() => {},
);
}, 60000);
it('record(e) inside Derived<T>::g() resolves via ADL to audit::record (not Base::record)', () => {
const calls = getRelationships(result, 'CALLS');
const recordCalls = calls.filter((c) => c.source === 'g' && c.target === 'record');
// Exactly 1: Base::record is class-owned so the global free-call
// fallback's `isFileLocalDef` blocks it (and U3's two-phase
// suppression also fires for unqualified calls inside template
// body when the candidate is a dependent-base member). ADL then
// surfaces audit::record via `audit::Event`'s associated namespace.
// The two-phase + ADL composition leaves exactly one CALLS edge —
// to audit::record in audit.h.
expect(recordCalls.length).toBe(1);
expect(recordCalls[0].targetFilePath).toContain('audit.h');
});
it('record(e) does NOT bind to Base::record (class-owned dependent-base member)', () => {
const calls = getRelationships(result, 'CALLS');
const baseRecordLeaks = calls.filter(
(c) => c.source === 'g' && c.target === 'record' && c.targetFilePath?.includes('base.h'),
);
expect(baseRecordLeaks.length).toBe(0);
});
});
describe('C++ Phase 5 U3×U5 — template Derived : outer::v1::Base<T> (inline)', () => {
let result: PipelineResult;
beforeAll(async () => {
result = await runPipelineFromRepo(
path.join(FIXTURES, 'cpp-phase5-u3-u5-inline-base'),
() => {},
);
}, 60000);
it('unqualified f() inside Derived<T>::g() does NOT bind to outer::v1::Base<T>::f (dependent base across inline namespace)', () => {
const calls = getRelationships(result, 'CALLS');
const fLeaks = calls.filter((c) => c.source === 'g' && c.target === 'f');
// Exact .toBe(0): same suppression rationale as the plain U3 fixture
// (`cpp-two-phase-dependent-base`) — `f()` is unqualified, Base is a
// dependent base, and Base::f is class-owned so the global free-call
// fallback's `isFileLocalDef` blocks it. The inline-namespace wrapper
// doesn't change the suppression behavior: dependent-base detection
// walks the heritage's simple name (`Base`) regardless of the
// qualifying namespace path.
expect(fLeaks.length).toBe(0);
});
});