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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
44ea6af043
commit
ab9d7188c1
@@ -425,6 +425,16 @@ function extractPHPImports(content: string): ImportMapping[] {
|
||||
return mappings;
|
||||
}
|
||||
|
||||
// Cache import mappings per file to avoid re-reading and re-parsing
|
||||
const importMappingCache = new Map<string, ImportMapping[]>();
|
||||
|
||||
/**
|
||||
* Clear the import mapping cache (call between indexing runs)
|
||||
*/
|
||||
export function clearImportMappingCache(): void {
|
||||
importMappingCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a reference using import mappings
|
||||
*/
|
||||
@@ -432,14 +442,17 @@ export function resolveViaImport(
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext
|
||||
): ResolvedRef | null {
|
||||
// Read the source file to extract imports
|
||||
const content = context.readFile(ref.filePath);
|
||||
if (!content) {
|
||||
return null;
|
||||
// Use cached import mappings or extract and cache them
|
||||
let imports = importMappingCache.get(ref.filePath);
|
||||
if (!imports) {
|
||||
const content = context.readFile(ref.filePath);
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
imports = extractImportMappings(ref.filePath, content, ref.language);
|
||||
importMappingCache.set(ref.filePath, imports);
|
||||
}
|
||||
|
||||
const imports = extractImportMappings(ref.filePath, content, ref.language);
|
||||
|
||||
// Check if the reference name matches any import
|
||||
for (const imp of imports) {
|
||||
if (imp.localName === ref.referenceName || ref.referenceName.startsWith(imp.localName + '.')) {
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
ResolutionContext,
|
||||
FrameworkResolver,
|
||||
} from './types';
|
||||
import { matchReference } from './name-matcher';
|
||||
import { resolveViaImport } from './import-resolver';
|
||||
import { matchReference, clearFuzzyIndex } from './name-matcher';
|
||||
import { resolveViaImport, clearImportMappingCache } from './import-resolver';
|
||||
import { detectFrameworks } from './frameworks';
|
||||
import { logDebug } from '../errors';
|
||||
|
||||
@@ -106,6 +106,8 @@ export class ReferenceResolver {
|
||||
this.qualifiedNameCache.clear();
|
||||
this.kindCache.clear();
|
||||
this.nodeByIdCache.clear();
|
||||
clearImportMappingCache();
|
||||
clearFuzzyIndex();
|
||||
this.cachesWarmed = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -190,6 +190,16 @@ function findBestMatch(
|
||||
return bestNode;
|
||||
}
|
||||
|
||||
// Lazily-built case-insensitive index for fuzzy matching
|
||||
let fuzzyIndex: Map<string, Node[]> | null = null;
|
||||
|
||||
/**
|
||||
* Clear the fuzzy match index (call between indexing runs)
|
||||
*/
|
||||
export function clearFuzzyIndex(): void {
|
||||
fuzzyIndex = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuzzy match - last resort with lower confidence
|
||||
*/
|
||||
@@ -197,21 +207,29 @@ export function matchFuzzy(
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext
|
||||
): ResolvedRef | null {
|
||||
// Try case-insensitive match
|
||||
const allNodes = [
|
||||
...context.getNodesByKind('function'),
|
||||
...context.getNodesByKind('method'),
|
||||
...context.getNodesByKind('class'),
|
||||
];
|
||||
// Build case-insensitive index on first use
|
||||
if (!fuzzyIndex) {
|
||||
fuzzyIndex = new Map();
|
||||
const kinds: Array<Node['kind']> = ['function', 'method', 'class'];
|
||||
for (const kind of kinds) {
|
||||
for (const node of context.getNodesByKind(kind)) {
|
||||
const lower = node.name.toLowerCase();
|
||||
const existing = fuzzyIndex.get(lower);
|
||||
if (existing) {
|
||||
existing.push(node);
|
||||
} else {
|
||||
fuzzyIndex.set(lower, [node]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lowerName = ref.referenceName.toLowerCase();
|
||||
|
||||
// Exact case-insensitive match
|
||||
const caseInsensitive = allNodes.filter(
|
||||
(n) => n.name.toLowerCase() === lowerName
|
||||
);
|
||||
// Exact case-insensitive match via index (O(1) lookup)
|
||||
const caseInsensitive = fuzzyIndex.get(lowerName);
|
||||
|
||||
if (caseInsensitive.length === 1) {
|
||||
if (caseInsensitive && caseInsensitive.length === 1) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: caseInsensitive[0]!.id,
|
||||
@@ -220,20 +238,7 @@ export function matchFuzzy(
|
||||
};
|
||||
}
|
||||
|
||||
// Try prefix match (e.g., "get" matches "getUser")
|
||||
const prefixMatches = allNodes.filter((n) =>
|
||||
n.name.toLowerCase().startsWith(lowerName)
|
||||
);
|
||||
|
||||
if (prefixMatches.length === 1) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: prefixMatches[0]!.id,
|
||||
confidence: 0.3,
|
||||
resolvedBy: 'fuzzy',
|
||||
};
|
||||
}
|
||||
|
||||
// Skip prefix matching — too expensive and low value (confidence 0.3)
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user