From 5a734eb301e9f9f53724be0da6818afa6927758f Mon Sep 17 00:00:00 2001 From: "jason.ma" Date: Thu, 9 Apr 2026 20:23:03 +0800 Subject: [PATCH] fix: resolve JVM imports in multi-module Maven/Gradle projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-module resolution (src/main/java/…) already worked. Multi-module layouts like: module-a/sub/src/main/java/com/example/Foo.java were silently unresolved because the three fixed src-dir prefixes never matched. The dependency graph therefore always produced 0 edges for any Java/Kotlin/Scala project that follows the standard Maven/Gradle multi-module layout. Fix: introduce `buildJvmSuffixMap()` which scans `fileSet` once (O(n)) and registers every JVM source file by its class-path key (i.e. everything after src/main//). `resolveImport` accepts the map as an optional last argument and falls back to it when the existing prefix-based approach yields no result. - `buildJvmSuffixMap` exported for reuse / testing. - Map is built once per `buildCodeGraph` call, only when the project contains at least one JVM file — zero cost for other language projects. - Lookup is O(1) per import, replacing a worst-case O(n) linear scan on every miss. - Works on both Windows (backslash) and POSIX (forward-slash) because the map key is built with `path.sep`. - 7 new unit tests covering: map construction, test-source exclusion, multi-module Java resolution, Kotlin resolution, unresolvable class, stdlib guard. Fixes: enterprise Java/Spring Boot codebases (30+ Maven modules) reporting 0 edges in codebase_graph_stats. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/services/code-graph.ts | 14 +++- src/services/graph-resolution.ts | 64 +++++++++++++- tests/unit/graph-resolution.test.ts | 126 +++++++++++++++++++++++++++- 3 files changed, 199 insertions(+), 5 deletions(-) diff --git a/src/services/code-graph.ts b/src/services/code-graph.ts index eb13397..9887bff 100644 --- a/src/services/code-graph.ts +++ b/src/services/code-graph.ts @@ -10,7 +10,7 @@ import { EXTRA_EXTENSIONS, getLanguageFromExtension, MAX_GRAPH_FILE_BYTES } from import type { CodeGraph, CodeGraphEdge, CodeGraphNode } from "../types.js"; import { loadPathAliases } from "./graph-aliases.js"; import { extractImports } from "./graph-imports.js"; -import { resolveImport } from "./graph-resolution.js"; +import { buildJvmSuffixMap, resolveImport } from "./graph-resolution.js"; import { createIgnoreFilter, shouldIgnore } from "./ignore.js"; import { logger } from "./logger.js"; import { deleteGraphData, getGraphMetadata, loadGraphData, saveGraphData } from "./qdrant.js"; @@ -390,6 +390,16 @@ export async function buildCodeGraph( const nodesMap = new Map(); const edges: CodeGraphEdge[] = []; + // Build a suffix lookup map for JVM multi-module projects (Java/Kotlin/Scala). + // This resolves FQNs like com.example.Foo when the class lives under a nested + // src/main/java/ tree (e.g. module-a/sub/src/main/java/com/example/Foo.java). + // Cost: O(n) once here, O(1) per import lookup — negligible vs. full AST parse. + const hasJvm = files.some((f) => { + const e = path.extname(f).toLowerCase(); + return e === ".java" || e === ".kt" || e === ".kts" || e === ".scala"; + }); + const jvmSuffixMap = hasJvm ? buildJvmSuffixMap(fileSet) : undefined; + for (const relPath of files) { const ext = path.extname(relPath).toLowerCase(); const lang = getAstGrepLang(ext); @@ -448,7 +458,7 @@ export async function buildCodeGraph( // Try to resolve to a project file // CSS imports from