Files
codegraph/src/resolution/name-matcher.ts
T
Colby McHenry acd9713632 Optimize reference resolution with in-memory caches
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.
2026-02-10 18:14:26 -06:00

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;
}