Merge pull request #29 from colbymchenry/optimize-reference-resolution
Optimize reference resolution with in-memory caches
This commit is contained in:
@@ -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
@@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user