This commit is contained in:
Colby McHenry
2026-01-18 16:25:00 -06:00
parent 08ccabb5a9
commit cc6e7a5c89
57 changed files with 23315 additions and 1 deletions
+267
View File
@@ -0,0 +1,267 @@
/**
* 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 {
// 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
);
if (caseInsensitive.length === 1) {
return {
original: ref,
targetNodeId: caseInsensitive[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;
}
/**
* 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;
}