Skip to content

Commit d04a911

Browse files
colbymchenryclaude
andcommitted
feat: Improve Java extraction, multi-symbol aggregation, and impact traversal
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) <noreply@anthropic.com>
1 parent 8b541be commit d04a911

7 files changed

Lines changed: 194 additions & 51 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@colbymchenry/codegraph",
3-
"version": "0.6.6",
3+
"version": "0.6.8",
44
"description": "Supercharge Claude Code with semantic code intelligence. 30% fewer tokens, 25% fewer tool calls, 100% local.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/db/queries.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,23 @@ export class QueryBuilder {
926926
this.db.prepare(`DELETE FROM unresolved_refs WHERE from_node_id IN (${placeholders})`).run(...fromNodeIds);
927927
}
928928

929+
/**
930+
* Delete specific resolved references by (fromNodeId, referenceName, referenceKind) tuples.
931+
* More precise than deleteResolvedReferences — only removes refs that were actually resolved.
932+
*/
933+
deleteSpecificResolvedReferences(refs: Array<{ fromNodeId: string; referenceName: string; referenceKind: string }>): void {
934+
if (refs.length === 0) return;
935+
const stmt = this.db.prepare(
936+
'DELETE FROM unresolved_refs WHERE from_node_id = ? AND reference_name = ? AND reference_kind = ?'
937+
);
938+
const deleteMany = this.db.transaction((items: typeof refs) => {
939+
for (const ref of items) {
940+
stmt.run(ref.fromNodeId, ref.referenceName, ref.referenceKind);
941+
}
942+
});
943+
deleteMany(refs);
944+
}
945+
929946
// ===========================================================================
930947
// Statistics
931948
// ===========================================================================

src/extraction/tree-sitter.ts

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,10 +1325,14 @@ export class TreeSitterExtractor {
13251325
let kind: NodeKind = 'interface';
13261326
if (this.language === 'rust') kind = 'trait';
13271327

1328-
this.createNode(kind, name, node, {
1328+
const interfaceNode = this.createNode(kind, name, node, {
13291329
docstring,
13301330
isExported,
13311331
});
1332+
if (!interfaceNode) return;
1333+
1334+
// Extract extends (interface inheritance)
1335+
this.extractInheritance(node, interfaceNode.id);
13321336
}
13331337

13341338
/**
@@ -1754,6 +1758,19 @@ export class TreeSitterExtractor {
17541758
signature: importText,
17551759
});
17561760
}
1761+
// Create unresolved reference for import resolution before returning
1762+
if (moduleName && this.nodeStack.length > 0) {
1763+
const parentId = this.nodeStack[this.nodeStack.length - 1];
1764+
if (parentId) {
1765+
this.unresolvedReferences.push({
1766+
fromNodeId: parentId,
1767+
referenceName: moduleName,
1768+
referenceKind: 'imports',
1769+
line: node.startPosition.row + 1,
1770+
column: node.startPosition.column,
1771+
});
1772+
}
1773+
}
17571774
return; // Java handled completely above
17581775
} else if (this.language === 'csharp') {
17591776
// C# using directives: using System, using System.Collections.Generic, using static X, using Alias = X
@@ -1960,20 +1977,36 @@ export class TreeSitterExtractor {
19601977

19611978
// Get the function/method being called
19621979
let calleeName = '';
1963-
const func = getChildByField(node, 'function') || node.namedChild(0);
1964-
1965-
if (func) {
1966-
if (func.type === 'member_expression' || func.type === 'attribute') {
1967-
// Method call: obj.method()
1968-
const property = getChildByField(func, 'property') || func.namedChild(1);
1969-
if (property) {
1970-
calleeName = getNodeText(property, this.source);
1980+
1981+
// Java/Kotlin method_invocation has 'object' + 'name' fields instead of 'function'
1982+
const nameField = getChildByField(node, 'name');
1983+
const objectField = getChildByField(node, 'object');
1984+
1985+
if (nameField && objectField && node.type === 'method_invocation') {
1986+
// Java-style method call: receiver.method()
1987+
const methodName = getNodeText(nameField, this.source);
1988+
const receiverName = getNodeText(objectField, this.source);
1989+
1990+
if (methodName) {
1991+
// Emit receiver.method form for qualified resolution
1992+
calleeName = `${receiverName}.${methodName}`;
1993+
}
1994+
} else {
1995+
const func = getChildByField(node, 'function') || node.namedChild(0);
1996+
1997+
if (func) {
1998+
if (func.type === 'member_expression' || func.type === 'attribute') {
1999+
// Method call: obj.method()
2000+
const property = getChildByField(func, 'property') || func.namedChild(1);
2001+
if (property) {
2002+
calleeName = getNodeText(property, this.source);
2003+
}
2004+
} else if (func.type === 'scoped_identifier' || func.type === 'scoped_call_expression') {
2005+
// Scoped call: Module::function()
2006+
calleeName = getNodeText(func, this.source);
2007+
} else {
2008+
calleeName = getNodeText(func, this.source);
19712009
}
1972-
} else if (func.type === 'scoped_identifier' || func.type === 'scoped_call_expression') {
1973-
// Scoped call: Module::function()
1974-
calleeName = getNodeText(func, this.source);
1975-
} else {
1976-
calleeName = getNodeText(func, this.source);
19772010
}
19782011
}
19792012

@@ -2023,30 +2056,38 @@ export class TreeSitterExtractor {
20232056
if (
20242057
child.type === 'extends_clause' ||
20252058
child.type === 'class_heritage' ||
2026-
child.type === 'superclass'
2059+
child.type === 'superclass' ||
2060+
child.type === 'extends_interfaces' // Java interface extends
20272061
) {
2028-
// Extract parent class name
2029-
const superclass = child.namedChild(0);
2030-
if (superclass) {
2031-
const name = getNodeText(superclass, this.source);
2032-
this.unresolvedReferences.push({
2033-
fromNodeId: classId,
2034-
referenceName: name,
2035-
referenceKind: 'extends',
2036-
line: child.startPosition.row + 1,
2037-
column: child.startPosition.column,
2038-
});
2062+
// Extract parent class/interface names
2063+
// Java uses type_list wrapper: superclass -> type_identifier, extends_interfaces -> type_list -> type_identifier
2064+
const typeList = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_list');
2065+
const targets = typeList ? typeList.namedChildren : [child.namedChild(0)];
2066+
for (const target of targets) {
2067+
if (target) {
2068+
const name = getNodeText(target, this.source);
2069+
this.unresolvedReferences.push({
2070+
fromNodeId: classId,
2071+
referenceName: name,
2072+
referenceKind: 'extends',
2073+
line: target.startPosition.row + 1,
2074+
column: target.startPosition.column,
2075+
});
2076+
}
20392077
}
20402078
}
20412079

20422080
if (
20432081
child.type === 'implements_clause' ||
20442082
child.type === 'class_interface_clause' ||
2083+
child.type === 'super_interfaces' || // Java class implements
20452084
child.type === 'interfaces' // Dart
20462085
) {
20472086
// Extract implemented interfaces
2048-
for (let j = 0; j < child.namedChildCount; j++) {
2049-
const iface = child.namedChild(j);
2087+
// Java uses type_list wrapper: super_interfaces -> type_list -> type_identifier
2088+
const typeList = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_list');
2089+
const targets = typeList ? typeList.namedChildren : child.namedChildren;
2090+
for (const iface of targets) {
20502091
if (iface) {
20512092
const name = getNodeText(iface, this.source);
20522093
this.unresolvedReferences.push({

src/graph/traversal.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,25 @@ export class GraphTraverser {
483483
}
484484
visited.add(nodeId);
485485

486+
// For container nodes (classes, interfaces, structs, etc.), also traverse
487+
// into their children so that callers of contained methods appear in impact
488+
const focalNode = this.queries.getNodeById(nodeId);
489+
if (focalNode) {
490+
const containerKinds = new Set(['class', 'interface', 'struct', 'trait', 'protocol', 'module', 'enum']);
491+
if (containerKinds.has(focalNode.kind)) {
492+
const containsEdges = this.queries.getOutgoingEdges(nodeId, ['contains']);
493+
for (const edge of containsEdges) {
494+
const childNode = this.queries.getNodeById(edge.target);
495+
if (childNode && !visited.has(childNode.id)) {
496+
nodes.set(childNode.id, childNode);
497+
edges.push(edge);
498+
// Recurse into children at the same depth (they're part of the same symbol)
499+
this.getImpactRecursive(childNode.id, maxDepth, currentDepth, nodes, edges, visited);
500+
}
501+
}
502+
}
503+
}
504+
486505
// Get all incoming edges (things that depend on this node)
487506
const incomingEdges = this.queries.getIncomingEdges(nodeId);
488507

src/mcp/tools.ts

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import CodeGraph, { findNearestCodeGraphRoot } from '../index';
8-
import type { Node, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
8+
import type { Node, Edge, SearchResult, Subgraph, TaskContext, NodeKind } from '../types';
99
import { createHash } from 'crypto';
1010
import { writeFileSync } from 'fs';
1111
import { clamp } from '../utils';
@@ -475,19 +475,28 @@ export class ToolHandler {
475475
const cg = this.getCodeGraph(args.projectPath as string | undefined);
476476
const limit = clamp((args.limit as number) || 20, 1, 100);
477477

478-
const match = this.findSymbol(cg, symbol);
479-
if (!match) {
478+
const allMatches = this.findAllSymbols(cg, symbol);
479+
if (allMatches.nodes.length === 0) {
480480
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
481481
}
482482

483-
const callers = cg.getCallers(match.node.id);
483+
// Aggregate callers across all matching symbols
484+
const seen = new Set<string>();
485+
const allCallers: Node[] = [];
486+
for (const node of allMatches.nodes) {
487+
for (const c of cg.getCallers(node.id)) {
488+
if (!seen.has(c.node.id)) {
489+
seen.add(c.node.id);
490+
allCallers.push(c.node);
491+
}
492+
}
493+
}
484494

485-
if (callers.length === 0) {
486-
return this.textResult(`No callers found for "${symbol}"${match.note}`);
495+
if (allCallers.length === 0) {
496+
return this.textResult(`No callers found for "${symbol}"${allMatches.note}`);
487497
}
488498

489-
const callerNodes = callers.slice(0, limit).map(c => c.node);
490-
const formatted = this.formatNodeList(callerNodes, `Callers of ${symbol}`) + match.note;
499+
const formatted = this.formatNodeList(allCallers.slice(0, limit), `Callers of ${symbol}`) + allMatches.note;
491500
return this.textResult(this.truncateOutput(formatted));
492501
}
493502

@@ -501,19 +510,28 @@ export class ToolHandler {
501510
const cg = this.getCodeGraph(args.projectPath as string | undefined);
502511
const limit = clamp((args.limit as number) || 20, 1, 100);
503512

504-
const match = this.findSymbol(cg, symbol);
505-
if (!match) {
513+
const allMatches = this.findAllSymbols(cg, symbol);
514+
if (allMatches.nodes.length === 0) {
506515
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
507516
}
508517

509-
const callees = cg.getCallees(match.node.id);
518+
// Aggregate callees across all matching symbols
519+
const seen = new Set<string>();
520+
const allCallees: Node[] = [];
521+
for (const node of allMatches.nodes) {
522+
for (const c of cg.getCallees(node.id)) {
523+
if (!seen.has(c.node.id)) {
524+
seen.add(c.node.id);
525+
allCallees.push(c.node);
526+
}
527+
}
528+
}
510529

511-
if (callees.length === 0) {
512-
return this.textResult(`No callees found for "${symbol}"${match.note}`);
530+
if (allCallees.length === 0) {
531+
return this.textResult(`No callees found for "${symbol}"${allMatches.note}`);
513532
}
514533

515-
const calleeNodes = callees.slice(0, limit).map(c => c.node);
516-
const formatted = this.formatNodeList(calleeNodes, `Callees of ${symbol}`) + match.note;
534+
const formatted = this.formatNodeList(allCallees.slice(0, limit), `Callees of ${symbol}`) + allMatches.note;
517535
return this.textResult(this.truncateOutput(formatted));
518536
}
519537

@@ -527,14 +545,37 @@ export class ToolHandler {
527545
const cg = this.getCodeGraph(args.projectPath as string | undefined);
528546
const depth = clamp((args.depth as number) || 2, 1, 10);
529547

530-
const match = this.findSymbol(cg, symbol);
531-
if (!match) {
548+
const allMatches = this.findAllSymbols(cg, symbol);
549+
if (allMatches.nodes.length === 0) {
532550
return this.textResult(`Symbol "${symbol}" not found in the codebase`);
533551
}
534552

535-
const impact = cg.getImpactRadius(match.node.id, depth);
553+
// Aggregate impact across all matching symbols
554+
const mergedNodes = new Map<string, Node>();
555+
const mergedEdges: Edge[] = [];
556+
const seenEdges = new Set<string>();
536557

537-
const formatted = this.formatImpact(symbol, impact) + match.note;
558+
for (const node of allMatches.nodes) {
559+
const impact = cg.getImpactRadius(node.id, depth);
560+
for (const [id, n] of impact.nodes) {
561+
mergedNodes.set(id, n);
562+
}
563+
for (const e of impact.edges) {
564+
const key = `${e.source}->${e.target}:${e.kind}`;
565+
if (!seenEdges.has(key)) {
566+
seenEdges.add(key);
567+
mergedEdges.push(e);
568+
}
569+
}
570+
}
571+
572+
const mergedImpact = {
573+
nodes: mergedNodes,
574+
edges: mergedEdges,
575+
roots: allMatches.nodes.map(n => n.id),
576+
};
577+
578+
const formatted = this.formatImpact(symbol, mergedImpact) + allMatches.note;
538579
return this.textResult(this.truncateOutput(formatted));
539580
}
540581

@@ -822,6 +863,31 @@ export class ToolHandler {
822863
return { node: results[0]!.node, note: '' };
823864
}
824865

866+
/**
867+
* Find ALL symbols matching a name. Used by callers/callees/impact to aggregate
868+
* results across all matching symbols (e.g., multiple classes with an `execute` method).
869+
*/
870+
private findAllSymbols(cg: CodeGraph, symbol: string): { nodes: Node[]; note: string } {
871+
const results = cg.searchNodes(symbol, { limit: 50 });
872+
873+
if (results.length === 0) {
874+
return { nodes: [], note: '' };
875+
}
876+
877+
const exactMatches = results.filter(r => r.node.name === symbol);
878+
879+
if (exactMatches.length <= 1) {
880+
const node = exactMatches[0]?.node ?? results[0]!.node;
881+
return { nodes: [node], note: '' };
882+
}
883+
884+
const locations = exactMatches.map(r =>
885+
`${r.node.kind} at ${r.node.filePath}:${r.node.startLine}`
886+
);
887+
const note = `\n\n> **Note:** Aggregated results across ${exactMatches.length} symbols named "${symbol}": ${locations.join(', ')}`;
888+
return { nodes: exactMatches.map(r => r.node), note };
889+
}
890+
825891
/**
826892
* Truncate output if it exceeds the maximum length
827893
*/

src/resolution/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export interface ResolvedRef {
3939
/** Confidence score (0-1) */
4040
confidence: number;
4141
/** How it was resolved */
42-
resolvedBy: 'exact-match' | 'import' | 'qualified-name' | 'framework' | 'fuzzy';
42+
resolvedBy: 'exact-match' | 'import' | 'qualified-name' | 'framework' | 'fuzzy' | 'instance-method';
4343
}
4444

4545
/**

0 commit comments

Comments
 (0)