Skip to content

Commit b020f3a

Browse files
aksOpsclaude
andcommitted
fix: ServiceDetector now scans filesystem for build files, not just node paths
Root cause: ServiceDetector only found services where a CodeNode existed with a build file path. In multi-module Maven projects, many sub-module pom.xml files don't produce CodeNodes (XML detector may skip them or they exceed size limits), making those modules invisible. Fix: Added scanFilesystemForBuildFiles() that walks the project directory tree (depth 10) looking for pom.xml, package.json, go.mod, Cargo.toml, etc. directly. Skips node_modules, .git, target, build directories. Falls back to node-based scanning as secondary source. For a 170-module project, this should detect all modules that have build files, not just the 8 that happened to have nodes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f179c3a commit b020f3a

1 file changed

Lines changed: 72 additions & 33 deletions

File tree

src/main/java/io/github/randomcodespace/iq/analyzer/ServiceDetector.java

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -107,56 +107,33 @@ public ServiceDetectionResult detect(List<CodeNode> nodes, List<CodeEdge> edges,
107107
*/
108108
public ServiceDetectionResult detect(List<CodeNode> nodes, List<CodeEdge> edges,
109109
String projectDir, Path projectRoot) {
110-
// 1. Find module boundaries by scanning node file paths for build files
111-
// Use TreeMap for deterministic ordering (sorted by directory path)
110+
// 1. Find module boundaries by scanning the filesystem for build files.
111+
// This is more reliable than scanning node file paths, which may miss
112+
// modules where no detector created a node from the build file.
112113
Map<String, ModuleInfo> modules = new TreeMap<>();
113114

115+
if (projectRoot != null) {
116+
// Scan filesystem directly for build files (most reliable)
117+
scanFilesystemForBuildFiles(projectRoot, projectRoot, modules);
118+
}
119+
120+
// Fallback: also scan node file paths in case filesystem scan missed anything
114121
for (CodeNode node : nodes) {
115122
String filePath = node.getFilePath();
116123
if (filePath == null) continue;
117124

118125
String fileName = Path.of(filePath).getFileName().toString();
119126
String dirPath = parentDir(filePath);
120127

121-
// Check known build files
122128
String buildTool = BUILD_FILES.get(fileName);
123129
if (buildTool != null) {
124-
// For Python: only register if no better build tool already present
125-
ModuleInfo existing = modules.get(dirPath);
126-
if (existing != null && isPythonTool(buildTool) && !isPythonTool(existing.buildTool())) {
127-
continue; // Don't override a non-Python build tool with Python
128-
}
129-
// For Docker: only register if no other build tool present
130-
if ("docker".equals(buildTool) && existing != null) {
131-
// Add docker as supplemental info but don't override
132-
continue;
133-
}
134-
// For Python files: prefer higher-priority ones
135-
if (isPythonTool(buildTool) && existing != null && isPythonTool(existing.buildTool())) {
136-
if (pythonPriority(fileName) >= pythonPriority(existing.buildFile())) {
137-
continue; // Current is same or lower priority
138-
}
139-
}
140-
modules.put(dirPath, new ModuleInfo(dirPath, buildTool, fileName));
130+
registerModule(modules, dirPath, buildTool, fileName);
141131
}
142-
// Check .csproj files
143132
if (fileName.endsWith(CSPROJ_EXTENSION)) {
144133
modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "dotnet", fileName));
145134
}
146135
}
147136

148-
// 1b. Check for Dockerfile as supplemental indicator -- create service
149-
// only if no other build file was found for that directory
150-
for (CodeNode node : nodes) {
151-
String filePath = node.getFilePath();
152-
if (filePath == null) continue;
153-
String fileName = Path.of(filePath).getFileName().toString();
154-
if ("Dockerfile".equals(fileName)) {
155-
String dirPath = parentDir(filePath);
156-
modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "docker", fileName));
157-
}
158-
}
159-
160137
// 2. If no modules detected, create one service for the whole project
161138
if (modules.isEmpty()) {
162139
modules.put("", new ModuleInfo("", "unknown", ""));
@@ -263,6 +240,68 @@ public ServiceDetectionResult detect(List<CodeNode> nodes, List<CodeEdge> edges,
263240
return new ServiceDetectionResult(serviceNodes, serviceEdges);
264241
}
265242

243+
/**
244+
* Scan the filesystem recursively for build files that indicate service/module boundaries.
245+
* More reliable than scanning node file paths since not all build files produce CodeNodes.
246+
*/
247+
private void scanFilesystemForBuildFiles(Path root, Path projectRoot, Map<String, ModuleInfo> modules) {
248+
try (var stream = Files.walk(root, 10)) {
249+
stream.filter(Files::isRegularFile)
250+
.filter(p -> {
251+
String name = p.getFileName().toString();
252+
return BUILD_FILES.containsKey(name) || name.endsWith(CSPROJ_EXTENSION);
253+
})
254+
.sorted() // deterministic
255+
.forEach(p -> {
256+
String name = p.getFileName().toString();
257+
String relDir = projectRoot.relativize(p.getParent()).toString()
258+
.replace('\\', '/');
259+
if (relDir.equals(".")) relDir = "";
260+
261+
// Skip node_modules, .git, target, build directories
262+
if (relDir.contains("node_modules") || relDir.contains(".git/")
263+
|| relDir.contains("/target/") || relDir.startsWith("target/")
264+
|| relDir.contains("/build/") || relDir.startsWith("build/")) {
265+
return;
266+
}
267+
268+
if (name.endsWith(CSPROJ_EXTENSION)) {
269+
modules.putIfAbsent(relDir, new ModuleInfo(relDir, "dotnet", name));
270+
} else {
271+
String buildTool = BUILD_FILES.get(name);
272+
if (buildTool != null) {
273+
registerModule(modules, relDir, buildTool, name);
274+
}
275+
}
276+
});
277+
} catch (IOException e) {
278+
log.warn("Could not scan filesystem for build files: {}", e.getMessage());
279+
}
280+
}
281+
282+
/**
283+
* Register a module, respecting priority rules for Python/Docker.
284+
*/
285+
private void registerModule(Map<String, ModuleInfo> modules, String dirPath,
286+
String buildTool, String fileName) {
287+
ModuleInfo existing = modules.get(dirPath);
288+
// Python doesn't override non-Python
289+
if (existing != null && isPythonTool(buildTool) && !isPythonTool(existing.buildTool())) {
290+
return;
291+
}
292+
// Docker doesn't override anything
293+
if ("docker".equals(buildTool) && existing != null) {
294+
return;
295+
}
296+
// Python priority: pyproject.toml > setup.py > requirements.txt > manage.py
297+
if (isPythonTool(buildTool) && existing != null && isPythonTool(existing.buildTool())) {
298+
if (pythonPriority(fileName) >= pythonPriority(existing.buildFile())) {
299+
return;
300+
}
301+
}
302+
modules.put(dirPath, new ModuleInfo(dirPath, buildTool, fileName));
303+
}
304+
266305
/**
267306
* Extract service name from build file contents if possible, otherwise use directory name.
268307
*/

0 commit comments

Comments
 (0)