Просмотр исходного кода

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

Optimize reference resolution with in-memory caches
Colby Mchenry 4 месяцев назад
Родитель
Сommit
7239a6f7d3

+ 3 - 5
src/resolution/import-resolver.ts

@@ -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
src/resolution/index.ts

@@ -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
src/resolution/name-matcher.ts

@@ -197,39 +197,20 @@ 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) {
-    return {
-      original: ref,
-      targetNodeId: caseInsensitive[0]!.id,
-      confidence: 0.5,
-      resolvedBy: 'fuzzy',
-    };
-  }
+  // Filter to callable kinds only (function, method, class)
+  const callableKinds = new Set(['function', 'method', 'class']);
+  const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind));
 
-  // Try prefix match (e.g., "get" matches "getUser")
-  const prefixMatches = allNodes.filter((n) =>
-    n.name.toLowerCase().startsWith(lowerName)
-  );
-
-  if (prefixMatches.length === 1) {
+  if (callableCandidates.length === 1) {
     return {
       original: ref,
-      targetNodeId: prefixMatches[0]!.id,
-      confidence: 0.3,
+      targetNodeId: callableCandidates[0]!.id,
+      confidence: 0.5,
       resolvedBy: 'fuzzy',
     };
   }

+ 4 - 0
src/resolution/types.ts

@@ -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[];
 }
 
 /**