Merge origin/main into delphi-support
Integrate main branch changes (WASM grammar architecture, centralized resolution caches, SQLite adapter) with delphi-support branch. Pascal grammar is now built as WASM and shipped in src/extraction/wasm/ for consistency with the WASM-based grammar loading approach. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,7 @@ function resolveRelativeImport(
|
||||
|
||||
// Try the path as-is first
|
||||
const basePath = path.resolve(fromDir, importPath);
|
||||
const relativePath = path.relative(projectRoot, basePath);
|
||||
const relativePath = path.relative(projectRoot, basePath).replace(/\\/g, '/');
|
||||
|
||||
// Try each extension
|
||||
for (const ext of extensions) {
|
||||
@@ -442,15 +442,10 @@ export function resolveViaImport(
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext
|
||||
): ResolvedRef | 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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Check if the reference name matches any import
|
||||
|
||||
+55
-4
@@ -15,9 +15,10 @@ import {
|
||||
ResolutionResult,
|
||||
ResolutionContext,
|
||||
FrameworkResolver,
|
||||
ImportMapping,
|
||||
} from './types';
|
||||
import { matchReference, clearFuzzyIndex } from './name-matcher';
|
||||
import { resolveViaImport, clearImportMappingCache } from './import-resolver';
|
||||
import { matchReference } from './name-matcher';
|
||||
import { resolveViaImport, extractImportMappings } from './import-resolver';
|
||||
import { detectFrameworks } from './frameworks';
|
||||
import { logDebug } from '../errors';
|
||||
|
||||
@@ -40,6 +41,9 @@ export class ReferenceResolver {
|
||||
private qualifiedNameCache: Map<string, Node[]> = new Map();
|
||||
private kindCache: Map<string, Node[]> = new Map();
|
||||
private nodeByIdCache: 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) {
|
||||
@@ -91,8 +95,20 @@ export class ReferenceResolver {
|
||||
|
||||
// Index by ID
|
||||
this.nodeByIdCache.set(node.id, 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;
|
||||
}
|
||||
|
||||
@@ -106,8 +122,9 @@ export class ReferenceResolver {
|
||||
this.qualifiedNameCache.clear();
|
||||
this.kindCache.clear();
|
||||
this.nodeByIdCache.clear();
|
||||
clearImportMappingCache();
|
||||
clearFuzzyIndex();
|
||||
this.lowerNameCache.clear();
|
||||
this.importMappingCache.clear();
|
||||
this.knownFiles = null;
|
||||
this.cachesWarmed = false;
|
||||
}
|
||||
|
||||
@@ -150,6 +167,14 @@ export class ReferenceResolver {
|
||||
},
|
||||
|
||||
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);
|
||||
@@ -183,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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -190,16 +190,6 @@ 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
|
||||
*/
|
||||
@@ -207,38 +197,24 @@ export function matchFuzzy(
|
||||
ref: UnresolvedRef,
|
||||
context: ResolutionContext
|
||||
): ResolvedRef | null {
|
||||
// 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 via index (O(1) lookup)
|
||||
const caseInsensitive = fuzzyIndex.get(lowerName);
|
||||
// Use pre-built lowercase index for O(1) lookup instead of scanning all nodes
|
||||
const candidates = context.getNodesByLowerName(lowerName);
|
||||
|
||||
if (caseInsensitive && 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',
|
||||
};
|
||||
}
|
||||
|
||||
// Skip prefix matching — too expensive and low value (confidence 0.3)
|
||||
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