From d04a911309524f59cefe6879d483e4c2bbdb4f0e Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Fri, 3 Apr 2026 12:30:28 -0500 Subject: [PATCH] feat: Improve Java extraction, multi-symbol aggregation, and impact traversal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Java extraction: - Handle Java method_invocation AST (receiver.method pattern) - Support Java extends_interfaces and super_interfaces with type_list - Create unresolved references for Java imports for cross-file resolution - Extract interface inheritance via extractInheritance MCP tools: - Aggregate callers/callees/impact across ALL matching symbols (e.g. multiple overloads or same-named methods in different classes) - New findAllSymbols() helper for multi-symbol lookup Graph traversal: - Impact analysis now traverses into container children (class → methods) so that callers of methods appear in the impact radius of their class Other: - Add deleteSpecificResolvedReferences() for precise cleanup after resolution - Add 'instance-method' to resolvedBy union type - Version bump to 0.6.8 Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 4 +- package.json | 2 +- src/db/queries.ts | 17 ++++++ src/extraction/tree-sitter.ts | 95 +++++++++++++++++++++--------- src/graph/traversal.ts | 19 ++++++ src/mcp/tools.ts | 108 +++++++++++++++++++++++++++------- src/resolution/types.ts | 2 +- 7 files changed, 195 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index 064a9ff..bd0029f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@colbymchenry/codegraph", - "version": "0.6.6", + "version": "0.6.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@colbymchenry/codegraph", - "version": "0.6.6", + "version": "0.6.8", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8c0df08..36c03fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/db/queries.ts b/src/db/queries.ts index 8d39fd4..001a432 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -926,6 +926,23 @@ export class QueryBuilder { 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 // =========================================================================== diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 220e353..c7c1365 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -1325,10 +1325,14 @@ export class TreeSitterExtractor { let kind: NodeKind = 'interface'; if (this.language === 'rust') kind = 'trait'; - this.createNode(kind, name, node, { + const interfaceNode = this.createNode(kind, name, node, { docstring, isExported, }); + if (!interfaceNode) return; + + // Extract extends (interface inheritance) + this.extractInheritance(node, interfaceNode.id); } /** @@ -1754,6 +1758,19 @@ export class TreeSitterExtractor { 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 } else if (this.language === 'csharp') { // 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 let calleeName = ''; - 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); + // Java/Kotlin method_invocation has 'object' + 'name' fields instead of 'function' + const nameField = getChildByField(node, 'name'); + const objectField = getChildByField(node, 'object'); + + if (nameField && objectField && node.type === 'method_invocation') { + // 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 ( child.type === 'extends_clause' || child.type === 'class_heritage' || - child.type === 'superclass' + child.type === 'superclass' || + child.type === 'extends_interfaces' // Java interface extends ) { - // Extract parent class name - const superclass = child.namedChild(0); - if (superclass) { - const name = getNodeText(superclass, this.source); - this.unresolvedReferences.push({ - fromNodeId: classId, - referenceName: name, - referenceKind: 'extends', - line: child.startPosition.row + 1, - column: child.startPosition.column, - }); + // Extract parent class/interface names + // Java uses type_list wrapper: superclass -> type_identifier, extends_interfaces -> type_list -> type_identifier + const typeList = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_list'); + const targets = typeList ? typeList.namedChildren : [child.namedChild(0)]; + for (const target of targets) { + if (target) { + const name = getNodeText(target, this.source); + this.unresolvedReferences.push({ + fromNodeId: classId, + referenceName: name, + referenceKind: 'extends', + line: target.startPosition.row + 1, + column: target.startPosition.column, + }); + } } } if ( child.type === 'implements_clause' || child.type === 'class_interface_clause' || + child.type === 'super_interfaces' || // Java class implements child.type === 'interfaces' // Dart ) { // Extract implemented interfaces - for (let j = 0; j < child.namedChildCount; j++) { - const iface = child.namedChild(j); + // Java uses type_list wrapper: super_interfaces -> type_list -> type_identifier + 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) { const name = getNodeText(iface, this.source); this.unresolvedReferences.push({ diff --git a/src/graph/traversal.ts b/src/graph/traversal.ts index 0951fca..8bfd4ac 100644 --- a/src/graph/traversal.ts +++ b/src/graph/traversal.ts @@ -483,6 +483,25 @@ export class GraphTraverser { } 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) const incomingEdges = this.queries.getIncomingEdges(nodeId); diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index d490a1b..a0e45d1 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -5,7 +5,7 @@ */ 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 { writeFileSync } from 'fs'; import { clamp } from '../utils'; @@ -475,19 +475,28 @@ export class ToolHandler { const cg = this.getCodeGraph(args.projectPath as string | undefined); const limit = clamp((args.limit as number) || 20, 1, 100); - const match = this.findSymbol(cg, symbol); - if (!match) { + const allMatches = this.findAllSymbols(cg, symbol); + if (allMatches.nodes.length === 0) { return this.textResult(`Symbol "${symbol}" not found in the codebase`); } - const callers = cg.getCallers(match.node.id); - - if (callers.length === 0) { - return this.textResult(`No callers found for "${symbol}"${match.note}`); + // Aggregate callers across all matching symbols + const seen = new Set(); + const allCallers: Node[] = []; + 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); - const formatted = this.formatNodeList(callerNodes, `Callers of ${symbol}`) + match.note; + if (allCallers.length === 0) { + 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)); } @@ -501,19 +510,28 @@ export class ToolHandler { const cg = this.getCodeGraph(args.projectPath as string | undefined); const limit = clamp((args.limit as number) || 20, 1, 100); - const match = this.findSymbol(cg, symbol); - if (!match) { + const allMatches = this.findAllSymbols(cg, symbol); + if (allMatches.nodes.length === 0) { return this.textResult(`Symbol "${symbol}" not found in the codebase`); } - const callees = cg.getCallees(match.node.id); - - if (callees.length === 0) { - return this.textResult(`No callees found for "${symbol}"${match.note}`); + // Aggregate callees across all matching symbols + const seen = new Set(); + const allCallees: Node[] = []; + 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); - const formatted = this.formatNodeList(calleeNodes, `Callees of ${symbol}`) + match.note; + if (allCallees.length === 0) { + 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)); } @@ -527,14 +545,37 @@ export class ToolHandler { const cg = this.getCodeGraph(args.projectPath as string | undefined); const depth = clamp((args.depth as number) || 2, 1, 10); - const match = this.findSymbol(cg, symbol); - if (!match) { + const allMatches = this.findAllSymbols(cg, symbol); + if (allMatches.nodes.length === 0) { 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(); + const mergedEdges: Edge[] = []; + const seenEdges = new Set(); - 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)); } @@ -822,6 +863,31 @@ export class ToolHandler { 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 */ diff --git a/src/resolution/types.ts b/src/resolution/types.ts index cd498db..c28c958 100644 --- a/src/resolution/types.ts +++ b/src/resolution/types.ts @@ -39,7 +39,7 @@ export interface ResolvedRef { /** Confidence score (0-1) */ confidence: number; /** How it was resolved */ - resolvedBy: 'exact-match' | 'import' | 'qualified-name' | 'framework' | 'fuzzy'; + resolvedBy: 'exact-match' | 'import' | 'qualified-name' | 'framework' | 'fuzzy' | 'instance-method'; } /**