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;
|
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
|
* Resolve a reference using import mappings
|
||||||
*/
|
*/
|
||||||
@@ -432,14 +442,17 @@ export function resolveViaImport(
|
|||||||
ref: UnresolvedRef,
|
ref: UnresolvedRef,
|
||||||
context: ResolutionContext
|
context: ResolutionContext
|
||||||
): ResolvedRef | null {
|
): ResolvedRef | null {
|
||||||
// Read the source file to extract imports
|
// Use cached import mappings or extract and cache them
|
||||||
const content = context.readFile(ref.filePath);
|
let imports = importMappingCache.get(ref.filePath);
|
||||||
if (!content) {
|
if (!imports) {
|
||||||
return null;
|
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
|
// Check if the reference name matches any import
|
||||||
for (const imp of imports) {
|
for (const imp of imports) {
|
||||||
if (imp.localName === ref.referenceName || ref.referenceName.startsWith(imp.localName + '.')) {
|
if (imp.localName === ref.referenceName || ref.referenceName.startsWith(imp.localName + '.')) {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import {
|
|||||||
ResolutionContext,
|
ResolutionContext,
|
||||||
FrameworkResolver,
|
FrameworkResolver,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { matchReference } from './name-matcher';
|
import { matchReference, clearFuzzyIndex } from './name-matcher';
|
||||||
import { resolveViaImport } from './import-resolver';
|
import { resolveViaImport, clearImportMappingCache } from './import-resolver';
|
||||||
import { detectFrameworks } from './frameworks';
|
import { detectFrameworks } from './frameworks';
|
||||||
import { logDebug } from '../errors';
|
import { logDebug } from '../errors';
|
||||||
|
|
||||||
@@ -106,6 +106,8 @@ export class ReferenceResolver {
|
|||||||
this.qualifiedNameCache.clear();
|
this.qualifiedNameCache.clear();
|
||||||
this.kindCache.clear();
|
this.kindCache.clear();
|
||||||
this.nodeByIdCache.clear();
|
this.nodeByIdCache.clear();
|
||||||
|
clearImportMappingCache();
|
||||||
|
clearFuzzyIndex();
|
||||||
this.cachesWarmed = false;
|
this.cachesWarmed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,16 @@ function findBestMatch(
|
|||||||
return bestNode;
|
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
|
* Fuzzy match - last resort with lower confidence
|
||||||
*/
|
*/
|
||||||
@@ -197,21 +207,29 @@ export function matchFuzzy(
|
|||||||
ref: UnresolvedRef,
|
ref: UnresolvedRef,
|
||||||
context: ResolutionContext
|
context: ResolutionContext
|
||||||
): ResolvedRef | null {
|
): ResolvedRef | null {
|
||||||
// Try case-insensitive match
|
// Build case-insensitive index on first use
|
||||||
const allNodes = [
|
if (!fuzzyIndex) {
|
||||||
...context.getNodesByKind('function'),
|
fuzzyIndex = new Map();
|
||||||
...context.getNodesByKind('method'),
|
const kinds: Array<Node['kind']> = ['function', 'method', 'class'];
|
||||||
...context.getNodesByKind('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();
|
const lowerName = ref.referenceName.toLowerCase();
|
||||||
|
|
||||||
// Exact case-insensitive match
|
// Exact case-insensitive match via index (O(1) lookup)
|
||||||
const caseInsensitive = allNodes.filter(
|
const caseInsensitive = fuzzyIndex.get(lowerName);
|
||||||
(n) => n.name.toLowerCase() === lowerName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (caseInsensitive.length === 1) {
|
if (caseInsensitive && caseInsensitive.length === 1) {
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
targetNodeId: caseInsensitive[0]!.id,
|
targetNodeId: caseInsensitive[0]!.id,
|
||||||
@@ -220,20 +238,7 @@ export function matchFuzzy(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try prefix match (e.g., "get" matches "getUser")
|
// Skip prefix matching — too expensive and low value (confidence 0.3)
|
||||||
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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user