Init
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user