/** * Unit coverage for `lookupBindingsAt` — the dual-source binding * lookup primitive used by every walker that needs cross-file * visibility (Step 2 of the binding-augmentation-channel refactor). * * These tests pin the contract exhaustively: precedence (finalized * first), dedup (by `def.nodeId`), empty-array semantics, and the * shared-empty-frozen-array identity for misses. Every other walker * test in this directory delegates to `lookupBindingsAt` after the * refactor, so a regression here surfaces quickly. */ import { describe, it, expect } from 'vitest'; import { findAllCallableBindingCandidatesInScope, findCallableBindingInScope, findClassBindingInScope, findExportedDefByName, lookupBindingsAt, } from '../../../src/core/ingestion/scope-resolution/scope/walkers.js'; import type { BindingRef, Scope, ScopeId, ScopeTree, SymbolDefinition } from 'gitnexus-shared'; import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js'; import type { WorkspaceResolutionIndex } from '../../../src/core/ingestion/scope-resolution/workspace-index.js'; const SCOPE = 'scope:m' as ScopeId; const def = (nodeId: string): SymbolDefinition => ({ nodeId, filePath: 'm.ts', type: 'Function' }) as SymbolDefinition; const ref = (nodeId: string, origin: BindingRef['origin'] = 'local'): BindingRef => ({ def: def(nodeId), origin }) as BindingRef; function indexesWith({ finalized, augmented, workspace, }: { finalized?: readonly BindingRef[]; augmented?: readonly BindingRef[]; workspace?: readonly BindingRef[]; }): ScopeResolutionIndexes { const bindings = new Map>(); if (finalized !== undefined) { Object.freeze(finalized as BindingRef[]); bindings.set(SCOPE, new Map([['name', finalized]])); } const bindingAugmentations = new Map>(); if (augmented !== undefined) bindingAugmentations.set(SCOPE, new Map([['name', augmented]])); const workspaceFqnBindings = new Map(); if (workspace !== undefined) workspaceFqnBindings.set('name', workspace); return { bindings, bindingAugmentations, workspaceFqnBindings, } as unknown as ScopeResolutionIndexes; } function scope(id: ScopeId, bindings = new Map()): Scope { return { id, kind: 'Module', parent: null, filePath: 'm.ts', range: { startLine: 1, startColumn: 0, endLine: 1, endColumn: 0 }, bindings, imports: [], ownedDefs: [], typeBindings: new Map(), } as unknown as Scope; } function indexesForScopeLookup( moduleScope: Scope, augmented: Map, ): ScopeResolutionIndexes { const scopeTree = { getScope: (id: ScopeId) => (id === moduleScope.id ? moduleScope : undefined), } as unknown as ScopeTree; return { scopeTree, bindings: new Map(), bindingAugmentations: new Map([[moduleScope.id, augmented]]), } as unknown as ScopeResolutionIndexes; } describe('lookupBindingsAt', () => { it('returns the finalized bucket when augmentations are absent', () => { const finalized = [ref('A'), ref('B')]; const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized })); expect(out).toEqual(finalized); // Identity preserved when only one channel populates — no allocation. expect(out).toBe(finalized); }); it('returns the augmented bucket when finalized is absent', () => { const augmented = [ref('X', 'namespace')]; const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ augmented })); expect(out).toEqual(augmented); expect(out).toBe(augmented); }); it('concatenates with finalized first when both populate disjoint nodeIds', () => { const finalized = [ref('A', 'import'), ref('B', 'import')]; const augmented = [ref('C', 'namespace'), ref('D', 'namespace')]; const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented })); expect(out.map((b) => b.def.nodeId)).toEqual(['A', 'B', 'C', 'D']); }); it('dedupes augmented entries that share a nodeId with finalized (finalized wins)', () => { const finalized = [ref('A', 'import'), ref('B', 'import')]; const augmented = [ref('A', 'namespace'), ref('C', 'namespace')]; const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented })); expect(out.map((b) => b.def.nodeId)).toEqual(['A', 'B', 'C']); expect(out.find((b) => b.def.nodeId === 'A')!.origin).toBe('import'); }); // Third channel: workspaceFqnBindings (scope-independent — global-namespace // C# types / PHP FQNs). Consulted LAST, after finalized + augmented. it('returns the workspace bucket when it is the only channel', () => { const workspace = [ref('W', 'namespace')]; const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ workspace })); expect(out).toEqual(workspace); expect(out).toBe(workspace); // identity preserved when only one channel populates }); it('appends workspace entries after finalized and augmented', () => { const finalized = [ref('A', 'import')]; const augmented = [ref('B', 'namespace')]; const workspace = [ref('C', 'namespace')]; const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented, workspace })); expect(out.map((b) => b.def.nodeId)).toEqual(['A', 'B', 'C']); }); it('dedupes workspace entries already present in finalized/augmented (workspace loses)', () => { const finalized = [ref('A', 'import')]; const augmented = [ref('B', 'namespace')]; const workspace = [ref('A', 'namespace'), ref('B', 'namespace'), ref('C', 'namespace')]; const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented, workspace })); expect(out.map((b) => b.def.nodeId)).toEqual(['A', 'B', 'C']); // The surviving A/B keep their finalized/augmented identity, not workspace's. expect(out.find((b) => b.def.nodeId === 'A')!.origin).toBe('import'); }); it('keeps finalized metadata when the same nodeId appears in both channels', () => { const finalizedDef = { nodeId: 'A', filePath: 'finalized.ts', qualifiedName: 'finalized.A', type: 'Function', } as SymbolDefinition; const augmentedDef = { nodeId: 'A', filePath: 'augmented.ts', qualifiedName: 'augmented.A', type: 'Method', } as SymbolDefinition; const out = lookupBindingsAt( SCOPE, 'name', indexesWith({ finalized: [{ def: finalizedDef, origin: 'import' } as BindingRef], augmented: [{ def: augmentedDef, origin: 'namespace' } as BindingRef], }), ); expect(out).toHaveLength(1); expect(out[0]!.def.filePath).toBe('finalized.ts'); expect(out[0]!.def.qualifiedName).toBe('finalized.A'); expect(out[0]!.origin).toBe('import'); }); it('returns the shared empty array on a miss in both channels', () => { const a = lookupBindingsAt(SCOPE, 'name', indexesWith({})); const b = lookupBindingsAt(SCOPE, 'other', indexesWith({})); expect(a).toEqual([]); expect(b).toEqual([]); expect(a).toBe(b); expect(Object.isFrozen(a)).toBe(true); }); it('treats an empty finalized bucket as absent (returns augmented)', () => { const augmented = [ref('Z', 'namespace')]; const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized: [], augmented })); expect(out).toBe(augmented); }); it('treats an empty augmented bucket as absent (returns finalized)', () => { const finalized = [ref('Z', 'import')]; const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented: [] })); expect(out).toBe(finalized); }); it('returns the shared empty array when both buckets exist but are empty', () => { const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized: [], augmented: [] })); expect(out).toEqual([]); expect(Object.isFrozen(out)).toBe(true); }); }); describe('walker helpers read bindingAugmentations', () => { it('findClassBindingInScope finds class-like refs that exist only in augmentations', () => { const moduleScope = scope(SCOPE); const classRef = { def: { ...def('ClassA'), type: 'Class' }, origin: 'namespace', } as BindingRef; const indexes = indexesForScopeLookup(moduleScope, new Map([['ClassA', [classRef]]])); expect(findClassBindingInScope(SCOPE, 'ClassA', indexes)?.nodeId).toBe('ClassA'); }); it('findCallableBindingInScope finds callable refs that exist only in augmentations', () => { const moduleScope = scope(SCOPE); const callableRef = { def: def('callMe'), origin: 'import' } as BindingRef; const indexes = indexesForScopeLookup(moduleScope, new Map([['callMe', [callableRef]]])); expect(findCallableBindingInScope(SCOPE, 'callMe', indexes)?.nodeId).toBe('callMe'); }); it('preserves augmentation provenance masked by a finalized binding', () => { const moduleScope = scope(SCOPE); const callable = def('callMe'); const finalized = { def: callable, origin: 'local' } as BindingRef; const staticImport = { def: callable, origin: 'import', visibility: 'static-member-import', } as BindingRef; const indexes = indexesForScopeLookup(moduleScope, new Map([['callMe', [staticImport]]])); indexes.bindings.set(SCOPE, new Map([['callMe', [finalized]]])); const candidates = findAllCallableBindingCandidatesInScope(SCOPE, 'callMe', indexes); expect(candidates).toHaveLength(1); expect(candidates[0]!.def).toBe(callable); expect(candidates[0]!.bindings).toEqual([finalized, staticImport]); }); it('findExportedDefByName finds callable refs that exist only in augmentations', () => { const moduleScope = scope(SCOPE); const callableRef = { def: def('fromAugmentation'), origin: 'import' } as BindingRef; const indexes = indexesForScopeLookup(moduleScope, new Map([['run', [callableRef]]])); const workspaceIndex = { moduleScopeByFile: new Map(), } as unknown as WorkspaceResolutionIndex; expect(findExportedDefByName('run', SCOPE, indexes, workspaceIndex)?.nodeId).toBe( 'fromAugmentation', ); }); });