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.
This commit is contained in:
Gergo Magyar 2026-03-19 21:37:21 +00:00
parent 00c5126b24
commit 3e29f4e4b9
3 changed files with 46 additions and 17 deletions

View file

@ -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

View file

@ -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<string, Map<string, SymbolDefinition>>();
// 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<string, Map<string, SymbolDefinition[]>>();
// 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 };
};

View file

@ -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');
});
});