Merge pull request #75 from colbymchenry/fix/python-resolution-and-context-relevance
feat: Improve Java/Python resolution, context relevance, and multi-symbol aggregation
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@colbymchenry/codegraph",
|
"name": "@colbymchenry/codegraph",
|
||||||
"version": "0.6.6",
|
"version": "0.6.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@colbymchenry/codegraph",
|
"name": "@colbymchenry/codegraph",
|
||||||
"version": "0.6.6",
|
"version": "0.6.8",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@colbymchenry/codegraph",
|
"name": "@colbymchenry/codegraph",
|
||||||
"version": "0.6.6",
|
"version": "0.6.8",
|
||||||
"description": "Supercharge Claude Code with semantic code intelligence. 30% fewer tokens, 25% fewer tool calls, 100% local.",
|
"description": "Supercharge Claude Code with semantic code intelligence. 30% fewer tokens, 25% fewer tool calls, 100% local.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { VectorManager } from '../vectors';
|
|||||||
import { formatContextAsMarkdown, formatContextAsJson } from './formatter';
|
import { formatContextAsMarkdown, formatContextAsJson } from './formatter';
|
||||||
import { logDebug, logWarn } from '../errors';
|
import { logDebug, logWarn } from '../errors';
|
||||||
import { validatePathWithinRoot } from '../utils';
|
import { validatePathWithinRoot } from '../utils';
|
||||||
|
import { isTestFile } from '../search/query-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract likely symbol names from a natural language query
|
* Extract likely symbol names from a natural language query
|
||||||
@@ -334,6 +335,17 @@ export class ContextBuilder {
|
|||||||
// Limit total results
|
// Limit total results
|
||||||
searchResults = searchResults.slice(0, opts.searchLimit * 2);
|
searchResults = searchResults.slice(0, opts.searchLimit * 2);
|
||||||
|
|
||||||
|
// Deprioritize test files unless the query is about tests
|
||||||
|
const queryLower = query.toLowerCase();
|
||||||
|
const isTestQuery = queryLower.includes('test') || queryLower.includes('spec');
|
||||||
|
if (!isTestQuery) {
|
||||||
|
searchResults = searchResults.map(r => ({
|
||||||
|
...r,
|
||||||
|
score: isTestFile(r.node.filePath) ? r.score * 0.3 : r.score,
|
||||||
|
}));
|
||||||
|
searchResults.sort((a, b) => b.score - a.score);
|
||||||
|
}
|
||||||
|
|
||||||
// Filter by minimum score
|
// Filter by minimum score
|
||||||
let filteredResults = searchResults.filter((r) => r.score >= opts.minScore);
|
let filteredResults = searchResults.filter((r) => r.score >= opts.minScore);
|
||||||
|
|
||||||
|
|||||||
@@ -926,6 +926,23 @@ export class QueryBuilder {
|
|||||||
this.db.prepare(`DELETE FROM unresolved_refs WHERE from_node_id IN (${placeholders})`).run(...fromNodeIds);
|
this.db.prepare(`DELETE FROM unresolved_refs WHERE from_node_id IN (${placeholders})`).run(...fromNodeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete specific resolved references by (fromNodeId, referenceName, referenceKind) tuples.
|
||||||
|
* More precise than deleteResolvedReferences — only removes refs that were actually resolved.
|
||||||
|
*/
|
||||||
|
deleteSpecificResolvedReferences(refs: Array<{ fromNodeId: string; referenceName: string; referenceKind: string }>): void {
|
||||||
|
if (refs.length === 0) return;
|
||||||
|
const stmt = this.db.prepare(
|
||||||
|
'DELETE FROM unresolved_refs WHERE from_node_id = ? AND reference_name = ? AND reference_kind = ?'
|
||||||
|
);
|
||||||
|
const deleteMany = this.db.transaction((items: typeof refs) => {
|
||||||
|
for (const ref of items) {
|
||||||
|
stmt.run(ref.fromNodeId, ref.referenceName, ref.referenceKind);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
deleteMany(refs);
|
||||||
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Statistics
|
// Statistics
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|||||||
@@ -1325,10 +1325,14 @@ export class TreeSitterExtractor {
|
|||||||
let kind: NodeKind = 'interface';
|
let kind: NodeKind = 'interface';
|
||||||
if (this.language === 'rust') kind = 'trait';
|
if (this.language === 'rust') kind = 'trait';
|
||||||
|
|
||||||
this.createNode(kind, name, node, {
|
const interfaceNode = this.createNode(kind, name, node, {
|
||||||
docstring,
|
docstring,
|
||||||
isExported,
|
isExported,
|
||||||
});
|
});
|
||||||
|
if (!interfaceNode) return;
|
||||||
|
|
||||||
|
// Extract extends (interface inheritance)
|
||||||
|
this.extractInheritance(node, interfaceNode.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1754,6 +1758,19 @@ export class TreeSitterExtractor {
|
|||||||
signature: importText,
|
signature: importText,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Create unresolved reference for import resolution before returning
|
||||||
|
if (moduleName && this.nodeStack.length > 0) {
|
||||||
|
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
||||||
|
if (parentId) {
|
||||||
|
this.unresolvedReferences.push({
|
||||||
|
fromNodeId: parentId,
|
||||||
|
referenceName: moduleName,
|
||||||
|
referenceKind: 'imports',
|
||||||
|
line: node.startPosition.row + 1,
|
||||||
|
column: node.startPosition.column,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return; // Java handled completely above
|
return; // Java handled completely above
|
||||||
} else if (this.language === 'csharp') {
|
} else if (this.language === 'csharp') {
|
||||||
// C# using directives: using System, using System.Collections.Generic, using static X, using Alias = X
|
// C# using directives: using System, using System.Collections.Generic, using static X, using Alias = X
|
||||||
@@ -1960,20 +1977,36 @@ export class TreeSitterExtractor {
|
|||||||
|
|
||||||
// Get the function/method being called
|
// Get the function/method being called
|
||||||
let calleeName = '';
|
let calleeName = '';
|
||||||
const func = getChildByField(node, 'function') || node.namedChild(0);
|
|
||||||
|
|
||||||
if (func) {
|
// Java/Kotlin method_invocation has 'object' + 'name' fields instead of 'function'
|
||||||
if (func.type === 'member_expression' || func.type === 'attribute') {
|
const nameField = getChildByField(node, 'name');
|
||||||
// Method call: obj.method()
|
const objectField = getChildByField(node, 'object');
|
||||||
const property = getChildByField(func, 'property') || func.namedChild(1);
|
|
||||||
if (property) {
|
if (nameField && objectField && node.type === 'method_invocation') {
|
||||||
calleeName = getNodeText(property, this.source);
|
// Java-style method call: receiver.method()
|
||||||
|
const methodName = getNodeText(nameField, this.source);
|
||||||
|
const receiverName = getNodeText(objectField, this.source);
|
||||||
|
|
||||||
|
if (methodName) {
|
||||||
|
// Emit receiver.method form for qualified resolution
|
||||||
|
calleeName = `${receiverName}.${methodName}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const func = getChildByField(node, 'function') || node.namedChild(0);
|
||||||
|
|
||||||
|
if (func) {
|
||||||
|
if (func.type === 'member_expression' || func.type === 'attribute') {
|
||||||
|
// Method call: obj.method()
|
||||||
|
const property = getChildByField(func, 'property') || func.namedChild(1);
|
||||||
|
if (property) {
|
||||||
|
calleeName = getNodeText(property, this.source);
|
||||||
|
}
|
||||||
|
} else if (func.type === 'scoped_identifier' || func.type === 'scoped_call_expression') {
|
||||||
|
// Scoped call: Module::function()
|
||||||
|
calleeName = getNodeText(func, this.source);
|
||||||
|
} else {
|
||||||
|
calleeName = getNodeText(func, this.source);
|
||||||
}
|
}
|
||||||
} else if (func.type === 'scoped_identifier' || func.type === 'scoped_call_expression') {
|
|
||||||
// Scoped call: Module::function()
|
|
||||||
calleeName = getNodeText(func, this.source);
|
|
||||||
} else {
|
|
||||||
calleeName = getNodeText(func, this.source);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2023,30 +2056,38 @@ export class TreeSitterExtractor {
|
|||||||
if (
|
if (
|
||||||
child.type === 'extends_clause' ||
|
child.type === 'extends_clause' ||
|
||||||
child.type === 'class_heritage' ||
|
child.type === 'class_heritage' ||
|
||||||
child.type === 'superclass'
|
child.type === 'superclass' ||
|
||||||
|
child.type === 'extends_interfaces' // Java interface extends
|
||||||
) {
|
) {
|
||||||
// Extract parent class name
|
// Extract parent class/interface names
|
||||||
const superclass = child.namedChild(0);
|
// Java uses type_list wrapper: superclass -> type_identifier, extends_interfaces -> type_list -> type_identifier
|
||||||
if (superclass) {
|
const typeList = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_list');
|
||||||
const name = getNodeText(superclass, this.source);
|
const targets = typeList ? typeList.namedChildren : [child.namedChild(0)];
|
||||||
this.unresolvedReferences.push({
|
for (const target of targets) {
|
||||||
fromNodeId: classId,
|
if (target) {
|
||||||
referenceName: name,
|
const name = getNodeText(target, this.source);
|
||||||
referenceKind: 'extends',
|
this.unresolvedReferences.push({
|
||||||
line: child.startPosition.row + 1,
|
fromNodeId: classId,
|
||||||
column: child.startPosition.column,
|
referenceName: name,
|
||||||
});
|
referenceKind: 'extends',
|
||||||
|
line: target.startPosition.row + 1,
|
||||||
|
column: target.startPosition.column,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
child.type === 'implements_clause' ||
|
child.type === 'implements_clause' ||
|
||||||
child.type === 'class_interface_clause' ||
|
child.type === 'class_interface_clause' ||
|
||||||
|
child.type === 'super_interfaces' || // Java class implements
|
||||||
child.type === 'interfaces' // Dart
|
child.type === 'interfaces' // Dart
|
||||||
) {
|
) {
|
||||||
// Extract implemented interfaces
|
// Extract implemented interfaces
|
||||||
for (let j = 0; j < child.namedChildCount; j++) {
|
// Java uses type_list wrapper: super_interfaces -> type_list -> type_identifier
|
||||||
const iface = child.namedChild(j);
|
const typeList = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_list');
|
||||||
|
const targets = typeList ? typeList.namedChildren : child.namedChildren;
|
||||||
|
for (const iface of targets) {
|
||||||
if (iface) {
|
if (iface) {
|
||||||
const name = getNodeText(iface, this.source);
|
const name = getNodeText(iface, this.source);
|
||||||
this.unresolvedReferences.push({
|
this.unresolvedReferences.push({
|
||||||
|
|||||||
@@ -483,6 +483,25 @@ export class GraphTraverser {
|
|||||||
}
|
}
|
||||||
visited.add(nodeId);
|
visited.add(nodeId);
|
||||||
|
|
||||||
|
// For container nodes (classes, interfaces, structs, etc.), also traverse
|
||||||
|
// into their children so that callers of contained methods appear in impact
|
||||||
|
const focalNode = this.queries.getNodeById(nodeId);
|
||||||
|
if (focalNode) {
|
||||||
|
const containerKinds = new Set(['class', 'interface', 'struct', 'trait', 'protocol', 'module', 'enum']);
|
||||||
|
if (containerKinds.has(focalNode.kind)) {
|
||||||
|
const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
|
||||||
|
for (const edge of containsEdges) {
|
||||||
|
const childNode = this.queries.getNodeById(edge.target);
|
||||||
|
if (childNode && !visited.has(childNode.id)) {
|
||||||
|
nodes.set(childNode.id, childNode);
|
||||||
|
edges.push(edge);
|
||||||
|
// Recurse into children at the same depth (they're part of the same symbol)
|
||||||
|
this.getImpactRecursive(childNode.id, maxDepth, currentDepth, nodes, edges, visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get all incoming edges (things that depend on this node)
|
// Get all incoming edges (things that depend on this node)
|
||||||
const incomingEdges = this.queries.getIncomingEdges(nodeId);
|
const incomingEdges = this.queries.getIncomingEdges(nodeId);
|
||||||
|
|
||||||
|
|||||||
+87
-21
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import CodeGraph, { findNearestCodeGraphRoot } from '../index';
|
import CodeGraph, { findNearestCodeGraphRoot } from '../index';
|
||||||
import type { Node, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
|
import type { Node, Edge, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { writeFileSync } from 'fs';
|
import { writeFileSync } from 'fs';
|
||||||
import { clamp } from '../utils';
|
import { clamp } from '../utils';
|
||||||
@@ -475,19 +475,28 @@ export class ToolHandler {
|
|||||||
const cg = this.getCodeGraph(args.projectPath as string | undefined);
|
const cg = this.getCodeGraph(args.projectPath as string | undefined);
|
||||||
const limit = clamp((args.limit as number) || 20, 1, 100);
|
const limit = clamp((args.limit as number) || 20, 1, 100);
|
||||||
|
|
||||||
const match = this.findSymbol(cg, symbol);
|
const allMatches = this.findAllSymbols(cg, symbol);
|
||||||
if (!match) {
|
if (allMatches.nodes.length === 0) {
|
||||||
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const callers = cg.getCallers(match.node.id);
|
// Aggregate callers across all matching symbols
|
||||||
|
const seen = new Set<string>();
|
||||||
if (callers.length === 0) {
|
const allCallers: Node[] = [];
|
||||||
return this.textResult(`No callers found for "${symbol}"${match.note}`);
|
for (const node of allMatches.nodes) {
|
||||||
|
for (const c of cg.getCallers(node.id)) {
|
||||||
|
if (!seen.has(c.node.id)) {
|
||||||
|
seen.add(c.node.id);
|
||||||
|
allCallers.push(c.node);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const callerNodes = callers.slice(0, limit).map(c => c.node);
|
if (allCallers.length === 0) {
|
||||||
const formatted = this.formatNodeList(callerNodes, `Callers of ${symbol}`) + match.note;
|
return this.textResult(`No callers found for "${symbol}"${allMatches.note}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = this.formatNodeList(allCallers.slice(0, limit), `Callers of ${symbol}`) + allMatches.note;
|
||||||
return this.textResult(this.truncateOutput(formatted));
|
return this.textResult(this.truncateOutput(formatted));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,19 +510,28 @@ export class ToolHandler {
|
|||||||
const cg = this.getCodeGraph(args.projectPath as string | undefined);
|
const cg = this.getCodeGraph(args.projectPath as string | undefined);
|
||||||
const limit = clamp((args.limit as number) || 20, 1, 100);
|
const limit = clamp((args.limit as number) || 20, 1, 100);
|
||||||
|
|
||||||
const match = this.findSymbol(cg, symbol);
|
const allMatches = this.findAllSymbols(cg, symbol);
|
||||||
if (!match) {
|
if (allMatches.nodes.length === 0) {
|
||||||
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const callees = cg.getCallees(match.node.id);
|
// Aggregate callees across all matching symbols
|
||||||
|
const seen = new Set<string>();
|
||||||
if (callees.length === 0) {
|
const allCallees: Node[] = [];
|
||||||
return this.textResult(`No callees found for "${symbol}"${match.note}`);
|
for (const node of allMatches.nodes) {
|
||||||
|
for (const c of cg.getCallees(node.id)) {
|
||||||
|
if (!seen.has(c.node.id)) {
|
||||||
|
seen.add(c.node.id);
|
||||||
|
allCallees.push(c.node);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const calleeNodes = callees.slice(0, limit).map(c => c.node);
|
if (allCallees.length === 0) {
|
||||||
const formatted = this.formatNodeList(calleeNodes, `Callees of ${symbol}`) + match.note;
|
return this.textResult(`No callees found for "${symbol}"${allMatches.note}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = this.formatNodeList(allCallees.slice(0, limit), `Callees of ${symbol}`) + allMatches.note;
|
||||||
return this.textResult(this.truncateOutput(formatted));
|
return this.textResult(this.truncateOutput(formatted));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,14 +545,37 @@ export class ToolHandler {
|
|||||||
const cg = this.getCodeGraph(args.projectPath as string | undefined);
|
const cg = this.getCodeGraph(args.projectPath as string | undefined);
|
||||||
const depth = clamp((args.depth as number) || 2, 1, 10);
|
const depth = clamp((args.depth as number) || 2, 1, 10);
|
||||||
|
|
||||||
const match = this.findSymbol(cg, symbol);
|
const allMatches = this.findAllSymbols(cg, symbol);
|
||||||
if (!match) {
|
if (allMatches.nodes.length === 0) {
|
||||||
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const impact = cg.getImpactRadius(match.node.id, depth);
|
// Aggregate impact across all matching symbols
|
||||||
|
const mergedNodes = new Map<string, Node>();
|
||||||
|
const mergedEdges: Edge[] = [];
|
||||||
|
const seenEdges = new Set<string>();
|
||||||
|
|
||||||
const formatted = this.formatImpact(symbol, impact) + match.note;
|
for (const node of allMatches.nodes) {
|
||||||
|
const impact = cg.getImpactRadius(node.id, depth);
|
||||||
|
for (const [id, n] of impact.nodes) {
|
||||||
|
mergedNodes.set(id, n);
|
||||||
|
}
|
||||||
|
for (const e of impact.edges) {
|
||||||
|
const key = `${e.source}->${e.target}:${e.kind}`;
|
||||||
|
if (!seenEdges.has(key)) {
|
||||||
|
seenEdges.add(key);
|
||||||
|
mergedEdges.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedImpact = {
|
||||||
|
nodes: mergedNodes,
|
||||||
|
edges: mergedEdges,
|
||||||
|
roots: allMatches.nodes.map(n => n.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatted = this.formatImpact(symbol, mergedImpact) + allMatches.note;
|
||||||
return this.textResult(this.truncateOutput(formatted));
|
return this.textResult(this.truncateOutput(formatted));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,6 +863,31 @@ export class ToolHandler {
|
|||||||
return { node: results[0]!.node, note: '' };
|
return { node: results[0]!.node, note: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find ALL symbols matching a name. Used by callers/callees/impact to aggregate
|
||||||
|
* results across all matching symbols (e.g., multiple classes with an `execute` method).
|
||||||
|
*/
|
||||||
|
private findAllSymbols(cg: CodeGraph, symbol: string): { nodes: Node[]; note: string } {
|
||||||
|
const results = cg.searchNodes(symbol, { limit: 50 });
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return { nodes: [], note: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const exactMatches = results.filter(r => r.node.name === symbol);
|
||||||
|
|
||||||
|
if (exactMatches.length <= 1) {
|
||||||
|
const node = exactMatches[0]?.node ?? results[0]!.node;
|
||||||
|
return { nodes: [node], note: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const locations = exactMatches.map(r =>
|
||||||
|
`${r.node.kind} at ${r.node.filePath}:${r.node.startLine}`
|
||||||
|
);
|
||||||
|
const note = `\n\n> **Note:** Aggregated results across ${exactMatches.length} symbols named "${symbol}": ${locations.join(', ')}`;
|
||||||
|
return { nodes: exactMatches.map(r => r.node), note };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate output if it exceeds the maximum length
|
* Truncate output if it exceeds the maximum length
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -375,6 +375,17 @@ export class ReferenceResolver {
|
|||||||
this.queries.insertEdges(edges);
|
this.queries.insertEdges(edges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up resolved refs from unresolved_refs table so metrics are accurate
|
||||||
|
if (result.resolved.length > 0) {
|
||||||
|
this.queries.deleteSpecificResolvedReferences(
|
||||||
|
result.resolved.map((r) => ({
|
||||||
|
fromNodeId: r.original.fromNodeId,
|
||||||
|
referenceName: r.original.referenceName,
|
||||||
|
referenceKind: r.original.referenceKind,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,6 +437,37 @@ export class ReferenceResolver {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Python built-in method calls (e.g., list.extend, dict.update, self.xxx)
|
||||||
|
if (ref.language === 'python') {
|
||||||
|
const dotIdx = name.indexOf('.');
|
||||||
|
if (dotIdx > 0) {
|
||||||
|
const receiver = name.substring(0, dotIdx);
|
||||||
|
// self.method and cls.method are internal calls, not built-in — let them resolve
|
||||||
|
// But receiver types that are built-in types should be filtered
|
||||||
|
const pythonBuiltInTypes = new Set([
|
||||||
|
'list', 'dict', 'set', 'tuple', 'str', 'int', 'float', 'bool',
|
||||||
|
'bytes', 'bytearray', 'frozenset', 'object', 'super',
|
||||||
|
]);
|
||||||
|
if (pythonBuiltInTypes.has(receiver)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also filter bare method names that are common Python built-in methods
|
||||||
|
// These get extracted as unresolved refs when called on arbitrary objects
|
||||||
|
const pythonBuiltInMethods = new Set([
|
||||||
|
'append', 'extend', 'insert', 'remove', 'pop', 'clear', 'sort', 'reverse', 'copy',
|
||||||
|
'update', 'keys', 'values', 'items', 'get',
|
||||||
|
'add', 'discard', 'union', 'intersection', 'difference',
|
||||||
|
'split', 'join', 'strip', 'lstrip', 'rstrip', 'replace', 'lower', 'upper',
|
||||||
|
'startswith', 'endswith', 'find', 'index', 'count', 'encode', 'decode',
|
||||||
|
'format', 'isdigit', 'isalpha', 'isalnum',
|
||||||
|
'read', 'write', 'readline', 'readlines', 'close', 'flush', 'seek',
|
||||||
|
]);
|
||||||
|
if (pythonBuiltInMethods.has(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Pascal/Delphi built-ins and standard library units
|
// Pascal/Delphi built-ins and standard library units
|
||||||
if (ref.language === 'pascal') {
|
if (ref.language === 'pascal') {
|
||||||
// Standard RTL/VCL/FMX unit prefixes — these are external dependencies
|
// Standard RTL/VCL/FMX unit prefixes — these are external dependencies
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ export function matchByExactName(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If only one match, use it
|
// If only one match, use it — but penalize cross-language matches
|
||||||
if (candidates.length === 1) {
|
if (candidates.length === 1) {
|
||||||
|
const isCrossLanguage = candidates[0]!.language !== ref.language;
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
targetNodeId: candidates[0]!.id,
|
targetNodeId: candidates[0]!.id,
|
||||||
confidence: 0.9,
|
confidence: isCrossLanguage ? 0.5 : 0.9,
|
||||||
resolvedBy: 'exact-match',
|
resolvedBy: 'exact-match',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -108,12 +109,14 @@ export function matchMethodCall(
|
|||||||
|
|
||||||
const [, objectOrClass, methodName] = match;
|
const [, objectOrClass, methodName] = match;
|
||||||
|
|
||||||
// Find the class/object first
|
// Strategy 1: Direct class name match (existing logic)
|
||||||
const classCandidates = context.getNodesByName(objectOrClass!);
|
const classCandidates = context.getNodesByName(objectOrClass!);
|
||||||
|
|
||||||
for (const classNode of classCandidates) {
|
for (const classNode of classCandidates) {
|
||||||
if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
|
if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
|
||||||
// Look for method in the same file
|
// Skip cross-language class matches
|
||||||
|
if (classNode.language !== ref.language) continue;
|
||||||
|
|
||||||
const nodesInFile = context.getNodesInFile(classNode.filePath);
|
const nodesInFile = context.getNodesInFile(classNode.filePath);
|
||||||
const methodNode = nodesInFile.find(
|
const methodNode = nodesInFile.find(
|
||||||
(n) =>
|
(n) =>
|
||||||
@@ -133,9 +136,102 @@ export function matchMethodCall(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Instance variable receiver - try capitalized form to find class
|
||||||
|
// e.g., "permissionEngine" → look for classes containing "PermissionEngine"
|
||||||
|
const capitalizedReceiver = objectOrClass!.charAt(0).toUpperCase() + objectOrClass!.slice(1);
|
||||||
|
if (capitalizedReceiver !== objectOrClass) {
|
||||||
|
const fuzzyClassCandidates = context.getNodesByName(capitalizedReceiver);
|
||||||
|
for (const classNode of fuzzyClassCandidates) {
|
||||||
|
if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'interface') {
|
||||||
|
// Skip cross-language class matches
|
||||||
|
if (classNode.language !== ref.language) continue;
|
||||||
|
|
||||||
|
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.8,
|
||||||
|
resolvedBy: 'instance-method',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: Find methods by name across the codebase, match by receiver
|
||||||
|
// name similarity with the containing class. Handles abbreviated variable
|
||||||
|
// names like permissionEngine → PermissionRuleEngine.
|
||||||
|
if (methodName) {
|
||||||
|
const methodCandidates = context.getNodesByName(methodName!);
|
||||||
|
const methods = methodCandidates.filter(
|
||||||
|
(n) => n.kind === 'method' && n.name === methodName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter to same-language candidates first
|
||||||
|
const sameLanguageMethods = methods.filter(m => m.language === ref.language);
|
||||||
|
const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
|
||||||
|
|
||||||
|
// If only one same-language method with this name exists, use it
|
||||||
|
if (targetMethods.length === 1 && targetMethods[0]!.language === ref.language) {
|
||||||
|
return {
|
||||||
|
original: ref,
|
||||||
|
targetNodeId: targetMethods[0]!.id,
|
||||||
|
confidence: 0.7,
|
||||||
|
resolvedBy: 'instance-method',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple methods: score by receiver name word overlap with class name
|
||||||
|
if (targetMethods.length > 1) {
|
||||||
|
const receiverWords = splitCamelCase(objectOrClass!);
|
||||||
|
let bestMatch: typeof targetMethods[0] | undefined;
|
||||||
|
let bestScore = 0;
|
||||||
|
|
||||||
|
for (const method of targetMethods) {
|
||||||
|
const classWords = splitCamelCase(method.qualifiedName);
|
||||||
|
let score = receiverWords.filter(w =>
|
||||||
|
classWords.some(cw => cw.toLowerCase() === w.toLowerCase())
|
||||||
|
).length;
|
||||||
|
// Bonus for same language
|
||||||
|
if (method.language === ref.language) score += 1;
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestMatch = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMatch && bestScore >= 2) {
|
||||||
|
return {
|
||||||
|
original: ref,
|
||||||
|
targetNodeId: bestMatch.id,
|
||||||
|
confidence: 0.65,
|
||||||
|
resolvedBy: 'instance-method',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a camelCase or PascalCase string into words.
|
||||||
|
*/
|
||||||
|
function splitCamelCase(str: string): string[] {
|
||||||
|
return str.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||||
|
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
||||||
|
.split(/[\s._:\/\\]+/)
|
||||||
|
.filter(w => w.length > 1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute directory proximity between two file paths.
|
* Compute directory proximity between two file paths.
|
||||||
* Returns a score based on the number of shared directory segments.
|
* Returns a score based on the number of shared directory segments.
|
||||||
@@ -187,9 +283,11 @@ function findBestMatch(
|
|||||||
// Directory proximity bonus — strongly prefer same module/package
|
// Directory proximity bonus — strongly prefer same module/package
|
||||||
score += computePathProximity(ref.filePath, candidate.filePath);
|
score += computePathProximity(ref.filePath, candidate.filePath);
|
||||||
|
|
||||||
// Same language bonus
|
// Language matching: strongly prefer same language, penalize cross-language
|
||||||
if (candidate.language === ref.language) {
|
if (candidate.language === ref.language) {
|
||||||
score += 50;
|
score += 50;
|
||||||
|
} else {
|
||||||
|
score -= 80;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For call references, prefer functions/methods
|
// For call references, prefer functions/methods
|
||||||
@@ -235,11 +333,16 @@ export function matchFuzzy(
|
|||||||
const callableKinds = new Set(['function', 'method', 'class']);
|
const callableKinds = new Set(['function', 'method', 'class']);
|
||||||
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind));
|
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind));
|
||||||
|
|
||||||
if (callableCandidates.length === 1) {
|
// Prefer same-language matches
|
||||||
|
const sameLanguageCandidates = callableCandidates.filter(n => n.language === ref.language);
|
||||||
|
const finalCandidates = sameLanguageCandidates.length > 0 ? sameLanguageCandidates : callableCandidates;
|
||||||
|
|
||||||
|
if (finalCandidates.length === 1) {
|
||||||
|
const isCrossLanguage = finalCandidates[0]!.language !== ref.language;
|
||||||
return {
|
return {
|
||||||
original: ref,
|
original: ref,
|
||||||
targetNodeId: callableCandidates[0]!.id,
|
targetNodeId: finalCandidates[0]!.id,
|
||||||
confidence: 0.5,
|
confidence: isCrossLanguage ? 0.3 : 0.5,
|
||||||
resolvedBy: 'fuzzy',
|
resolvedBy: 'fuzzy',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export interface ResolvedRef {
|
|||||||
/** Confidence score (0-1) */
|
/** Confidence score (0-1) */
|
||||||
confidence: number;
|
confidence: number;
|
||||||
/** How it was resolved */
|
/** How it was resolved */
|
||||||
resolvedBy: 'exact-match' | 'import' | 'qualified-name' | 'framework' | 'fuzzy';
|
resolvedBy: 'exact-match' | 'import' | 'qualified-name' | 'framework' | 'fuzzy' | 'instance-method';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -79,9 +79,45 @@ export function scorePathRelevance(filePath: string, query: string): number {
|
|||||||
else if (pathLower.includes(term)) score += 3;
|
else if (pathLower.includes(term)) score += 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprioritize test files unless the query is explicitly about tests
|
||||||
|
const queryLower = query.toLowerCase();
|
||||||
|
const isTestQuery = queryLower.includes('test') || queryLower.includes('spec');
|
||||||
|
if (!isTestQuery && isTestFile(filePath)) {
|
||||||
|
score -= 15;
|
||||||
|
}
|
||||||
|
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file path looks like a test file
|
||||||
|
*/
|
||||||
|
export function isTestFile(filePath: string): boolean {
|
||||||
|
const lower = filePath.toLowerCase();
|
||||||
|
const fileName = path.basename(lower);
|
||||||
|
|
||||||
|
// Common test file patterns
|
||||||
|
return (
|
||||||
|
fileName.startsWith('test_') ||
|
||||||
|
fileName.startsWith('test.') ||
|
||||||
|
fileName.endsWith('.test.ts') ||
|
||||||
|
fileName.endsWith('.test.js') ||
|
||||||
|
fileName.endsWith('.test.tsx') ||
|
||||||
|
fileName.endsWith('.test.jsx') ||
|
||||||
|
fileName.endsWith('.spec.ts') ||
|
||||||
|
fileName.endsWith('.spec.js') ||
|
||||||
|
fileName.endsWith('_test.go') ||
|
||||||
|
fileName.endsWith('_test.py') ||
|
||||||
|
fileName.endsWith('_test.rs') ||
|
||||||
|
fileName.endsWith('Tests.java') ||
|
||||||
|
fileName.endsWith('Test.java') ||
|
||||||
|
lower.includes('/tests/') ||
|
||||||
|
lower.includes('/test/') ||
|
||||||
|
lower.includes('/__tests__/') ||
|
||||||
|
lower.includes('/spec/')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kind-based bonus for search ranking
|
* Kind-based bonus for search ranking
|
||||||
* Functions and classes are typically more relevant than variables/imports
|
* Functions and classes are typically more relevant than variables/imports
|
||||||
@@ -91,10 +127,10 @@ export function kindBonus(kind: Node['kind']): number {
|
|||||||
function: 10,
|
function: 10,
|
||||||
method: 10,
|
method: 10,
|
||||||
class: 8,
|
class: 8,
|
||||||
interface: 7,
|
interface: 9,
|
||||||
type_alias: 6,
|
type_alias: 6,
|
||||||
struct: 6,
|
struct: 6,
|
||||||
trait: 6,
|
trait: 9,
|
||||||
enum: 5,
|
enum: 5,
|
||||||
component: 8,
|
component: 8,
|
||||||
route: 9,
|
route: 9,
|
||||||
@@ -108,7 +144,7 @@ export function kindBonus(kind: Node['kind']): number {
|
|||||||
parameter: 0,
|
parameter: 0,
|
||||||
namespace: 4,
|
namespace: 4,
|
||||||
file: 0,
|
file: 0,
|
||||||
protocol: 6,
|
protocol: 9,
|
||||||
enum_member: 3,
|
enum_member: 3,
|
||||||
};
|
};
|
||||||
return bonuses[kind] ?? 0;
|
return bonuses[kind] ?? 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user