The resolving refs phase stalled on large projects (3400+ files, 38k+ nodes) because matchFuzzy loaded ALL functions/methods/classes per ref, import mappings were re-extracted per ref, and fileExists hit disk every call. Add kindCache, lowerNameCache, importMappingCache, and knownFiles set to warmCaches(). Rewrite matchFuzzy to use O(1) lowercase index lookup instead of 3x getNodesByKind scans. Cache import mappings per file. Pre-build file existence set from the index for O(1) fileExists checks.
249 lines
6.0 KiB
TypeScript
249 lines
6.0 KiB
TypeScript
/**
|
|
* Name Matcher
|
|
*
|
|
* Handles symbol name matching for reference resolution.
|
|
*/
|
|
|
|
import { Node } from '../types';
|
|
import { UnresolvedRef, ResolvedRef, ResolutionContext } from './types';
|
|
|
|
/**
|
|
* Try to resolve a reference by exact name match
|
|
*/
|
|
export function matchByExactName(
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext
|
|
): ResolvedRef | null {
|
|
const candidates = context.getNodesByName(ref.referenceName);
|
|
|
|
if (candidates.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// If only one match, use it
|
|
if (candidates.length === 1) {
|
|
return {
|
|
original: ref,
|
|
targetNodeId: candidates[0]!.id,
|
|
confidence: 0.9,
|
|
resolvedBy: 'exact-match',
|
|
};
|
|
}
|
|
|
|
// Multiple matches - try to narrow down
|
|
const bestMatch = findBestMatch(ref, candidates, context);
|
|
if (bestMatch) {
|
|
return {
|
|
original: ref,
|
|
targetNodeId: bestMatch.id,
|
|
confidence: 0.7,
|
|
resolvedBy: 'exact-match',
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Try to resolve by qualified name
|
|
*/
|
|
export function matchByQualifiedName(
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext
|
|
): ResolvedRef | null {
|
|
// Check if the reference name looks qualified (contains :: or .)
|
|
if (!ref.referenceName.includes('::') && !ref.referenceName.includes('.')) {
|
|
return null;
|
|
}
|
|
|
|
const candidates = context.getNodesByQualifiedName(ref.referenceName);
|
|
|
|
if (candidates.length === 1) {
|
|
return {
|
|
original: ref,
|
|
targetNodeId: candidates[0]!.id,
|
|
confidence: 0.95,
|
|
resolvedBy: 'qualified-name',
|
|
};
|
|
}
|
|
|
|
// Try partial qualified name match
|
|
const parts = ref.referenceName.split(/[:.]/);
|
|
const lastName = parts[parts.length - 1];
|
|
if (lastName) {
|
|
const partialCandidates = context.getNodesByName(lastName);
|
|
for (const candidate of partialCandidates) {
|
|
if (candidate.qualifiedName.endsWith(ref.referenceName)) {
|
|
return {
|
|
original: ref,
|
|
targetNodeId: candidate.id,
|
|
confidence: 0.85,
|
|
resolvedBy: 'qualified-name',
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Try to resolve by method name on a class/object
|
|
*/
|
|
export function matchMethodCall(
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext
|
|
): ResolvedRef | null {
|
|
// Parse method call patterns like "obj.method" or "Class::method"
|
|
const dotMatch = ref.referenceName.match(/^(\w+)\.(\w+)$/);
|
|
const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
|
|
|
|
const match = dotMatch || colonMatch;
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const [, objectOrClass, methodName] = match;
|
|
|
|
// Find the class/object first
|
|
const classCandidates = context.getNodesByName(objectOrClass!);
|
|
|
|
for (const classNode of classCandidates) {
|
|
if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
|
|
// Look for method in the same file
|
|
const nodesInFile = context.getNodesInFile(classNode.filePath);
|
|
const methodNode = nodesInFile.find(
|
|
(n) =>
|
|
n.kind === 'method' &&
|
|
n.name === methodName &&
|
|
n.qualifiedName.includes(classNode.name)
|
|
);
|
|
|
|
if (methodNode) {
|
|
return {
|
|
original: ref,
|
|
targetNodeId: methodNode.id,
|
|
confidence: 0.85,
|
|
resolvedBy: 'qualified-name',
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Find the best matching node when there are multiple candidates
|
|
*/
|
|
function findBestMatch(
|
|
ref: UnresolvedRef,
|
|
candidates: Node[],
|
|
_context: ResolutionContext
|
|
): Node | null {
|
|
// Prioritization rules:
|
|
// 1. Same file > different file
|
|
// 2. Same language > different language
|
|
// 3. Functions/methods > classes/types (for call references)
|
|
// 4. Exported > non-exported
|
|
|
|
let bestScore = -1;
|
|
let bestNode: Node | null = null;
|
|
|
|
for (const candidate of candidates) {
|
|
let score = 0;
|
|
|
|
// Same file bonus
|
|
if (candidate.filePath === ref.filePath) {
|
|
score += 100;
|
|
}
|
|
|
|
// Same language bonus
|
|
if (candidate.language === ref.language) {
|
|
score += 50;
|
|
}
|
|
|
|
// For call references, prefer functions/methods
|
|
if (ref.referenceKind === 'calls') {
|
|
if (candidate.kind === 'function' || candidate.kind === 'method') {
|
|
score += 25;
|
|
}
|
|
}
|
|
|
|
// Exported bonus
|
|
if (candidate.isExported) {
|
|
score += 10;
|
|
}
|
|
|
|
// Closer line number (within same file)
|
|
if (candidate.filePath === ref.filePath && candidate.startLine) {
|
|
const distance = Math.abs(candidate.startLine - ref.line);
|
|
score += Math.max(0, 20 - distance / 10);
|
|
}
|
|
|
|
if (score > bestScore) {
|
|
bestScore = score;
|
|
bestNode = candidate;
|
|
}
|
|
}
|
|
|
|
return bestNode;
|
|
}
|
|
|
|
/**
|
|
* Fuzzy match - last resort with lower confidence
|
|
*/
|
|
export function matchFuzzy(
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext
|
|
): ResolvedRef | null {
|
|
const lowerName = ref.referenceName.toLowerCase();
|
|
|
|
// Use pre-built lowercase index for O(1) lookup instead of scanning all nodes
|
|
const candidates = context.getNodesByLowerName(lowerName);
|
|
|
|
// 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: callableCandidates[0]!.id,
|
|
confidence: 0.5,
|
|
resolvedBy: 'fuzzy',
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Match all strategies in order of confidence
|
|
*/
|
|
export function matchReference(
|
|
ref: UnresolvedRef,
|
|
context: ResolutionContext
|
|
): ResolvedRef | null {
|
|
// Try strategies in order of confidence
|
|
let result: ResolvedRef | null;
|
|
|
|
// 1. Qualified name match (highest confidence)
|
|
result = matchByQualifiedName(ref, context);
|
|
if (result) return result;
|
|
|
|
// 2. Method call pattern
|
|
result = matchMethodCall(ref, context);
|
|
if (result) return result;
|
|
|
|
// 3. Exact name match
|
|
result = matchByExactName(ref, context);
|
|
if (result) return result;
|
|
|
|
// 4. Fuzzy match (lowest confidence)
|
|
result = matchFuzzy(ref, context);
|
|
if (result) return result;
|
|
|
|
return null;
|
|
}
|