From 3e29f4e4b9c0399e015b069c025f7114055ce2ed Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Thu, 19 Mar 2026 21:37:21 +0000 Subject: [PATCH] fix(symbol-table): store all overloads in fileIndex instead of last-write-wins The fileIndex Map stored SymbolDefinition per name, silently dropping earlier overloads via Map.set(). Changed to SymbolDefinition[] so all same-name methods (e.g., Java overloads) survive in same-file resolution. Added lookupExactAll() for resolution-context to pass all same-file candidates through to candidate filtering. --- .../src/core/ingestion/resolution-context.ts | 8 ++--- gitnexus/src/core/ingestion/symbol-table.ts | 34 +++++++++++++++---- gitnexus/test/unit/symbol-table.test.ts | 21 ++++++++---- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/gitnexus/src/core/ingestion/resolution-context.ts b/gitnexus/src/core/ingestion/resolution-context.ts index bac5bfb94..7184bd954 100644 --- a/gitnexus/src/core/ingestion/resolution-context.ts +++ b/gitnexus/src/core/ingestion/resolution-context.ts @@ -81,10 +81,10 @@ export const createResolutionContext = (): ResolutionContext => { // --- Core resolution (single implementation of tier logic) --- const resolveUncached = (name: string, fromFile: string): TieredCandidates | null => { - // Tier 1: Same file — authoritative match - const localDef = symbols.lookupExactFull(fromFile, name); - if (localDef) { - return { candidates: [localDef], tier: 'same-file' }; + // Tier 1: Same file — authoritative match (returns all overloads) + const localDefs = symbols.lookupExactAll(fromFile, name); + if (localDefs.length > 0) { + return { candidates: localDefs, tier: 'same-file' }; } // Get all global definitions for subsequent tiers diff --git a/gitnexus/src/core/ingestion/symbol-table.ts b/gitnexus/src/core/ingestion/symbol-table.ts index 5d3fbef7c..92d705db7 100644 --- a/gitnexus/src/core/ingestion/symbol-table.ts +++ b/gitnexus/src/core/ingestion/symbol-table.ts @@ -34,9 +34,17 @@ export interface SymbolTable { /** * High Confidence: Look for a symbol in a specific file, returning full definition. * Includes type information needed for heritage resolution (Class vs Interface). + * Returns first matching definition — use lookupExactAll for overloaded methods. */ lookupExactFull: (filePath: string, name: string) => SymbolDefinition | undefined; + /** + * High Confidence: Look for ALL symbols with this name in a specific file. + * Returns all definitions, including overloaded methods with the same name. + * Used by resolution-context to pass all same-file overloads to candidate filtering. + */ + lookupExactAll: (filePath: string, name: string) => SymbolDefinition[]; + /** * Low Confidence: Look for a symbol anywhere in the project * Used when imports are missing or for framework magic @@ -69,9 +77,10 @@ export interface SymbolTable { } export const createSymbolTable = (): SymbolTable => { - // 1. File-Specific Index — stores full SymbolDefinition for O(1) lookupExactFull - // Structure: FilePath -> (SymbolName -> SymbolDefinition) - const fileIndex = new Map>(); + // 1. File-Specific Index — stores full SymbolDefinition(s) for O(1) lookup. + // Structure: FilePath -> (SymbolName -> SymbolDefinition[]) + // Array allows overloaded methods (same name, different signatures) to coexist. + const fileIndex = new Map>(); // 2. Global Reverse Index (The "Backup") // Structure: SymbolName -> [List of Definitions] @@ -109,7 +118,12 @@ export const createSymbolTable = (): SymbolTable => { if (!fileIndex.has(filePath)) { fileIndex.set(filePath, new Map()); } - fileIndex.get(filePath)!.set(name, def); + const fileMap = fileIndex.get(filePath)!; + if (!fileMap.has(name)) { + fileMap.set(name, [def]); + } else { + fileMap.get(name)!.push(def); + } // B. Properties go to fieldByOwner index only — skip globalIndex to prevent // namespace pollution for common names like 'id', 'name', 'type'. @@ -134,11 +148,17 @@ export const createSymbolTable = (): SymbolTable => { }; const lookupExact = (filePath: string, name: string): string | undefined => { - return fileIndex.get(filePath)?.get(name)?.nodeId; + const defs = fileIndex.get(filePath)?.get(name); + return defs?.[0]?.nodeId; }; const lookupExactFull = (filePath: string, name: string): SymbolDefinition | undefined => { - return fileIndex.get(filePath)?.get(name); + const defs = fileIndex.get(filePath)?.get(name); + return defs?.[0]; + }; + + const lookupExactAll = (filePath: string, name: string): SymbolDefinition[] => { + return fileIndex.get(filePath)?.get(name) ?? []; }; const lookupFuzzy = (name: string): SymbolDefinition[] => { @@ -173,5 +193,5 @@ export const createSymbolTable = (): SymbolTable => { fieldByOwner.clear(); }; - return { add, lookupExact, lookupExactFull, lookupFuzzy, lookupFuzzyCallable, lookupFieldByOwner, getStats, clear }; + return { add, lookupExact, lookupExactFull, lookupExactAll, lookupFuzzy, lookupFuzzyCallable, lookupFieldByOwner, getStats, clear }; }; diff --git a/gitnexus/test/unit/symbol-table.test.ts b/gitnexus/test/unit/symbol-table.test.ts index e1dc2b8da..7af8bfbcb 100644 --- a/gitnexus/test/unit/symbol-table.test.ts +++ b/gitnexus/test/unit/symbol-table.test.ts @@ -30,11 +30,13 @@ describe('SymbolTable', () => { expect(table.getStats().globalSymbolCount).toBe(1); }); - it('allows duplicate adds for same file and name', () => { + it('allows duplicate adds for same file and name (overloads preserved)', () => { table.add('src/a.ts', 'foo', 'func:foo:1', 'Function'); table.add('src/a.ts', 'foo', 'func:foo:2', 'Function'); - // File index overwrites: last wins - expect(table.lookupExact('src/a.ts', 'foo')).toBe('func:foo:2'); + // File index stores both overloads; lookupExact returns first + expect(table.lookupExact('src/a.ts', 'foo')).toBe('func:foo:1'); + // lookupExactAll returns all overloads + expect(table.lookupExactAll('src/a.ts', 'foo')).toHaveLength(2); // Global index appends expect(table.lookupFuzzy('foo')).toHaveLength(2); }); @@ -495,13 +497,20 @@ describe('SymbolTable', () => { expect(def!.ownerId).toBe('class:User'); }); - it('last-write-wins when same file and name are added twice', () => { + it('returns first definition when same file and name are added twice (overloads preserved)', () => { table.add('src/a.ts', 'foo', 'func:foo:v1', 'Function', { returnType: 'void' }); table.add('src/a.ts', 'foo', 'func:foo:v2', 'Function', { returnType: 'string' }); + // lookupExactFull returns first match const def = table.lookupExactFull('src/a.ts', 'foo'); expect(def).toBeDefined(); - expect(def!.nodeId).toBe('func:foo:v2'); - expect(def!.returnType).toBe('string'); + expect(def!.nodeId).toBe('func:foo:v1'); + expect(def!.returnType).toBe('void'); + // lookupExactAll returns all overloads + const all = table.lookupExactAll('src/a.ts', 'foo'); + expect(all).toHaveLength(2); + expect(all[0].nodeId).toBe('func:foo:v1'); + expect(all[1].nodeId).toBe('func:foo:v2'); + expect(all[1].returnType).toBe('string'); }); });