소스 검색

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>
Colby McHenry 2 달 전
부모
커밋
d04a911309
7개의 변경된 파일194개의 추가작업 그리고 51개의 파일을 삭제
  1. 2 2
      package-lock.json
  2. 1 1
      package.json
  3. 17 0
      src/db/queries.ts
  4. 69 28
      src/extraction/tree-sitter.ts
  5. 19 0
      src/graph/traversal.ts
  6. 85 19
      src/mcp/tools.ts
  7. 1 1
      src/resolution/types.ts

+ 2 - 2
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": {

+ 1 - 1
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",

+ 17 - 0
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
   // ===========================================================================

+ 69 - 28
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({

+ 19 - 0
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);
 

+ 85 - 19
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);
+    // Aggregate callers across all matching symbols
+    const seen = new Set<string>();
+    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);
+        }
+      }
+    }
 
-    if (callers.length === 0) {
-      return this.textResult(`No callers found for "${symbol}"${match.note}`);
+    if (allCallers.length === 0) {
+      return this.textResult(`No callers found for "${symbol}"${allMatches.note}`);
     }
 
-    const callerNodes = callers.slice(0, limit).map(c => c.node);
-    const formatted = this.formatNodeList(callerNodes, `Callers of ${symbol}`) + match.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);
+    // Aggregate callees across all matching symbols
+    const seen = new Set<string>();
+    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);
+        }
+      }
+    }
 
-    if (callees.length === 0) {
-      return this.textResult(`No callees found for "${symbol}"${match.note}`);
+    if (allCallees.length === 0) {
+      return this.textResult(`No callees found for "${symbol}"${allMatches.note}`);
     }
 
-    const calleeNodes = callees.slice(0, limit).map(c => c.node);
-    const formatted = this.formatNodeList(calleeNodes, `Callees of ${symbol}`) + match.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<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));
   }
 
@@ -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
    */

+ 1 - 1
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';
 }
 
 /**