/** * C++: diamond inheritance + include-based imports + ambiguous #include disambiguation */ import { describe, it, expect, beforeAll } from 'vitest'; import path from 'path'; import { FIXTURES, CROSS_FILE_FIXTURES, getRelationships, getNodesByLabel, getNodesByLabelFull, getResolutionOutcomes, findDanglingEdges, edgeSet, runPipelineFromRepo, type PipelineResult, } from './helpers.js'; // --------------------------------------------------------------------------- // C++ overloaded operators (#1636) // --------------------------------------------------------------------------- describe('C++ overloaded operator call resolution (#1636)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-overloaded-operators'), () => {}); }, 60000); it('resolves member operator+ for user-defined operands', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'runMember' && c.target === 'operator+', ); expect(calls).toHaveLength(1); expect(calls[0]?.targetLabel).toBe('Method'); expect(calls[0]?.targetFilePath).toBe('lib.h'); }); it('resolves free operator<< for user-defined operands', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'runFree' && c.target === 'operator<<', ); expect(calls).toHaveLength(1); expect(calls[0]?.targetLabel).toBe('Function'); expect(calls[0]?.targetFilePath).toBe('lib.cpp'); }); it('does not synthesize an operator edge for built-in int + int', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'runBuiltin' && c.target.startsWith('operator'), ); expect(calls).toHaveLength(0); }); it('does not synthesize operator edges for built-in int variables', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'runBuiltinVariables' && c.target.startsWith('operator'), ); expect(calls).toHaveLength(0); }); it('classifies reference-return inline operators as methods', () => { const methods = getNodesByLabelFull(result, 'Method').filter((m) => m.name === 'operator+='); const functions = getNodesByLabelFull(result, 'Function').filter( (f) => f.name === 'operator+=', ); expect(methods).toHaveLength(1); expect(methods[0]?.properties.filePath).toBe('lib.h'); expect(functions).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // 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'); }); }); describe('C++ variadic packs and dependent-name resolution (#1894)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-variadic-dependent-resolution'), () => {}, ); }, 60000); it('keeps parameter-pack functions viable when call arity exceeds the fixed prefix', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'callVariadic' && c.target === 'logMany', ); expect(calls).toHaveLength(1); }); it('emits one fold-expression edge when the folded callee is unambiguous', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'logMany' && c.target === 'sink', ); expect(calls).toHaveLength(1); }); it('emits zero fold-expression edges when overload resolution remains ambiguous', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'foldAmbiguous' && c.target === 'ambiguous', ); expect(calls).toHaveLength(0); }); it('does not emit a concrete EXTENDS edge for a pack-expanded base', () => { const extendsEdges = getRelationships(result, 'EXTENDS').filter( (e) => e.source === 'Mix' && e.target === 'B', ); expect(extendsEdges).toHaveLength(0); }); it('keeps the comment-free pack-expanded base path covered', () => { const extendsEdges = getRelationships(result, 'EXTENDS').filter( (e) => e.source === 'PlainMix' && e.target === 'B', ); const inheritedCalls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'plainRun' && c.target === 'inherited', ); expect(extendsEdges).toHaveLength(0); expect(inheritedCalls).toHaveLength(0); }); it('does not bind unqualified member lookup through a pack-expanded dependent base', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'inherited', ); expect(calls).toHaveLength(0); }); it('preserves free helper calls inside a class with a pack-expanded dependent base', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'helper', ); expect(calls).toHaveLength(1); }); it('preserves using-declaration namespace helper calls inside a pack-base class', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'namespaceHelper', ); expect(calls).toHaveLength(1); }); it('resolves current-instantiation unqualified member calls', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'own', ); expect(calls).toHaveLength(1); }); it('keeps unknown-specialization member types unresolved', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'use', ); expect(calls).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // 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(); }); // F9 — a plain structured-binding declaration emits one Variable per bound name. it('emits a Variable for each name in `auto [firstId, secondId] = makePair();`', () => { const vars = getNodesByLabelFull(result, 'Variable').map((v) => v.name); expect(vars).toContain('firstId'); expect(vars).toContain('secondId'); }); it('classifies top-level structured-binding names as module scope', () => { const bound = getNodesByLabelFull(result, 'Variable').filter( (v) => v.name === 'firstId' || v.name === 'secondId', ); expect(bound).toHaveLength(2); for (const v of bound) expect(v.properties.scope).toBe('module'); }); }); // --------------------------------------------------------------------------- // 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++ braced-init-list overload disambiguation (#1899 A8 conservative)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-braced-init-list-overload'), () => {}, ); }, 60000); const callsFrom = (source: string, target: string) => getRelationships(result, 'CALLS').filter( (edge) => edge.source === source && edge.target === target, ); const singleTargetParameterTypes = (source: string, target: string) => { const calls = callsFrom(source, target); expect(calls).toHaveLength(1); const [call] = calls; expect(call).toBeDefined(); return call === undefined ? undefined : result.graph.getNode(call.rel.targetId)?.properties.parameterTypes; }; it('resolves homogeneous literal braces to initializer_list overloads', () => { expect(singleTargetParameterTypes('callHomogeneousInitList', 'consume')).toEqual([ 'std::initializer_list', ]); }); it('resolves homogeneous literal braces to container overloads', () => { expect(singleTargetParameterTypes('callHomogeneousVector', 'consumeVector')).toEqual([ 'std::vector', ]); }); it('prefers a scalar overload for single-element braced-init lists', () => { expect(singleTargetParameterTypes('callSingleElementScalar', 'consumeScalarOrVector')).toEqual([ 'int', ]); }); it('rejects container overloads whose value type cannot accept the braced elements', () => { expect(callsFrom('callStringVectorMismatch', 'consumeStringVectorMismatch')).toHaveLength(0); }); it('suppresses heterogeneous braced-init lists instead of guessing an element type', () => { expect(callsFrom('callHeterogeneousInitList', 'consumeMixed')).toHaveLength(0); }); it('suppresses empty braced-init lists instead of guessing an element type', () => { expect(callsFrom('callEmptyInitList', 'consumeEmpty')).toHaveLength(0); }); it('preserves single-overload heterogeneous braced-init recall', () => { expect(callsFrom('callSingleHeterogeneousInitList', 'consumeSingleMixed')).toHaveLength(1); }); it('preserves single-overload empty braced-init recall', () => { expect(callsFrom('callSingleEmptyInitList', 'consumeSingleEmpty')).toHaveLength(1); }); }); 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() // --------------------------------------------------------------------------- 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 vs vector)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-template-overload'), () => {}); }, 60000); it('produces distinct nodes for process(vector) and process(vector)', () => { 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 vs ~vector) 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]); }); }); describe('C++ template partial ordering (#1635)', () => { it('pick(T*) wins over pick(T) for pointer arguments', async () => { const result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-template-partial-order-pointer'), () => {}, ); const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'pick', ); expect(calls.length).toBe(1); const target = result.graph.getNode(calls[0].rel.targetId); expect(target?.properties.startLine).toBe(5); }); it('pick(T*, T) vs pick(T, T*) emits zero CALLS edges when partial ordering is incomparable', async () => { const result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-template-partial-order-tied'), () => {}, ); const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'pick', ); expect(calls.length).toBe(0); }); }); // ── 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 and vector', () => { 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']); }); }); describe('C++ template specialization disambiguation across files', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-template-specialization-disambiguation'), () => {}, ); }, 60000); it('emits distinct Class nodes for List and List', () => { const classes = getNodesByLabelFull(result, 'Class').filter( (c) => c.name === 'List' && Array.isArray(c.properties.templateArguments), ); expect(classes.length).toBe(2); const fingerprints = new Set(classes.map((c) => c.properties.templateArguments.join(','))); expect(fingerprints).toEqual(new Set(['User', 'Order'])); }); it('callSave() in each specialization resolves to its own save()', () => { const calls = getRelationships(result, 'CALLS'); const saveEdges = calls.filter((c) => c.source === 'callSave' && c.target === 'save'); expect(saveEdges.length).toBe(2); const hasMethod = getRelationships(result, 'HAS_METHOD'); const ownerFingerprints = new Set(); for (const edge of saveEdges) { const sourceOwnerEdge = hasMethod.find((e) => e.rel.targetId === edge.rel.sourceId); const targetOwnerEdge = hasMethod.find((e) => e.rel.targetId === edge.rel.targetId); expect(sourceOwnerEdge).toBeDefined(); expect(targetOwnerEdge).toBeDefined(); expect(sourceOwnerEdge!.rel.sourceId).toBe(targetOwnerEdge!.rel.sourceId); const ownerNode = result.graph.getNode(sourceOwnerEdge!.rel.sourceId); const fp = ownerNode?.properties.templateArguments?.join(','); if (fp) ownerFingerprints.add(fp); } expect(ownerFingerprints).toEqual(new Set(['User', 'Order'])); }); it('save specialization bodies route to their own sibling method', () => { const calls = getRelationships(result, 'CALLS'); const persistUserCalls = calls.filter((c) => c.target === 'persistUser'); expect(persistUserCalls.length).toBe(1); const userSaveOwner = getRelationships(result, 'HAS_METHOD').find( (e) => e.rel.targetId === persistUserCalls[0].rel.sourceId, ); expect(userSaveOwner).toBeDefined(); const userOwnerNode = result.graph.getNode(userSaveOwner!.rel.sourceId); expect(userOwnerNode?.properties.templateArguments).toEqual(['User']); const persistOrderCalls = calls.filter((c) => c.target === 'persistOrder'); expect(persistOrderCalls.length).toBe(1); const orderSaveOwner = getRelationships(result, 'HAS_METHOD').find( (e) => e.rel.targetId === persistOrderCalls[0].rel.sourceId, ); expect(orderSaveOwner).toBeDefined(); const orderOwnerNode = result.graph.getNode(orderSaveOwner!.rel.sourceId); expect(orderOwnerNode?.properties.templateArguments).toEqual(['Order']); }); it('resolves external List receiver call to List::save', () => { const calls = getRelationships(result, 'CALLS'); const edge = calls.find( (c) => c.source === 'callUserSave' && c.target === 'save' && c.targetFilePath === 'list_user.h', ); expect(edge).toBeDefined(); const ownerEdge = getRelationships(result, 'HAS_METHOD').find( (e) => e.rel.targetId === edge!.rel.targetId, ); expect(ownerEdge).toBeDefined(); const ownerNode = result.graph.getNode(ownerEdge!.rel.sourceId); expect(ownerNode?.properties.templateArguments).toEqual(['User']); }); }); // ── 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: inherited method resolution — 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'); }); }); describe('C++ inheritance-lattice member lookup (#1891)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-member-lattice'), () => {}); }, 60000); it('suppresses same-name members inherited from unrelated bases', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'ambiguousCall' && call.target === 'collide', ); expect(calls).toHaveLength(0); }); it('lets a derived declaration hide both base declarations', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'dominantCall' && call.target === 'collide', ); expect(calls).toHaveLength(1); expect(calls[0]?.targetFilePath).toBe('main.cpp'); }); it('merges a shared virtual base into one member subobject', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'virtualDiamondCall' && call.target === 'shared', ); expect(calls).toHaveLength(1); }); it('suppresses the same declaration reached through two non-virtual base subobjects', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'plainDiamondCall' && call.target === 'shared', ); expect(calls).toHaveLength(0); }); it('adds a member using-declaration to the derived overload set', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'usingCall' && call.target === 'select', ); expect(calls).toHaveLength(1); const target = result.graph.getNode(calls[0]!.rel.targetId); expect(target?.properties.parameterTypes).toEqual(['int']); }); it('records both conservative ambiguity suppressions', () => { const outcomes = getResolutionOutcomes(result).filter( (outcome) => outcome.kind === 'suppressed' && outcome.reason === 'member-lookup-ambiguous', ); const names = outcomes.map((outcome) => outcome.name); expect(names).toContain('collide'); expect(names).toContain('overrideMember'); expect(names).toContain('shared'); }); it('keeps sibling non-virtual subobjects ambiguous when one branch overrides the member', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'nonVirtualOverrideCall' && call.target === 'overrideMember', ); expect(calls).toHaveLength(0); }); it('merges inherited using-declarations with methods declared by the same intermediate class', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'inheritedUsingCall' && call.target === 'inheritedUsing', ); expect(calls).toHaveLength(1); const target = result.graph.getNode(calls[0]!.rel.targetId); expect(target?.properties.parameterTypes).toEqual(['int']); }); it('uses qualified base identities when same-simple-name direct bases collide', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'qualifiedUsingCall' && call.target === 'qualified', ); expect(calls).toHaveLength(1); const target = result.graph.getNode(calls[0]!.rel.targetId); expect(target?.properties.parameterTypes).toEqual(['int']); }); it('normalizes every segment of a nested templated base name', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'nestedTemplateCall' && call.target === 'nestedTemplate', ); expect(calls).toHaveLength(1); }); it('applies lattice ambiguity suppression to explicit this receivers', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'callThis' && call.target === 'collide', ); expect(calls).toHaveLength(0); }); it('resolves inherited members across files', () => { const calls = getRelationships(result, 'CALLS').filter( (call) => call.source === 'crossFileCall' && call.target === 'crossFile', ); expect(calls).toHaveLength(1); expect(calls[0]?.targetFilePath).toBe('base.h'); }); }); // --------------------------------------------------------------------------- // 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); }); it('records a structured suppression reason for normalization ambiguity', () => { const outcomes = getResolutionOutcomes(result).filter( (o) => o.kind === 'suppressed' && o.name === 'process' && o.phase === 'receiver-bound-calls' && o.filePath.endsWith('caller.cpp') && o.reason === 'overload-ambiguous-normalization', ); expect(outcomes.length).toBeGreaterThan(0); expect(outcomes[0]?.candidateIds.length).toBe(2); expect(outcomes[0]?.range.startLine).toBeGreaterThan(0); }); }); // --------------------------------------------------------------------------- // C++ overload resolution: standard-conversion-sequence ranking (#1578) // Disambiguates overloads when exact normalized-type matching cannot, // by scoring each candidate's conversion cost. Exact match (rank 0) wins // over standard conversion (rank 2); same-rank ties still suppress. // --------------------------------------------------------------------------- describe('C++ overload resolution — conversion-rank disambiguation (#1578)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-overload-conversion-rank'), () => {}, ); }, 60000); it('f(2.5) resolves to f(double) — exact match beats standard conversion', () => { const calls = getRelationships(result, 'CALLS'); const fCalls = calls.filter((c) => c.source === 'run' && c.target === 'f'); // Conversion-rank scoring picks f(double) as the unique best: // f(double) is exact match (rank 0), f(int) is standard conversion (rank 2). const fDoubleEdges = fCalls.filter((c) => { const tgt = result.graph.getNode(c.rel.targetId); return tgt?.properties.parameterTypes?.[0] === 'double'; }); expect(fDoubleEdges.length).toBe(1); }); it('f(42) resolves to f(int) — exact match beats standard conversion', () => { const calls = getRelationships(result, 'CALLS'); const fCalls = calls.filter((c) => c.source === 'run' && c.target === 'f'); // f(int) is exact match (rank 0), f(double) is standard conversion (rank 2). const fIntEdges = fCalls.filter((c) => { const tgt = result.graph.getNode(c.rel.targetId); return tgt?.properties.parameterTypes?.[0] === 'int'; }); expect(fIntEdges.length).toBe(1); }); it('g(42) emits zero CALLS edges — int/long normalize to same type, ambiguous', () => { const calls = getRelationships(result, 'CALLS'); const gCalls = calls.filter((c) => c.source === 'run' && c.target === 'g'); // g(int) and g(long) both normalize to parameterTypes=['int'], // so isOverloadAmbiguousAfterNormalization triggers suppression. expect(gCalls.length).toBe(0); }); it("p('a') resolves to p(int) — char promotion (rank 1) beats char→double conversion (rank 2)", () => { const calls = getRelationships(result, 'CALLS'); const pCalls = calls.filter((c) => c.source === 'run' && c.target === 'p'); // p('a'): argType='char'. Exact-type filter misses both p(int) and // p(double), forcing the conversion ranker (step 4b). char→int is an // integral promotion (rank 1), char→double is a standard conversion // (rank 2). p(int) wins with the lower total cost. expect(pCalls.length).toBe(1); const tgt = result.graph.getNode(pCalls[0].rel.targetId); expect(tgt?.properties.parameterTypes?.[0]).toBe('int'); }); it('h(42, 2.5) emits zero CALLS edges — incomparable multi-arg overloads, ambiguous', () => { const calls = getRelationships(result, 'CALLS'); const hCalls = calls.filter((c) => c.source === 'run' && c.target === 'h'); // h(42, 2.5) + h('a', 2.5): both call sites produce incomparable // pairwise rankings. For h(42, 2.5) with argTypes=['int','double']: // h(int,int): [rank('int','int')=0, rank('double','int')=2] // h(double,double): [rank('int','double')=2, rank('double','double')=0] // h(int,int) better at arg0, h(double,double) better at arg1 → neither // dominates → ambiguous. Same pattern for h('a',2.5). // Contract: zero edges for ALL h() call sites combined (dedup). expect(hCalls.length).toBe(0); }); it('records a structured suppression reason for conversion-rank ties', () => { const outcomes = getResolutionOutcomes(result).filter( (o) => o.kind === 'suppressed' && o.name === 'h' && o.phase === 'free-call-fallback' && o.reason === 'conversion-rank-tied', ); expect(outcomes.length).toBeGreaterThan(0); expect(outcomes[0]?.candidateIds.length).toBe(2); expect(outcomes[0]?.range.startLine).toBeGreaterThan(0); }); }); // C++ overload resolution: pointer/nullptr/ellipsis conversion ranks (#1637) describe('C++ overload resolution — pointer/nullptr/ellipsis ranks (#1637)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-overload-pointer-null-ellipsis'), () => {}, ); }, 60000); it('f(nullptr) and f(p) resolve to f(int*) while f(42) resolves to f(bool)', () => { const calls = getRelationships(result, 'CALLS'); const nullptrCall = calls.find((c) => c.source === 'runNullptr' && c.target === 'f'); const pointerCall = calls.find((c) => c.source === 'runPointer' && c.target === 'f'); const boolCall = calls.find((c) => c.source === 'runBoolConversion' && c.target === 'f'); expect( result.graph.getNode(nullptrCall?.rel.targetId ?? '')?.properties.parameterTypes, ).toEqual(['int']); expect( result.graph.getNode(pointerCall?.rel.targetId ?? '')?.properties.parameterTypes, ).toEqual(['int']); expect(result.graph.getNode(boolCall?.rel.targetId ?? '')?.properties.parameterTypes).toEqual([ 'bool', ]); }); it('g(1, 2) resolves to fixed-arity g(int, int), not g(int, ...)', () => { const calls = getRelationships(result, 'CALLS'); const gCalls = calls.filter((c) => c.source === 'run' && c.target === 'g'); expect(gCalls.length).toBe(1); const tgt = result.graph.getNode(gCalls[0].rel.targetId); expect(tgt?.properties.parameterTypes).toEqual(['int', 'int']); }); it("h(1, 'a') resolves to h(int, double), not h(int, ...)", () => { const calls = getRelationships(result, 'CALLS'); const hCalls = calls.filter((c) => c.source === 'run' && c.target === 'h'); expect(hCalls.length).toBe(1); const tgt = result.graph.getNode(hCalls[0].rel.targetId); expect(tgt?.properties.parameterTypes).toEqual(['int', 'double']); }); it('k(1, 2, 3) keeps the ellipsis overload viable when it is the only match', () => { const calls = getRelationships(result, 'CALLS'); const kCalls = calls.filter((c) => c.source === 'run' && c.target === 'k'); expect(kCalls.length).toBe(1); const tgt = result.graph.getNode(kCalls[0].rel.targetId); expect(tgt?.properties.parameterCount).toBeUndefined(); expect(tgt?.properties.parameterTypes).toEqual(['int']); }); }); describe('C++ overload resolution — user-defined conversion rank (#1631)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-overload-user-defined-conversion'), () => {}, ); }, 60000); it('f(42) resolves to f(double) because standard conversion beats constructor UDC', () => { const calls = getRelationships(result, 'CALLS'); const fCalls = calls.filter((c) => c.source === 'run' && c.target === 'f'); expect(fCalls.length).toBe(1); const target = result.graph.getNode(fCalls[0].rel.targetId); expect(target?.properties.parameterTypes).toEqual(['double']); }); it('g(42) keeps a single constructor UDC viable when no standard conversion overload exists', () => { const calls = getRelationships(result, 'CALLS'); const gCalls = calls.filter((c) => c.source === 'run' && c.target === 'g'); expect(gCalls.length).toBe(1); const target = result.graph.getNode(gCalls[0].rel.targetId); expect(target?.properties.parameterTypes).toEqual(['Wrap']); }); it('h(42) emits zero CALLS edges when two single-step constructor UDCs tie', () => { const calls = getRelationships(result, 'CALLS'); const hCalls = calls.filter((c) => c.source === 'run' && c.target === 'h'); expect(hCalls.length).toBe(0); }); it('e(42) ignores the explicit-constructor overload and keeps the implicit UDC viable', () => { const calls = getRelationships(result, 'CALLS'); const eCalls = calls.filter((c) => c.source === 'run' && c.target === 'e'); expect(eCalls.length).toBe(1); const target = result.graph.getNode(eCalls[0].rel.targetId); expect(target?.properties.parameterTypes).toEqual(['Wrap']); }); }); describe('C++ overload resolution — UDC namespace collision guard (#1631)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-overload-udc-namespace-collision'), () => {}, ); }, 60000); it('does not let beta::Token(int) tie the valid alpha::Other(int) conversion', () => { const calls = getRelationships(result, 'CALLS'); const fCalls = calls.filter((c) => c.source === 'run' && c.target === 'f'); expect(fCalls.length).toBe(1); const target = result.graph.getNode(fCalls[0].rel.targetId); expect(target?.properties.parameterTypes).toEqual(['Other']); }); }); // --------------------------------------------------------------------------- // 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 — 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::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::g() -> f() does NOT bind to Base::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::h() -> i does NOT bind to Base::i (dependent base)', () => { const accesses = getRelationships(result, 'ACCESSES'); const leaks = accesses.filter((c) => c.source === 'h' && c.target === 'i'); expect(leaks.length).toBe(0); }); }); describe('C++ two-phase template lookup — positive this-qualified calls', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-two-phase-this-qualified'), () => {}, ); }, 60000); it('Derived::g() -> this->f() resolves to f (1 edge)', () => { const calls = getRelationships(result, 'CALLS'); const thisCalls = calls.filter((c) => c.source === 'g' && c.target === 'f'); expect(thisCalls.length).toBe(1); expect(thisCalls[0].targetFilePath).toContain('base.h'); }); it('Derived::k() -> this->base_method() resolves via EXTENDS chain (1 edge)', () => { const calls = getRelationships(result, 'CALLS'); const inheritedCalls = calls.filter((c) => c.source === 'k' && c.target === 'base_method'); expect(inheritedCalls.length).toBe(1); expect(inheritedCalls[0].targetFilePath).toContain('base.h'); }); }); describe('C++ two-phase template lookup — paired unqualified + this-qualified in one fixture', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-two-phase-paired'), () => {}); }, 60000); it('Derived::g_unqualified() -> f() does NOT bind to Base::f', () => { const calls = getRelationships(result, 'CALLS'); const leaks = calls.filter((c) => c.source === 'g_unqualified' && c.target === 'f'); expect(leaks.length).toBe(0); }); it('Derived::g_this() -> this->f() resolves to Base::f (1 edge)', () => { const calls = getRelationships(result, 'CALLS'); const resolved = calls.filter((c) => c.source === 'g_this' && c.target === 'f'); expect(resolved.length).toBe(1); expect(resolved[0].targetFilePath).toContain('base.h'); }); }); describe('C++ two-phase template lookup — namespace calls inside template body', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-two-phase-namespace-free-call-inside-template'), () => {}, ); }, 60000); it('D::g() -> utils::ns_helper() resolves (1 edge)', () => { const calls = getRelationships(result, 'CALLS'); const qualifiedCalls = calls.filter((c) => c.source === 'g' && c.target === 'ns_helper'); expect(qualifiedCalls.length).toBe(1); expect(qualifiedCalls[0].targetFilePath).toContain('helpers.h'); }); it('D::g() -> ns_helper_2() resolves after using-declaration (1 edge)', () => { const calls = getRelationships(result, 'CALLS'); const usingCalls = calls.filter((c) => c.source === 'g' && c.target === 'ns_helper_2'); expect(usingCalls.length).toBe(1); expect(usingCalls[0].targetFilePath).toContain('helpers.h'); }); }); describe('C++ two-phase template lookup — this-> name-hiding arity mismatch', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-two-phase-this-name-hiding-arity'), () => {}, ); }, 60000); it('Derived::g() -> this->f() emits zero CALLS edges when only hidden derived overload is arity-incompatible', () => { const calls = getRelationships(result, 'CALLS'); const fCalls = calls.filter((c) => c.source === 'g' && c.target === 'f'); expect(fCalls.length).toBe(0); }); it('Derived::g_ok() -> this->f(42) resolves to derived overload (1 edge)', () => { const calls = getRelationships(result, 'CALLS'); const fCalls = calls.filter((c) => c.source === 'g_ok' && c.target === 'f'); expect(fCalls.length).toBe(1); expect(fCalls[0].targetFilePath).toContain('derived.h'); }); }); describe('C++ two-phase template lookup — dependent-base cross-namespace (nested ns)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-two-phase-dependent-base-cross-ns-pos'), () => {}, ); }, 60000); it('Derived::g() -> this->f() resolves to inner::Inner::f when Inner is in a nested namespace (1 edge)', () => { const calls = getRelationships(result, 'CALLS'); const resolved = calls.filter((c) => c.source === 'g' && c.target === 'f'); expect(resolved.length).toBe(1); expect(resolved[0].targetFilePath).toContain('lib.h'); }); }); describe('C++ two-phase template lookup — dependent-base cross-namespace (negative)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-two-phase-dependent-base-cross-ns-neg'), () => {}, ); }, 60000); it('Derived::g() -> this->f() emits zero CALLS when no Inner exists in the nested namespace', () => { const calls = getRelationships(result, 'CALLS'); const leaks = calls.filter((c) => c.source === 'g' && c.target === 'f'); expect(leaks.length).toBe(0); }); }); describe('C++ two-phase template lookup — dependent-base inline-namespace variant', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-two-phase-dependent-base-cross-ns-inline'), () => {}, ); }, 60000); it('Derived::g() -> this->f() resolves to v1::Base::f when Base is in an inline namespace (1 edge)', () => { const calls = getRelationships(result, 'CALLS'); const resolved = calls.filter((c) => c.source === 'g' && c.target === 'f'); expect(resolved.length).toBe(1); expect(resolved[0].targetFilePath).toContain('lib.h'); }); }); describe('C++ two-phase template lookup — dependent-base deep nesting suppression', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-two-phase-dependent-base-cross-ns-deep'), () => {}, ); }, 60000); it('Derived::g() -> this->f() emits zero CALLS when Inner is two levels deep (ns.a.b) — one-level cap enforced', () => { const calls = getRelationships(result, 'CALLS'); const leaks = calls.filter((c) => c.source === 'g' && c.target === 'f'); expect(leaks.length).toBe(0); }); // The base is `ns::a::b::Inner`, and this fixture deliberately also declares a // global-scope `Inner` to force the multi-candidate path. `resolveDefGraphId` // must reach the deep node, not the global decoy: the scope-extractor leaves // the base's `qualifiedName` a bare `Inner` with the path on `namespacePrefix`, // so only the namespace-prefixed key can distinguish them, and it has to be // tried BEFORE the bare key or the decoy answers first (#2745 review — the // reorder that fixed this was previously unpinned by any assertion). it('EXTENDS anchors on the deep ns.a.b.Inner, not the global Inner decoy', () => { const extendsEdges = getRelationships(result, 'EXTENDS').filter((e) => e.source === 'Derived'); expect(extendsEdges).toMatchObject([{ rel: { targetId: 'Struct:lib.h:ns.a.b.Inner' } }]); }); }); describe('C++ two-phase template lookup — dependent-base sibling-namespace suppression', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-two-phase-dependent-base-cross-ns-sibling-suppress'), () => {}, ); }, 60000); it('Derived::g() -> this->f_a() emits zero CALLS when detail::Inner and public_api::Inner are sibling namespaces (ambiguity suppressed)', () => { const calls = getRelationships(result, 'CALLS'); const suppressed = calls.filter((c) => c.source === 'g' && c.target === 'f_a'); expect(suppressed.length).toBe(0); }); }); // --------------------------------------------------------------------------- // U3 cross-file namespace variant: Base lives in a different file AND // inside a namespace. The fixture also contains a free function with the // same name inside the namespace — that candidate has no ownerId, so the // class-owned filter does NOT apply to it; it is instead suppressed by the // namespace-nesting filter. Both candidates must still yield zero edges. // --------------------------------------------------------------------------- describe('C++ two-phase template lookup — cross-file namespace variant', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-two-phase-dependent-base-ns'), () => {}, ); }, 60000); it('geom::Derived::g() -> compute() does NOT bind to geom::Base::compute (cross-file dependent base, class-owned)', () => { const calls = getRelationships(result, 'CALLS'); const leaks = calls.filter((c) => c.source === 'g' && c.target === 'compute'); expect(leaks.length).toBe(0); }); it('geom::Derived::h() -> area does NOT bind to geom::Base::area (cross-file dependent base, class-owned)', () => { const accesses = getRelationships(result, 'ACCESSES'); const leaks = accesses.filter((c) => c.source === 'h' && c.target === 'area'); expect(leaks.length).toBe(0); }); }); // --------------------------------------------------------------------------- // 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 args and template specializations with // explicit type arguments included. Function pointers and base-class // associated namespaces remain 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 — merges with non-empty ordinary lookup', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-merge-nonempty-ordinary'), () => {}, ); }, 60000); it('swap(a, b) prefers data::swap(Pair&, Pair&) over app::swap(int, int)', () => { const calls = getRelationships(result, 'CALLS'); const swapCalls = calls.filter((c) => c.source === 'run' && c.target === 'swap'); expect(swapCalls.length).toBe(1); expect(swapCalls[0].targetFilePath).toContain('data.h'); }); }); describe('C++ ADL — hidden friend and namespace callable in one namespace', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-ns-plus-hidden-friend-same-name'), () => {}, ); }, 60000); // pickCppAdlCandidates merges two buckets for one associated namespace: // friendCandidates (hidden friends of associated classes) and nsCandidates // (namespace-owned callables). This fixture reaches exactly one callable // through each bucket — `combine` only as a hidden friend, `process` only as // a namespace member — so a regression that stopped consulting either bucket // would drop the corresponding edge. (Candidate ORDER is not observable — // overload narrowing resolves a unique survivor or suppresses — so the guard // is on the SET: both edges must be present.) it('combine(a, b) resolves to the hidden friend via friendCandidates', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'call_friend' && c.target === 'combine', ); expect(calls.length).toBe(1); expect(calls[0].targetFilePath).toContain('lib.h'); }); it('process(t) resolves to the namespace callable via nsCandidates', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'call_ns' && c.target === 'process', ); expect(calls.length).toBe(1); expect(calls[0].targetFilePath).toContain('lib.h'); }); }); describe('C++ ADL — base-class associated namespaces', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-base-associated-namespaces'), () => {}, ); }, 60000); it('resolves log(d) to base_lib::log via ADL when Derived inherits from base_lib::Base', () => { const calls = getRelationships(result, 'CALLS'); const logCalls = calls.filter((c) => c.source === 'run_single' && c.target === 'log'); expect(logCalls.length).toBe(1); expect(logCalls[0].targetFilePath).toContain('base_lib.h'); const targetNode = result.graph.getNode(logCalls[0].rel.targetId); expect(logCalls[0].rel.targetId).toBe('Function:base_lib.h:log'); expect(targetNode?.properties.parameterTypes).toEqual(['Base']); }); it('resolves trace(m) via full MRO walk when MultiLevel inherits via middle_lib::Mid -> base_lib::Root', () => { const calls = getRelationships(result, 'CALLS'); const traceCalls = calls.filter((c) => c.source === 'run_multi' && c.target === 'trace'); expect(traceCalls.length).toBe(1); expect(traceCalls[0].targetFilePath).toContain('base_lib.h'); const targetNode = result.graph.getNode(traceCalls[0].rel.targetId); expect(traceCalls[0].rel.targetId).toBe('Function:base_lib.h:trace'); expect(targetNode?.properties.parameterTypes).toEqual(['Root']); }); it('diamond inheritance contributes base namespace once (no duplicate/crash)', () => { const calls = getRelationships(result, 'CALLS'); const pingCalls = calls.filter((c) => c.source === 'run_diamond' && c.target === 'ping'); expect(pingCalls.length).toBe(1); expect(pingCalls[0].targetFilePath).toContain('base_lib.h'); const targetNode = result.graph.getNode(pingCalls[0].rel.targetId); expect(pingCalls[0].rel.targetId).toBe('Function:base_lib.h:ping'); expect(targetNode?.properties.parameterTypes).toEqual(['DiamondBase']); }); }); describe('C++ ADL — base-class namespace MRO with simple-name class collisions', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-base-associated-namespaces-collision'), () => {}, ); }, 60000); it('does NOT emit CALLS for collide(t) when class-name lookup is ambiguous', () => { const calls = getRelationships(result, 'CALLS'); const collideCalls = calls.filter((c) => c.source === 'run' && c.target === 'collide'); expect(collideCalls.length).toBe(0); }); }); describe('C++ ADL — base-class namespace mapping skips anonymous/unresolved bases', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-base-associated-namespaces-negative'), () => {}, ); }, 60000); it('hidden_probe(d) still resolves via ordinary lookup when declaration is visible', () => { const calls = getRelationships(result, 'CALLS'); const hiddenProbeCalls = calls.filter( (c) => c.source === 'run_hidden' && c.target === 'hidden_probe', ); expect(hiddenProbeCalls.length).toBe(1); expect(hiddenProbeCalls[0].targetFilePath).toContain('base_lib.h'); }); it('unresolved_probe(d) emits zero CALLS when base class cannot be resolved', () => { const calls = getRelationships(result, 'CALLS'); const unresolvedProbeCalls = calls.filter( (c) => c.source === 'run_missing' && c.target === 'unresolved_probe', ); expect(unresolvedProbeCalls.length).toBe(0); }); }); 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 unwrapping', () => { 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* resolves to audit::record via ADL', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); expect(recordCalls.length).toBe(1); expect(recordCalls[0].targetFilePath).toContain('audit.h'); }); }); describe('C++ ADL — reference arg unwrapping', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-reference-arg-boundary'), () => {}, ); }, 60000); it('record(s) where s is audit::Event& resolves to audit::record via ADL', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'runRef' && c.target === 'record'); expect(recordCalls.length).toBe(1); expect(recordCalls[0].targetFilePath).toContain('record.h'); }); it('recordConst(cs) where cs is const audit::Event& resolves via ADL', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter( (c) => c.source === 'runConstRef' && c.target === 'recordConst', ); expect(recordCalls.length).toBe(1); expect(recordCalls[0].targetFilePath).toContain('record.h'); }); it('note(r) where r is int& emits zero CALLS edges (primitive ref)', () => { const calls = getRelationships(result, 'CALLS'); const noteCalls = calls.filter((c) => c.source === 'runPrimitiveRef' && c.target === 'note'); expect(noteCalls.length).toBe(0); }); }); describe('C++ ADL — rvalue reference args participate', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-rvalue-ref'), () => {}); }, 60000); it('record(rr) where rr is audit::Event&& resolves to audit::record via ADL', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'runRvalueRef' && c.target === 'record'); expect(recordCalls.length).toBe(1); expect(recordCalls[0].targetFilePath).toContain('record-rvalue.h'); }); }); describe('C++ ADL — function pointer args do not participate', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-function-pointer-arg'), () => {}, ); }, 60000); it('record(g) where g is void (*)() emits zero CALLS edges', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); expect(recordCalls.length).toBe(0); }); }); describe('C++ ADL — preceding function-pointer declarations do not block class args', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-function-pointer-before-class-arg'), () => {}, ); }, 60000); it('record(e) still resolves via ADL when an earlier declaration is void (*)()', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); expect(recordCalls.length).toBe(1); expect(recordCalls[0].targetFilePath).toContain('audit.h'); }); }); describe('C++ ADL — class-returning function pointer args do not participate', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-function-pointer-class-return-arg'), () => {}, ); }, 60000); it('record(factory) where factory is audit::Event (*)() emits zero CALLS edges', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); expect(recordCalls.length).toBe(0); }); }); describe('C++ ADL — pointer-to-pointer args participate', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-pointer-to-pointer'), () => {}); }, 60000); it('record(pp) where pp 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'); expect(recordCalls.length).toBe(1); expect(recordCalls[0].targetFilePath).toContain('audit.h'); }); }); describe('C++ ADL — template specialization args contribute associated namespaces', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-template-args'), () => {}); }, 60000); it('apply(v) where v is std::vector resolves to N::apply via ADL template-arg namespace', () => { const calls = getRelationships(result, 'CALLS'); const applyCalls = calls.filter((c) => c.source === 'run' && c.target === 'apply'); expect(applyCalls.length).toBe(1); expect(applyCalls[0].targetFilePath).toContain('audit.h'); }); it('applyNested(m) where m is std::map> resolves via nested template-arg namespace', () => { const calls = getRelationships(result, 'CALLS'); const applyCalls = calls.filter((c) => c.source === 'runNested' && c.target === 'applyNested'); expect(applyCalls.length).toBe(1); expect(applyCalls[0].targetFilePath).toContain('audit.h'); }); it('applyArray(a) where a is std::array resolves to N::applyArray (non-type arg ignored)', () => { const calls = getRelationships(result, 'CALLS'); const applyCalls = calls.filter((c) => c.source === 'runArray' && c.target === 'applyArray'); expect(applyCalls.length).toBe(1); expect(applyCalls[0].targetFilePath).toContain('audit.h'); }); it('applyStdConflict(v) is suppressed when ADL surfaces both N and std candidates', () => { const calls = getRelationships(result, 'CALLS'); const applyCalls = calls.filter( (c) => c.source === 'runStdConflict' && c.target === 'applyStdConflict', ); expect(applyCalls.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 in merged ordinary+ADL narrowing, so fallback 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); }); }); describe('C++ ADL — merged narrowing to zero suppresses global fallback', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-merged-narrow-zero'), () => {}); }, 60000); it('probe(t, 42) emits zero CALLS when ADL contributes only arity-mismatched candidates', () => { const calls = getRelationships(result, 'CALLS'); const probeCalls = calls.filter((c) => c.source === 'run' && c.target === 'probe'); // ADL surfaces alpha::probe(Token), but call arity is 2 (`probe(t, 42)`), // so merged overload narrowing yields zero survivors. The site is treated // as handled and must NOT fall through to global simple-name fallback. expect(probeCalls.length).toBe(0); }); }); // --------------------------------------------------------------------------- // ADL V2 — ISO C++ `[basic.lookup.argdep]` §2: enum types contribute their // enclosing namespace to the associated set, just like class types. // --------------------------------------------------------------------------- describe('C++ ADL — enum-typed argument contributes enclosing namespace', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-enum-arg'), () => {}); }, 60000); it('serialize(ch) where ch is color::Channel resolves to color::serialize via ADL', () => { const calls = getRelationships(result, 'CALLS'); const serializeCalls = calls.filter((c) => c.source === 'run' && c.target === 'serialize'); // Exactly 1: ordinary lookup in app::run finds nothing for `serialize`. // ADL surfaces color::serialize because color::Channel's enclosing // namespace is `color`. Before the enum gap fix, this was 0. expect(serializeCalls.length).toBe(1); expect(serializeCalls[0].targetFilePath).toContain('color.h'); }); }); // --------------------------------------------------------------------------- // ADL V2 — ISO C++ `[basic.lookup.argdep]` §2: "hidden friend" functions // declared inside a class body are visible via ADL. They are not namespace- // scope declarations (owned by the class scope in tree-sitter-cpp), so they // require scanning associated class scopes in addition to namespace scopes. // --------------------------------------------------------------------------- describe('C++ ADL — hidden friend function visible via ADL', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-hidden-friend'), () => {}); }, 60000); it('process(f) where f is lib::Foo resolves to hidden friend process(Foo&) via ADL', () => { const calls = getRelationships(result, 'CALLS'); const processCalls = calls.filter((c) => c.source === 'run' && c.target === 'process'); // Exactly 1: process(Foo&) is a hidden friend declared inside Foo's // class body. Ordinary namespace-scope lookup won't find it — only ADL // scanning the associated class's ownedDefs can surface it. expect(processCalls.length).toBe(1); expect(processCalls[0].targetFilePath).toContain('lib.h'); }); }); // --------------------------------------------------------------------------- // ADL V2 — ISO C++ `[basic.lookup.unqual]` §7: non-function ordinary lookup // result blocks ADL. If the name resolves to a variable/class/enum in scope, // ADL does not fire even if class-typed arguments are present. // --------------------------------------------------------------------------- describe('C++ ADL — non-function ordinary lookup suppresses ADL', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-non-function-blocks'), () => {}, ); }, 60000); it('record(e) emits zero CALLS when a variable named record exists in scope', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); // ISO C++: `int record = 0;` in namespace app means ordinary lookup // finds a non-function entity. ADL should be suppressed — even though // `e` is audit::Event, audit::record should NOT be discovered. expect(recordCalls.length).toBe(0); }); it('records a structured suppression reason for ADL blocker lookup', () => { const outcomes = getResolutionOutcomes(result).filter( (o) => o.kind === 'suppressed' && o.name === 'record' && o.phase === 'free-call-fallback' && o.reason === 'adl-ordinary-lookup-blocked', ); expect(outcomes.length).toBeGreaterThan(0); expect(outcomes[0]?.candidateIds.length).toBe(0); expect(outcomes[0]?.range.startLine).toBeGreaterThan(0); }); }); describe('C++ ADL — inner callable + outer non-callable: ADL not suppressed', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-inner-callable-outer-noncallable'), () => {}, ); }, 60000); it('swap(a,b) resolves to data::swap when inner scope has callable swap and outer has variable', () => { const calls = getRelationships(result, 'CALLS'); const swapCalls = calls.filter((c) => c.source === 'run' && c.target === 'swap'); // Ordinary lookup finds `inner::swap(int,int)` at the nearest scope. // The outer `app::swap` (variable) does NOT suppress ADL because // ordinary lookup stopped at the inner scope. ADL contributes // data::swap(Pair&,Pair&) which wins via argTypes narrowing. expect(swapCalls.length).toBe(1); expect(swapCalls[0].targetFilePath).toContain('data.h'); }); }); describe('C++ ADL — block-scope function declaration suppresses ADL', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-block-scope-decl-blocks'), () => {}, ); }, 60000); it('record(e) emits zero CALLS when a block-scope function declaration exists', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); // ISO C++ [basic.lookup.argdep]: a block-scope function declaration // (not via using-declaration) suppresses ADL — even though `e` is // audit::Event, audit::record should NOT be discovered. expect(recordCalls.length).toBe(0); }); }); // --------------------------------------------------------------------------- // ADL V2 - strict function-type associated entities. // // Function-reference arguments follow strict ISO C++ ADL: GitNexus walks the // referenced overload set's parameter and return types instead of contributing // the referenced function's enclosing namespace. // For `void worker()`, the associated set is empty; for `void worker(api::Token)` // or `api::Token make_token()`, `api` is associated through `Token`. // --------------------------------------------------------------------------- describe('C++ ADL - free-function reference does not contribute its namespace', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-adl-free-func-ref'), () => {}); }, 60000); it('with_callback(utils::worker) emits zero CALLS edges when worker has no class parameter or return type', () => { const calls = getRelationships(result, 'CALLS'); const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'with_callback'); expect(cbCalls.length).toBe(0); }); }); describe('C++ ADL - free-function reference contributes parameter-type associated namespace', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-free-func-ref-strict'), () => {}, ); }, 60000); it('run_callback(utils::worker) resolves hidden friend through worker(api::Token)', () => { const calls = getRelationships(result, 'CALLS'); const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'run_callback'); expect(cbCalls.length).toBe(1); expect(cbCalls[0].targetFilePath).toContain('lib.h'); }); }); describe('C++ ADL - free-function reference contributes return-type associated namespace', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-free-func-ref-return-strict'), () => {}, ); }, 60000); it('run_callback(utils::make_token) resolves hidden friend through api::Token return type', () => { const calls = getRelationships(result, 'CALLS'); const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'run_callback'); expect(cbCalls.length).toBe(1); expect(cbCalls[0].targetFilePath).toContain('lib.h'); }); }); describe('C++ ADL - overloaded free-function reference stays strict', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-free-func-ref-overloaded'), () => {}, ); }, 60000); it('with_callback(utils::worker) with overloaded utils::worker still emits zero CALLS edges', () => { const calls = getRelationships(result, 'CALLS'); const cbCalls = calls.filter((c) => c.source === 'run' && c.target === 'with_callback'); expect(cbCalls.length).toBe(0); }); }); describe('C++ ADL — namespace-qualified variable arg does NOT contribute namespace', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-qualified-variable-arg'), () => {}, ); }, 60000); it('process(data::value) emits zero CALLS edges — data::value is a variable, not a function', () => { const calls = getRelationships(result, 'CALLS'); const processCalls = calls.filter((c) => c.source === 'run' && c.target === 'process'); // data::value is a namespace-qualified integer variable. tree-sitter-cpp // produces a qualified_identifier AST node regardless of whether `value` // denotes a function, variable, enum, or static member. The GitNexus guard // in collectFunctionTypeAssociatedNamespaces verifies that a Function/Method // named `value` exists in the `data` namespace before walking any function // type. Since `data::value` is an int variable, no function type is walked, // so data::process is never found as an ADL candidate. expect(processCalls.length).toBe(0); }); }); describe('C++ ADL — function parameter does NOT trigger free-function-ref ADL', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-param-not-free-func-ref'), () => {}, ); }, 60000); it('run_with(callback) emits zero CALLS edges when callback is a parameter, not a function reference', () => { const calls = getRelationships(result, 'CALLS'); const runWithCalls = calls.filter((c) => c.source === 'run' && c.target === 'run_with'); // `callback` is an int parameter of `caller::run`. Function parameters // live in the parameter_list, not in the compound_statement, so the // local-scope declaration scan would not find it and would return null — // previously misclassifying it as an unqualified free-function reference. // The workspace contains utils::callback(), so the scan would find it and // contribute `utils` to the ADL set, emitting a false-positive CALLS edge // to utils::run_with. isIdentifierAFunctionParameter now catches this and // returns EMPTY_ADL_ARG, preventing the workspace scan entirely. expect(runWithCalls.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++ ADL — local function-pointer var shadows same-named free function', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-local-fp-shadows-free-func'), () => {}, ); }, 60000); it('record(g) emits zero CALLS edges even though audit::g() exists in the workspace', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); // `g` is a locally-declared `void (*g)()` variable. `audit::g()` also // exists in the workspace. Without the foundAsLocalFunctionPointer guard, // `g` would not be detected in the compound_statement (it IS there, but // as a function-pointer declarator), and the workspace scan would find // audit::g, contribute `audit` to the ADL set, and emit a false-positive // CALLS edge to audit::record. The guard correctly returns EMPTY_ADL_ARG, // so no namespace is contributed and no edge is emitted. expect(recordCalls.length).toBe(0); }); }); describe('C++ ADL — unqualified free-function ref with namespace collision', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-unqualified-ref-collision'), () => {}, ); }, 60000); it('run_with(worker) emits zero CALLS edges when worker exists in two namespaces', () => { const calls = getRelationships(result, 'CALLS'); const runWithCalls = calls.filter((c) => c.source === 'run' && c.target === 'run_with'); // Unqualified `worker` → workspace scan finds alpha::worker and beta::worker. // Both alpha and beta are added to the associated set. run_with() exists in // both namespaces → two candidates → merged narrowing suppression → // zero CALLS edges (suppressed rather than arbitrary pick). expect(runWithCalls.length).toBe(0); }); }); 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 — ambiguous same-name across inline children (#1564)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-inline-namespace-ambiguous'), () => {}, ); }, 60000); it('outer::foo() emits zero CALLS edges when v1 and v2 both declare foo', () => { const calls = getRelationships(result, 'CALLS'); const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo'); // ISO C++ leaves this ambiguous — both inline namespace children declare // the same name. The resolver must suppress rather than pick arbitrarily. expect(fooCalls.length).toBe(0); }); it('records a structured suppression reason for inline namespace ambiguity', () => { const outcomes = getResolutionOutcomes(result).filter( (o) => o.kind === 'suppressed' && o.name === 'foo' && o.phase === 'receiver-bound-calls' && o.reason === 'inline-ns-ambiguous', ); expect(outcomes.length).toBeGreaterThan(0); expect(outcomes[0]?.candidateIds.length).toBe(0); expect(outcomes[0]?.range.startLine).toBeGreaterThan(0); }); }); describe('C++ inline namespace — distinct signatures resolved via call-site types', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-inline-namespace-ambiguous-diff-sigs'), () => {}, ); }, 60000); it('outer::foo(42) emits exactly 1 CALLS edge to v1::foo(int) when v1 declares foo(int) and v2 declares foo(double)', () => { const calls = getRelationships(result, 'CALLS'); const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo'); // Call-site arity and argument types are now threaded through the // resolveQualifiedReceiverMember contract (#1632). narrowOverloadCandidates // matches the exact type 'int' against v1::foo(int), producing exactly 1 edge. expect(fooCalls).toHaveLength(1); // Verify it resolved to v1::foo(int) at line 4 (0-indexed), not v2::foo(double) at line 7 const targetNode = result.graph.getNode(fooCalls[0].rel.targetId); expect(targetNode?.properties.startLine).toBe(4); }); }); describe('C++ inline namespace — ambiguous normalized signatures', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-inline-namespace-ambiguous-normalized'), () => {}, ); }, 60000); it('outer::foo(42) emits zero CALLS edges when v1 declares foo(int) and v2 declares foo(long) — both normalize to int', () => { const calls = getRelationships(result, 'CALLS'); const fooCalls = calls.filter((c) => c.source === 'run' && c.target === 'foo'); // int and long both normalize to 'int' via normalizeCppParamType, making // the two candidates indistinguishable after normalization. The resolver // must suppress rather than pick arbitrarily (isOverloadAmbiguousAfterNormalization). expect(fooCalls.length).toBe(0); }); }); 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'); }); }); describe('C++ ADL — inline namespace expansion in associated set', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-adl-inline-ns-expansion'), () => {}, ); }, 60000); it('record(e) resolves to audit::v1::record when Event is in outer audit and record is in inline v1 (arity-disambiguated from other::record(int))', () => { const calls = getRelationships(result, 'CALLS'); const recordCalls = calls.filter((c) => c.source === 'run' && c.target === 'record'); // ISO C++: inline namespaces are transparent — candidates in // `audit::v1` are visible as if declared at `audit` level. With a // competing `other::record(int)` (different arity), the merged // ordinary+ADL overload narrowing must select `audit::v1::record(Event)` // since it's the only arity-matching candidate for `record(e)`. 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::method() inside template body', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-phase5-u1-u3-qualified-base-call'), () => {}, ); }, 60000); it('emits EXTENDS edge: Derived → Base for template base Base', () => { const extends_ = getRelationships(result, 'EXTENDS'); expect(edgeSet(extends_)).toContain('Derived → Base'); }); it('Base::method() resolves to Base::method inside template body', () => { const calls = getRelationships(result, 'CALLS'); const methodCalls = calls.filter((c) => c.source === 'g' && c.target === 'method'); expect(methodCalls.length).toBe(1); expect(methodCalls[0].targetFilePath).toContain('classes.h'); }); }); describe('C++ Phase 5 U1×U3 — template multi-base list', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-phase5-u1-u3-template-multi-base-list'), () => {}, ); }, 60000); it('emits EXTENDS edges: Derived → A, Derived → B for template multi-base list', () => { const extends_ = getRelationships(result, 'EXTENDS'); expect(extends_.length).toBe(2); expect(edgeSet(extends_)).toEqual(['Derived → A', 'Derived → B']); }); }); 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::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 (inline)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-phase5-u3-u5-inline-base'), () => {}, ); }, 60000); it('unqualified f() inside Derived::g() does NOT bind to outer::v1::Base::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); }); }); describe('C++ Phase 5 U1×U3×U5 — qualified outer::v1::Base::f() inside template body', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-phase5-u1-u3-u5-qualified-inline-base-call'), () => {}, ); }, 60000); it('emits EXTENDS edge: Derived → Base for qualified template base outer::v1::Base', () => { const extends_ = getRelationships(result, 'EXTENDS'); expect(edgeSet(extends_)).toContain('Derived → Base'); }); it('outer::v1::Base::f() resolves to Base::f inside template body', () => { const calls = getRelationships(result, 'CALLS'); const fCalls = calls.filter((c) => c.source === 'g' && c.target === 'f'); expect(fCalls.length).toBe(1); expect(fCalls[0].targetFilePath).toContain('base.h'); }); it('outer::v1::free_fn() resolves as a namespace free function, not a super-receiver method', () => { const calls = getRelationships(result, 'CALLS'); const freeCalls = calls.filter((c) => c.source === 'g' && c.target === 'free_fn'); expect(freeCalls.length).toBe(1); expect(freeCalls[0].targetLabel).toBe('Function'); expect(freeCalls[0].rel.reason).toBe('import-resolved'); }); }); // --------------------------------------------------------------------------- // SFINAE / concept-constrained candidate filtering (issue #1579) // Pre-fix: `enable_if_t` / `requires` guarded overloads collapse into a // false multi-candidate ambiguity → suppressed edge. With // constraintCompatibility wired up the integral / floating overloads // disambiguate cleanly. // --------------------------------------------------------------------------- describe('C++ SFINAE filter — golden case (enable_if_t guarded free function templates)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-sfinae-golden'), () => {}); }, 60000); it('enable_if_t> overload binds only on integral call sites', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'process', ); expect(calls.length).toBe(2); // Distinct targets — the integral and floating overloads disambiguate // via constraintCompatibility, not collapsing to one arbitrary pick. const targetIds = new Set(calls.map((c) => c.rel.targetId)); expect(targetIds.size).toBe(2); }); it('enable_if_t> overload binds only on floating call sites', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'process', ); // Disambiguate-by-startLine — integral overload (earlier line) vs // floating overload (later line). Both must be reachable as targets. const targetStartLines = calls .map((c) => result.graph.getNode(c.rel.targetId)) .filter((n): n is NonNullable => n !== undefined) .map((n) => (n.properties as { startLine?: number }).startLine) .filter((x): x is number => typeof x === 'number') .sort((a, b) => a - b); expect(targetStartLines.length).toBe(2); expect(targetStartLines[0]).toBeLessThan(targetStartLines[1]); }); }); describe('C++ SFINAE filter — C++20 requires-clause shape', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-sfinae-requires-clause'), () => {}); }, 60000); it('requires-clause overloads disambiguate same as enable_if_t (F4 AST shape)', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'process', ); expect(calls.length).toBe(2); const targetIds = new Set(calls.map((c) => c.rel.targetId)); expect(targetIds.size).toBe(2); }); }); describe('C++ SFINAE filter — Tier-A type_traits predicates', () => { async function runFixture(name: string): Promise { return runPipelineFromRepo(path.join(FIXTURES, name), () => {}); } function callsFromRunToPick(result: PipelineResult) { return getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'pick', ); } it('is_pointer_v and is_class_v disambiguate pointer vs class arguments', async () => { const result = await runFixture('cpp-sfinae-is-pointer'); const calls = callsFromRunToPick(result); expect(calls.length).toBe(2); expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2); }, 60000); it('is_reference_v keeps reference-shaped arguments distinct from values', async () => { const result = await runFixture('cpp-sfinae-is-reference'); const calls = callsFromRunToPick(result); expect(calls.length).toBe(2); expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2); }, 60000); it('is_class_v rejects primitive arguments while keeping class arguments', async () => { const result = await runFixture('cpp-sfinae-is-class'); const calls = callsFromRunToPick(result); expect(calls.length).toBe(2); expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2); }, 60000); it('is_enum_v distinguishes known enum declarations from primitives', async () => { const result = await runFixture('cpp-sfinae-is-enum'); const calls = callsFromRunToPick(result); expect(calls.length).toBe(2); expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2); }, 60000); it('is_const_v and is_volatile_v disambiguate cv-qualified locals', async () => { const result = await runFixture('cpp-sfinae-is-const-volatile'); const calls = callsFromRunToPick(result); expect(calls.length).toBe(2); expect(new Set(calls.map((c) => c.rel.targetId)).size).toBe(2); }, 60000); it('is_void_v does not misclassify void pointers as void values', async () => { const result = await runFixture('cpp-sfinae-is-void'); const calls = callsFromRunToPick(result); expect(calls.length).toBe(1); }, 60000); }); describe('C++ SFINAE filter — unknown predicate keeps both candidates (monotonicity contract)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-sfinae-unknown-predicate'), () => {}, ); }, 60000); it('emits zero CALLS edges when predicate is outside the Tier-A registry', () => { // `MyCustomTrait_v` is not registered; both overloads' constraint // check returns 'unknown' → both kept → OVERLOAD_AMBIGUOUS suppression // by `isOverloadAmbiguousAfterNormalization` (both have parameterTypes=['T']). // Asserts the monotonicity guarantee: adding a predicate must never // produce a wrong edge. const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'process', ); expect(calls.length).toBe(0); }); }); describe('C++ SFINAE filter — arity gate runs before constraint filter', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-sfinae-arity-survives-unknown'), () => {}, ); }, 60000); it('emits exactly 1 CALLS edge to the arity-matching overload (bad-arity dropped before constraint check)', () => { const calls = getRelationships(result, 'CALLS').filter( (c) => c.source === 'run' && c.target === 'process', ); expect(calls.length).toBe(1); }); }); // --------------------------------------------------------------------------- // Out-of-line nested definitions — method ownership + collision (issue #1975) // // `struct Outer::Inner { ... }` (name = qualified_identifier) and its in-class // forward declaration `struct Outer { struct Inner; }` are the SAME type. Once // qualified node ids are on (#1978), both key to one canonical node whose // qualifiedName is the normalized scope path `Outer.Inner` — so the forward // decl and the out-of-line definition correctly UNIFY instead of producing two // redundant nodes (the pre-#1978 base kept them separate). Crucially, a // same-tail type in another scope (`Other::Inner`) stays a DISTINCT node — no // merge, no method mis-attribution. Owner identity is asserted on the // qualifiedName + distinct node id (the real key), not the simple `name` // (which is just the tail `Inner` for both, by design). // --------------------------------------------------------------------------- describe('C++ out-of-line nested definitions — ownership + collision (issue #1975)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-out-of-line-class'), () => {}); }, 60000); it('owns each out-of-line method with no dangling HAS_METHOD edges', () => { expect(findDanglingEdges(result, ['HAS_METHOD'])).toEqual([]); }); // R3: same-tail types in different scopes must NOT merge — each method owns // through its own distinct node (positive owner-identity, not just dangle-free). it('keeps Outer::Inner and Other::Inner distinct (no cross-wired methods)', () => { const hasMethod = getRelationships(result, 'HAS_METHOD'); const outer = hasMethod.find((e) => e.target === 'from_outer'); const other = hasMethod.find((e) => e.target === 'from_other'); expect(outer).toBeDefined(); expect(other).toBeDefined(); const ownerQn = (e: typeof outer) => result.graph.getNode(e!.rel.sourceId)?.properties.qualifiedName; expect(ownerQn(outer)).toBe('Outer.Inner'); expect(ownerQn(other)).toBe('Other.Inner'); expect(outer!.rel.sourceId).not.toBe(other!.rel.sourceId); // Discriminator: with qualifiedNodeId ON the owner node id is keyed by the // NORMALIZED dotted path (Struct:...:Outer.Inner); with the fix OFF the // out-of-line node is keyed by the raw scoped text (...:Outer::Inner). The // `qualifiedName` PROPERTY is normalized either way, so assert on the id to // actually prove the fix is engaged (test-soundness, workflow finding #5). expect(outer!.rel.sourceId).toContain('Outer.Inner'); expect(outer!.rel.sourceId).not.toContain('::'); expect(other!.rel.sourceId).not.toContain('::'); }); }); // --------------------------------------------------------------------------- // Inline nested same-tail collision — distinct qualified nodes (issue #1978) // // `struct Outer { struct Inner {...} }` + `struct Other { struct Inner {...} }` // must materialize TWO distinct Struct nodes (qn Outer.Inner vs Other.Inner), // each owning its own method/field. On the pre-fix base both Inner structs // merge into one simple-keyed node and the methods cross-wire (dangling:0 but // wrong). Asserts positive owner-identity via the resolved node's qualifiedName, // not just dangle-free (R7). Distinct from the #1977 out-of-line case above. // --------------------------------------------------------------------------- describe('C++ inline nested same-tail collision — distinct qualified nodes (issue #1978)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-nested-tail-collision'), () => {}); }, 60000); it('materializes Outer.Inner and Other.Inner as two distinct Struct nodes', () => { const qns = getNodesByLabelFull(result, 'Struct') .map((n) => n.properties.qualifiedName) .filter((q) => q === 'Outer.Inner' || q === 'Other.Inner') .sort(); expect(qns).toEqual(['Other.Inner', 'Outer.Inner']); }); it('owns from_outer / from_other through their OWN distinct node (positive identity, R7)', () => { expect(findDanglingEdges(result, ['HAS_METHOD', 'HAS_PROPERTY'])).toEqual([]); const hm = getRelationships(result, 'HAS_METHOD'); const ownerQn = (target: string) => { const e = hm.find((x) => x.target === target); expect(e, `HAS_METHOD -> ${target}`).toBeDefined(); return result.graph.getNode(e!.rel.sourceId)?.properties.qualifiedName; }; expect(ownerQn('from_outer')).toBe('Outer.Inner'); expect(ownerQn('from_other')).toBe('Other.Inner'); }); it('owns outer_field under Outer.Inner (struct field via the main HAS_PROPERTY path)', () => { const hp = getRelationships(result, 'HAS_PROPERTY'); const e = hp.find((x) => x.target === 'outer_field'); expect(e).toBeDefined(); expect(result.graph.getNode(e!.rel.sourceId)?.properties.qualifiedName).toBe('Outer.Inner'); }); }); // Same collision fixture, forced through the WORKER pool (parse-worker.ts) rather // than the sequential parsing-processor.ts. Production parses repos >= 15 files via // the pool, so the qualified node-id + owner-edge logic must hold on BOTH paths // (workflow finding #4: the #1978 fixtures otherwise only exercise the sequential // path). Asserts worker == sequential for the distinct-node + owner outcome. describe('C++ inline nested same-tail collision — worker path parity (issue #1978)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-nested-tail-collision'), () => {}, { // Force the worker-pool gate low so the 1-file fixture engages the pool. workerPoolSize: 2, }); }, 120000); it('genuinely used the worker pool (guards against silent sequential fallback)', () => { expect(result.usedWorkerPool).toBe(true); }); it('materializes two distinct Struct nodes and owns each method correctly (R7)', () => { const qns = getNodesByLabelFull(result, 'Struct') .map((n) => n.properties.qualifiedName) .filter((q) => q === 'Outer.Inner' || q === 'Other.Inner') .sort(); expect(qns).toEqual(['Other.Inner', 'Outer.Inner']); expect(findDanglingEdges(result, ['HAS_METHOD', 'HAS_PROPERTY'])).toEqual([]); const hm = getRelationships(result, 'HAS_METHOD'); const ownerQn = (target: string) => result.graph.getNode(hm.find((x) => x.target === target)!.rel.sourceId)?.properties .qualifiedName; expect(ownerQn('from_outer')).toBe('Outer.Inner'); expect(ownerQn('from_other')).toBe('Other.Inner'); }); it('resolves DerivedB : Other::Inner → EXTENDS Other.Inner on the worker path (#1982: rawQualifiedName survives worker serialization)', () => { const e = getRelationships(result, 'EXTENDS').find( (x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === 'DerivedB', ); expect(e, 'DerivedB EXTENDS edge (worker path)').toBeDefined(); expect(e!.rel.targetId).toContain('Other.Inner'); expect(e!.rel.targetId).not.toContain('Outer.Inner'); }); it('resolves DerivedA : Outer::Inner → EXTENDS Outer.Inner on the worker path (parity + no duplicate)', () => { const edges = getRelationships(result, 'EXTENDS').filter( (x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === 'DerivedA', ); expect(edges, 'DerivedA EXTENDS edges (worker path)').toHaveLength(1); expect(edges[0]!.rel.targetId).toContain('Outer.Inner'); expect(edges[0]!.rel.targetId).not.toContain('Other.Inner'); }); }); // --------------------------------------------------------------------------- // Named-union nested same-tail collision — distinct qualified nodes (issue #1995) // // `union U1 { struct Inner {...} }` + `union U2 { struct Inner {...} }` must // materialize TWO distinct Struct nodes (qn U1.Inner / U2.Inner). `union_specifier` // was missing from cppClassConfig.ancestorScopeNodeTypes, so both Inner structs // qualified to the bare `Inner` and merged (dangling:0 but wrong). Mirrors the // #1978 inline-collision template; positive owner-identity, not just dangle-free. // --------------------------------------------------------------------------- describe('C++ named-union nested same-tail collision — distinct qualified nodes (issue #1995)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-union-nested-tail-collision'), () => {}, ); }, 60000); it('materializes U1.Inner and U2.Inner as two distinct Struct nodes [#1995-union]', () => { const qns = getNodesByLabelFull(result, 'Struct') .map((n) => n.properties.qualifiedName) .filter((q) => q === 'U1.Inner' || q === 'U2.Inner') .sort(); expect(qns).toEqual(['U1.Inner', 'U2.Inner']); }); it('owns from_u1 / from_u2 through their OWN distinct node (positive identity) [#1995-union]', () => { expect(findDanglingEdges(result, ['HAS_METHOD'])).toEqual([]); const hm = getRelationships(result, 'HAS_METHOD'); const ownerQn = (target: string) => { const e = hm.find((x) => x.target === target); expect(e, `HAS_METHOD -> ${target}`).toBeDefined(); return result.graph.getNode(e!.rel.sourceId)?.properties.qualifiedName; }; expect(ownerQn('from_u1')).toBe('U1.Inner'); expect(ownerQn('from_u2')).toBe('U2.Inner'); }); }); // Worker-path parity for the named-union collision (parse-worker.ts must qualify // the union scope byte-identically to the sequential parser). describe('C++ named-union nested same-tail collision — worker path parity (issue #1995)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-union-nested-tail-collision'), () => {}, { workerPoolSize: 2 }, ); }, 120000); it('genuinely used the worker pool [#1995-union]', () => { expect(result.usedWorkerPool).toBe(true); }); it('materializes U1.Inner / U2.Inner and owns each method on the worker path [#1995-union]', () => { const qns = getNodesByLabelFull(result, 'Struct') .map((n) => n.properties.qualifiedName) .filter((q) => q === 'U1.Inner' || q === 'U2.Inner') .sort(); expect(qns).toEqual(['U1.Inner', 'U2.Inner']); expect(findDanglingEdges(result, ['HAS_METHOD'])).toEqual([]); const hm = getRelationships(result, 'HAS_METHOD'); const ownerQn = (target: string) => result.graph.getNode(hm.find((x) => x.target === target)!.rel.sourceId)?.properties .qualifiedName; expect(ownerQn('from_u1')).toBe('U1.Inner'); expect(ownerQn('from_u2')).toBe('U2.Inner'); }); }); // --------------------------------------------------------------------------- // Anonymous-namespace nested same-tail collision — distinct nodes (issue #1995) // // Two `namespace { struct Inner {...} }` blocks must materialize TWO distinct // Struct nodes. An anonymous namespace_definition has no `name` child, so both // Inner structs qualified to the bare `Inner` and merged. A C++ extractScopeSegments // override gives each anon block a deterministic start-byte discriminator. The // discriminator value is not portable, so assert on node DISTINCTNESS (count==2 / // distinct owner ids), never a literal qualifiedName. // --------------------------------------------------------------------------- describe('C++ anonymous-namespace nested same-tail collision — distinct nodes (issue #1995)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-anon-ns-tail-collision'), () => {}); }, 60000); it('materializes two distinct Struct Inner nodes (one per anon namespace) [#1995-anon]', () => { const innerQns = getNodesByLabelFull(result, 'Struct') .map((n) => n.properties.qualifiedName) .filter((q): q is string => typeof q === 'string' && q.endsWith('Inner')); // Start-byte discriminator → assert DISTINCTNESS, not a literal value. Pre-fix // both Inner structs merge onto one bare `Inner` node (set size 1). expect(new Set(innerQns).size).toBe(2); }); it('owns from_anon_a / from_anon_b through DISTINCT nodes (no merge) [#1995-anon]', () => { expect(findDanglingEdges(result, ['HAS_METHOD'])).toEqual([]); const hm = getRelationships(result, 'HAS_METHOD'); const a = hm.find((x) => x.target === 'from_anon_a'); const b = hm.find((x) => x.target === 'from_anon_b'); expect(a, 'HAS_METHOD -> from_anon_a').toBeDefined(); expect(b, 'HAS_METHOD -> from_anon_b').toBeDefined(); expect(a!.rel.sourceId).not.toBe(b!.rel.sourceId); }); }); // Worker-path parity for the anonymous-namespace collision: the start-byte // discriminator must be deterministic across the worker's full-file parse. describe('C++ anonymous-namespace nested same-tail collision — worker path parity (issue #1995)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-anon-ns-tail-collision'), () => {}, { workerPoolSize: 2 }, ); }, 120000); it('genuinely used the worker pool [#1995-anon]', () => { expect(result.usedWorkerPool).toBe(true); }); it('materializes two distinct anon Inner nodes and owns each method on the worker path [#1995-anon]', () => { const innerQns = getNodesByLabelFull(result, 'Struct') .map((n) => n.properties.qualifiedName) .filter((q): q is string => typeof q === 'string' && q.endsWith('Inner')); expect(new Set(innerQns).size).toBe(2); expect(findDanglingEdges(result, ['HAS_METHOD'])).toEqual([]); const hm = getRelationships(result, 'HAS_METHOD'); const a = hm.find((x) => x.target === 'from_anon_a'); const b = hm.find((x) => x.target === 'from_anon_b'); expect(a, 'HAS_METHOD -> from_anon_a').toBeDefined(); expect(b, 'HAS_METHOD -> from_anon_b').toBeDefined(); expect(a!.rel.sourceId).not.toBe(b!.rel.sourceId); }); }); // --------------------------------------------------------------------------- // Inline nested same-tail HERITAGE — qualified base resolution (issue #1982) // // `struct DerivedA : Outer::Inner` + `struct DerivedB : Other::Inner` must each // resolve EXTENDS to the MATCHING nested node. The qualifier is discarded // (cpp/captures.ts emits the bare tail `Inner`), so resolveInheritanceBaseInScope // sees an ambiguous same-tail base. Asserts the resolved EXTENDS endpoint's id // contains the right qn (KTD-4: assert on the node id, not the property). // --------------------------------------------------------------------------- describe('C++ inline nested same-tail heritage — qualified base (issue #1982)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-nested-tail-collision'), () => {}); }, 60000); const extendsTargetIdOf = (childQn: string): string | undefined => { const ext = getRelationships(result, 'EXTENDS'); const e = ext.find( (x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === childQn, ); return e?.rel.targetId; }; it('resolves DerivedA : Outer::Inner → EXTENDS the Outer.Inner node', () => { const tid = extendsTargetIdOf('DerivedA'); expect(tid, 'DerivedA EXTENDS endpoint').toBeDefined(); expect(tid).toContain('Outer.Inner'); expect(tid).not.toContain('Other.Inner'); }); it('resolves DerivedB : Other::Inner → EXTENDS the Other.Inner node (not Outer.Inner)', () => { const tid = extendsTargetIdOf('DerivedB'); expect(tid, 'DerivedB EXTENDS endpoint').toBeDefined(); expect(tid).toContain('Other.Inner'); expect(tid).not.toContain('Outer.Inner'); }); }); // --------------------------------------------------------------------------- // Namespaced same-tail nested heritage — qualified base resolution (issue #1982) // // `namespace NS { struct A{struct Inner{};}; struct B{struct Inner{};}; // struct DA:A::Inner{}; struct DB:B::Inner{}; }` — the bases NS::A::Inner and // NS::B::Inner are namespace-nested. The structure phase materializes distinct // NS.A.Inner / NS.B.Inner nodes, but the scope-model def.qualifiedName dropped // the namespace (`A.Inner` not `NS.A.Inner`), so resolveDefGraphId missed the // namespaced node key and the simpleKey('Inner') fallback collapsed both bases — // DB's EXTENDS pointed at NS.A.Inner. Asserts each Derived EXTENDS its own // namespaced base by NODE ID (KTD3). Registry-primary only. // --------------------------------------------------------------------------- describe('C++ namespaced same-tail nested heritage — qualified base (issue #1982)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-namespaced-collision'), () => {}); }, 60000); const extendsTargetIdOf = (childQn: string): string | undefined => { const ext = getRelationships(result, 'EXTENDS'); const e = ext.find( (x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === childQn, ); return e?.rel.targetId; }; it('resolves NS::DA : A::Inner → EXTENDS the NS.A.Inner node', () => { const tid = extendsTargetIdOf('NS.DA'); expect(tid, 'NS.DA EXTENDS endpoint').toBeDefined(); expect(tid).toContain('NS.A.Inner'); expect(tid).not.toContain('NS.B.Inner'); }); it('resolves NS::DB : B::Inner → EXTENDS the NS.B.Inner node (not NS.A.Inner)', () => { const tid = extendsTargetIdOf('NS.DB'); expect(tid, 'NS.DB EXTENDS endpoint').toBeDefined(); expect(tid).toContain('NS.B.Inner'); expect(tid).not.toContain('NS.A.Inner'); }); }); // Same namespaced fixture through the WORKER pool — the namespacePrefix tag is // applied during main-process scope-resolution (after worker parse/merge), so // the fix must hold on both paths. Registry-primary only. describe('C++ namespaced same-tail nested heritage — worker path parity (issue #1982)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-namespaced-collision'), () => {}, { workerPoolSize: 2, }); }, 120000); it('genuinely used the worker pool for the namespaced fixture', () => { expect(result.usedWorkerPool).toBe(true); }); it('resolves NS::DA / NS::DB to their own namespaced base on the worker path', () => { const extendsTargetIdOf = (childQn: string): string | undefined => { const ext = getRelationships(result, 'EXTENDS'); const e = ext.find( (x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === childQn, ); return e?.rel.targetId; }; const da = extendsTargetIdOf('NS.DA'); const db = extendsTargetIdOf('NS.DB'); expect(da, 'NS.DA EXTENDS (worker)').toBeDefined(); expect(db, 'NS.DB EXTENDS (worker)').toBeDefined(); expect(da).toContain('NS.A.Inner'); expect(db).toContain('NS.B.Inner'); expect(db).not.toContain('NS.A.Inner'); }); }); // --------------------------------------------------------------------------- // Root-anchored base must not pick up enclosing-relative segments (issue #1982) // // `namespace Outer { struct Wrap { struct A{struct Inner{};}; struct D : ::A::Inner {}; }; }` // with a GLOBAL `struct A { struct Inner {}; }` — the leading `::` names the // global type. Without the root-anchor guard, resolveQualifiedInheritanceBase // prepends the deriving class's enclosing segments and tries `Wrap.A.Inner` // first, mis-binding D to the inner type. With it, only the root-anchored // `A.Inner` key is tried → the global type. Registry-primary only. // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // Cross-namespace same-tail nested heritage — bridge-held tie-break (issue #1993) // // NS1::A::Inner and NS2::A::Inner both key the namespace-omitted `A.Inner` in the // qualifiedNames index, so resolveQualifiedInheritanceBase refused-on-tie and the // scope-walk fallback first-wins to NS1's Inner — DB CROSS-WIRES its EXTENDS to // NS1::A::Inner (DA resolves correctly only by that first-wins luck). The cross-wire // still resolves to a real node, so findDanglingEdges can't catch it, and the #1982 // bridge can't reach it either (it rescues the structure-phase node lookup, not the // resolution-index tie). The `namespacePrefix` sidecar breaks the tie: DA's enclosing // namespace NS1 selects NS1::A::Inner. Bridge-held — def.qualifiedName and the index // keys are unchanged. Registry-primary only (the qualified-base resolver is the bridge). // --------------------------------------------------------------------------- describe('C++ cross-namespace same-tail nested heritage — bridge-held tie-break (issue #1993)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-cross-namespace-same-tail'), () => {}, ); }, 60000); it('routes NS1.DA EXTENDS NS1.A.Inner and NS2.DB EXTENDS NS2.A.Inner (no cross-ns tie)', () => { const extendsEdges = getRelationships(result, 'EXTENDS'); const baseQnOf = (derivedQn: string) => { const e = extendsEdges.find( (x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === derivedQn, ); expect(e, `EXTENDS from ${derivedQn}`).toBeDefined(); return result.graph.getNode(e!.rel.targetId)?.properties.qualifiedName; }; expect(baseQnOf('NS1.DA')).toBe('NS1.A.Inner'); expect(baseQnOf('NS2.DB')).toBe('NS2.A.Inner'); }); }); describe('C++ cross-namespace same-tail nested heritage — worker path parity (issue #1993)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo( path.join(FIXTURES, 'cpp-cross-namespace-same-tail'), () => {}, { workerPoolSize: 2 }, ); }, 120000); it('genuinely used the worker pool for the cross-namespace fixture', () => { expect(result.usedWorkerPool).toBe(true); }); it('routes NS1.DA / NS2.DB to their own namespaced base on the worker path (no cross-ns tie)', () => { const extendsEdges = getRelationships(result, 'EXTENDS'); const baseQnOf = (derivedQn: string) => { const e = extendsEdges.find( (x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === derivedQn, ); expect(e, `EXTENDS from ${derivedQn} (worker)`).toBeDefined(); return result.graph.getNode(e!.rel.targetId)?.properties.qualifiedName; }; expect(baseQnOf('NS1.DA')).toBe('NS1.A.Inner'); expect(baseQnOf('NS2.DB')).toBe('NS2.A.Inner'); }); }); describe('C++ root-anchored base ignores enclosing-relative type (issue #1982)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-global-base-anchor'), () => {}); }, 60000); it('resolves Outer::Wrap::D : ::A::Inner → EXTENDS the GLOBAL A.Inner (not Wrap.A.Inner)', () => { const e = getRelationships(result, 'EXTENDS').find( (x) => result.graph.getNode(x.rel.sourceId)?.properties.qualifiedName === 'Outer.Wrap.D', ); expect(e, 'Outer.Wrap.D EXTENDS endpoint').toBeDefined(); // Global node id is `Struct:main.cpp:A.Inner`; the enclosing-relative type // is `Struct:main.cpp:Outer.Wrap.A.Inner`. KTD3: discriminate on the node id. expect(e!.rel.targetId).toContain('A.Inner'); expect(e!.rel.targetId).not.toContain('Wrap'); }); }); describe('C++ deleted overload selection (#1893 A2)', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-deleted-overload'), () => {}); }, 60000); const callsFrom = (source: string, target: string) => getRelationships(result, 'CALLS').filter( (edge) => edge.source === source && edge.target === target, ); const targetParameterTypes = (source: string, target: string) => { const edge = callsFrom(source, target); expect(edge).toHaveLength(1); return result.graph.getNode(edge[0]!.rel.targetId)?.properties.parameterTypes; }; it('keeps a live free-function winner callable', () => { expect(callsFrom('call_live_free', 'choose')).toHaveLength(1); }); it('suppresses a deleted best free-function match instead of rerouting', () => { expect(callsFrom('call_deleted_free', 'choose')).toHaveLength(0); }); it('keeps a live member winner callable', () => { expect(targetParameterTypes('call_live_member', 'touch')).toEqual(['int']); }); it('suppresses a deleted best member match instead of rerouting', () => { expect(callsFrom('call_deleted_member', 'touch')).toHaveLength(0); }); it('keeps a defaulted constructor callable', () => { expect(callsFrom('call_defaulted_constructor', 'Gadget')).toHaveLength(1); }); it('ranks a live base-qualified overload declared after a deleted sibling', () => { expect(targetParameterTypes('call_base_qualified_live', 'select')).toEqual(['int']); }); it('ranks inherited overloads before applying deleted suppression', () => { expect(targetParameterTypes('call_inherited_live', 'select')).toEqual(['int']); expect(callsFrom('call_inherited_deleted', 'select')).toHaveLength(0); }); it('ranks class-qualified static overloads before applying deleted suppression', () => { expect(targetParameterTypes('call_static_live', 'select')).toEqual(['int']); expect(callsFrom('call_static_deleted', 'select')).toHaveLength(0); }); it('ranks namespace-qualified overloads before applying deleted suppression', () => { expect(targetParameterTypes('call_namespace_live', 'select')).toEqual(['int']); expect(callsFrom('call_namespace_deleted', 'select')).toHaveLength(0); }); it('keeps a same-arity defaulted copy constructor callable', () => { expect(callsFrom('call_same_arity_defaulted_constructor', 'DefaultedChoice')).toHaveLength(1); }); it('records every deleted-winner suppression explicitly', () => { const outcomes = getResolutionOutcomes(result).filter( (outcome) => outcome.kind === 'suppressed' && outcome.reason === 'selected-callable-deleted', ); expect(outcomes).toHaveLength(5); expect(outcomes.map((outcome) => outcome.name).sort()).toEqual([ 'choose', 'select', 'select', 'select', 'touch', ]); }); }); // --------------------------------------------------------------------------- // Structural receiver typing for a `->` base receiver. // // The all-language rollout is proven behaviourally for TypeScript only; this is // the second language, and the one the rollout measurably changed: C++ // `svc->getUser()->save()` emitted NO edge and recorded NO drop before it. // --------------------------------------------------------------------------- describe('C++ structural receiver chain through a pointer base', () => { let result: PipelineResult; beforeAll(async () => { result = await runPipelineFromRepo(path.join(FIXTURES, 'cpp-receiver-chain-arrow'), () => {}); }, 60000); it('resolves svc->getUser()->save() — the `->` base the text cascade could not parse', () => { const calls = getRelationships(result, 'CALLS'); expect( calls.find((c) => c.source === 'pointerArrowChain' && c.target === 'save'), ).toMatchObject({ target: 'save' }); }); it('keeps the value-dot base resolving (control — worked before this work)', () => { const calls = getRelationships(result, 'CALLS'); expect(calls.find((c) => c.source === 'valueDotChain' && c.target === 'save')).toMatchObject({ target: 'save', }); }); });