Skip to content

Commit ab9d718

Browse files
omonienclaude
andcommitted
perf: Cache import mappings and index fuzzy matches for resolution
Two major optimizations for the ref resolution phase: 1. Cache extractImportMappings() results per file path — previously re-read and re-parsed the source file for every single ref from that file (e.g. 100 refs from one file = 100 identical file reads) 2. Replace linear scan in matchFuzzy() with a lazily-built case-insensitive Map index — O(1) lookup instead of iterating all function/method/class nodes for every unresolved ref. Also drop low-value prefix matching (confidence 0.3). Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 44ea6af commit ab9d718

3 files changed

Lines changed: 53 additions & 33 deletions

File tree

src/resolution/import-resolver.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -425,21 +425,34 @@ function extractPHPImports(content: string): ImportMapping[] {
425425
return mappings;
426426
}
427427

428+
// Cache import mappings per file to avoid re-reading and re-parsing
429+
const importMappingCache = new Map<string, ImportMapping[]>();
430+
431+
/**
432+
* Clear the import mapping cache (call between indexing runs)
433+
*/
434+
export function clearImportMappingCache(): void {
435+
importMappingCache.clear();
436+
}
437+
428438
/**
429439
* Resolve a reference using import mappings
430440
*/
431441
export function resolveViaImport(
432442
ref: UnresolvedRef,
433443
context: ResolutionContext
434444
): ResolvedRef | null {
435-
// Read the source file to extract imports
436-
const content = context.readFile(ref.filePath);
437-
if (!content) {
438-
return null;
445+
// Use cached import mappings or extract and cache them
446+
let imports = importMappingCache.get(ref.filePath);
447+
if (!imports) {
448+
const content = context.readFile(ref.filePath);
449+
if (!content) {
450+
return null;
451+
}
452+
imports = extractImportMappings(ref.filePath, content, ref.language);
453+
importMappingCache.set(ref.filePath, imports);
439454
}
440455

441-
const imports = extractImportMappings(ref.filePath, content, ref.language);
442-
443456
// Check if the reference name matches any import
444457
for (const imp of imports) {
445458
if (imp.localName === ref.referenceName || ref.referenceName.startsWith(imp.localName + '.')) {

src/resolution/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
ResolutionContext,
1717
FrameworkResolver,
1818
} from './types';
19-
import { matchReference } from './name-matcher';
20-
import { resolveViaImport } from './import-resolver';
19+
import { matchReference, clearFuzzyIndex } from './name-matcher';
20+
import { resolveViaImport, clearImportMappingCache } from './import-resolver';
2121
import { detectFrameworks } from './frameworks';
2222
import { logDebug } from '../errors';
2323

@@ -106,6 +106,8 @@ export class ReferenceResolver {
106106
this.qualifiedNameCache.clear();
107107
this.kindCache.clear();
108108
this.nodeByIdCache.clear();
109+
clearImportMappingCache();
110+
clearFuzzyIndex();
109111
this.cachesWarmed = false;
110112
}
111113

src/resolution/name-matcher.ts

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -190,28 +190,46 @@ function findBestMatch(
190190
return bestNode;
191191
}
192192

193+
// Lazily-built case-insensitive index for fuzzy matching
194+
let fuzzyIndex: Map<string, Node[]> | null = null;
195+
196+
/**
197+
* Clear the fuzzy match index (call between indexing runs)
198+
*/
199+
export function clearFuzzyIndex(): void {
200+
fuzzyIndex = null;
201+
}
202+
193203
/**
194204
* Fuzzy match - last resort with lower confidence
195205
*/
196206
export function matchFuzzy(
197207
ref: UnresolvedRef,
198208
context: ResolutionContext
199209
): ResolvedRef | null {
200-
// Try case-insensitive match
201-
const allNodes = [
202-
...context.getNodesByKind('function'),
203-
...context.getNodesByKind('method'),
204-
...context.getNodesByKind('class'),
205-
];
210+
// Build case-insensitive index on first use
211+
if (!fuzzyIndex) {
212+
fuzzyIndex = new Map();
213+
const kinds: Array<Node['kind']> = ['function', 'method', 'class'];
214+
for (const kind of kinds) {
215+
for (const node of context.getNodesByKind(kind)) {
216+
const lower = node.name.toLowerCase();
217+
const existing = fuzzyIndex.get(lower);
218+
if (existing) {
219+
existing.push(node);
220+
} else {
221+
fuzzyIndex.set(lower, [node]);
222+
}
223+
}
224+
}
225+
}
206226

207227
const lowerName = ref.referenceName.toLowerCase();
208228

209-
// Exact case-insensitive match
210-
const caseInsensitive = allNodes.filter(
211-
(n) => n.name.toLowerCase() === lowerName
212-
);
229+
// Exact case-insensitive match via index (O(1) lookup)
230+
const caseInsensitive = fuzzyIndex.get(lowerName);
213231

214-
if (caseInsensitive.length === 1) {
232+
if (caseInsensitive && caseInsensitive.length === 1) {
215233
return {
216234
original: ref,
217235
targetNodeId: caseInsensitive[0]!.id,
@@ -220,20 +238,7 @@ export function matchFuzzy(
220238
};
221239
}
222240

223-
// Try prefix match (e.g., "get" matches "getUser")
224-
const prefixMatches = allNodes.filter((n) =>
225-
n.name.toLowerCase().startsWith(lowerName)
226-
);
227-
228-
if (prefixMatches.length === 1) {
229-
return {
230-
original: ref,
231-
targetNodeId: prefixMatches[0]!.id,
232-
confidence: 0.3,
233-
resolvedBy: 'fuzzy',
234-
};
235-
}
236-
241+
// Skip prefix matching — too expensive and low value (confidence 0.3)
237242
return null;
238243
}
239244

0 commit comments

Comments
 (0)