Merge pull request #29 from colbymchenry/optimize-reference-resolution

Optimize reference resolution with in-memory caches
This commit is contained in:
Colby Mchenry
2026-02-10 18:15:36 -06:00
committed by GitHub
4 changed files with 82 additions and 33 deletions
+3 -5
View File
@@ -432,14 +432,12 @@ export function resolveViaImport(
ref: UnresolvedRef,
context: ResolutionContext
): ResolvedRef | null {
// Read the source file to extract imports
const content = context.readFile(ref.filePath);
if (!content) {
// Use cached import mappings (avoids re-reading and re-parsing per ref)
const imports = context.getImportMappings(ref.filePath, ref.language);
if (imports.length === 0 && !context.readFile(ref.filePath)) {
return null;
}
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 + '.')) {
+67 -1
View File
@@ -15,9 +15,10 @@ import {
ResolutionResult,
ResolutionContext,
FrameworkResolver,
ImportMapping,
} from './types';
import { matchReference } from './name-matcher';
import { resolveViaImport } from './import-resolver';
import { resolveViaImport, extractImportMappings } from './import-resolver';
import { detectFrameworks } from './frameworks';
import { logDebug } from '../errors';
@@ -39,6 +40,10 @@ export class ReferenceResolver {
private nameCache: Map<string, Node[]> = new Map();
private qualifiedNameCache: Map<string, Node[]> = new Map();
private nodeByIdCache: Map<string, Node> = new Map();
private kindCache: Map<string, Node[]> = new Map();
private lowerNameCache: Map<string, Node[]> = new Map();
private importMappingCache: Map<string, ImportMapping[]> = new Map();
private knownFiles: Set<string> | null = null;
private cachesWarmed = false;
constructor(projectRoot: string, queries: QueryBuilder) {
@@ -82,8 +87,28 @@ export class ReferenceResolver {
// Index by ID
this.nodeByIdCache.set(node.id, node);
// Index by kind
const byKind = this.kindCache.get(node.kind);
if (byKind) {
byKind.push(node);
} else {
this.kindCache.set(node.kind, [node]);
}
// Index by lowercase name (for fuzzy matching)
const lowerName = node.name.toLowerCase();
const byLower = this.lowerNameCache.get(lowerName);
if (byLower) {
byLower.push(node);
} else {
this.lowerNameCache.set(lowerName, [node]);
}
}
// Pre-build known files set from index
this.knownFiles = new Set(this.queries.getAllFiles().map((f) => f.path));
this.cachesWarmed = true;
}
@@ -96,6 +121,10 @@ export class ReferenceResolver {
this.nameCache.clear();
this.qualifiedNameCache.clear();
this.nodeByIdCache.clear();
this.kindCache.clear();
this.lowerNameCache.clear();
this.importMappingCache.clear();
this.knownFiles = null;
this.cachesWarmed = false;
}
@@ -131,10 +160,21 @@ export class ReferenceResolver {
},
getNodesByKind: (kind: Node['kind']) => {
if (this.cachesWarmed) {
return this.kindCache.get(kind) ?? [];
}
return this.queries.getNodesByKind(kind);
},
fileExists: (filePath: string) => {
// Check pre-built known files set first (O(1))
if (this.knownFiles) {
const normalized = filePath.replace(/\\/g, '/');
if (this.knownFiles.has(filePath) || this.knownFiles.has(normalized)) {
return true;
}
}
// Fall back to filesystem for files not yet indexed
const fullPath = path.join(this.projectRoot, filePath);
try {
return fs.existsSync(fullPath);
@@ -168,6 +208,32 @@ export class ReferenceResolver {
getAllFiles: () => {
return this.queries.getAllFiles().map((f) => f.path);
},
getNodesByLowerName: (lowerName: string) => {
if (this.cachesWarmed) {
return this.lowerNameCache.get(lowerName) ?? [];
}
// Fallback: scan all nodes (expensive, but only used if cache not warm)
return this.queries.getAllNodes().filter(
(n) => n.name.toLowerCase() === lowerName
);
},
getImportMappings: (filePath: string, language) => {
const cacheKey = filePath;
const cached = this.importMappingCache.get(cacheKey);
if (cached) return cached;
const content = this.context.readFile(filePath);
if (!content) {
this.importMappingCache.set(cacheKey, []);
return [];
}
const mappings = extractImportMappings(filePath, content, language);
this.importMappingCache.set(cacheKey, mappings);
return mappings;
},
};
}
+8 -27
View File
@@ -197,43 +197,24 @@ export function matchFuzzy(
ref: UnresolvedRef,
context: ResolutionContext
): ResolvedRef | null {
// Try case-insensitive match
const allNodes = [
...context.getNodesByKind('function'),
...context.getNodesByKind('method'),
...context.getNodesByKind('class'),
];
const lowerName = ref.referenceName.toLowerCase();
// Exact case-insensitive match
const caseInsensitive = allNodes.filter(
(n) => n.name.toLowerCase() === lowerName
);
// Use pre-built lowercase index for O(1) lookup instead of scanning all nodes
const candidates = context.getNodesByLowerName(lowerName);
if (caseInsensitive.length === 1) {
// Filter to callable kinds only (function, method, class)
const callableKinds = new Set(['function', 'method', 'class']);
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind));
if (callableCandidates.length === 1) {
return {
original: ref,
targetNodeId: caseInsensitive[0]!.id,
targetNodeId: callableCandidates[0]!.id,
confidence: 0.5,
resolvedBy: 'fuzzy',
};
}
// 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',
};
}
return null;
}
+4
View File
@@ -79,6 +79,10 @@ export interface ResolutionContext {
getProjectRoot(): string;
/** Get all files */
getAllFiles(): string[];
/** Get nodes by lowercase name (O(1) lookup for fuzzy matching) */
getNodesByLowerName(lowerName: string): Node[];
/** Get cached import mappings for a file */
getImportMappings(filePath: string, language: Language): ImportMapping[];
}
/**