Skip to content

Commit 7ed49ba

Browse files
aksOpsclaude
andcommitted
fix: ServiceDetector filesystem walk was traversing entire directory tree
Files.walk() visited every file (potentially millions in large monorepos) before filtering. For a 170-module/44K file project this caused enrich to hang on "Detecting service boundaries". Fix: replaced Files.walk() with Files.walkFileTree() using a visitor that SKIP_SUBTREE on node_modules, .git, target, build, dist, .gradle, __pycache__, venv, vendor, etc. Now only visits directories that could contain build files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4c02514 commit 7ed49ba

1 file changed

Lines changed: 50 additions & 37 deletions

File tree

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

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -319,46 +319,59 @@ public ServiceDetectionResult detect(List<CodeNode> nodes, List<CodeEdge> edges,
319319
* Scan the filesystem recursively for build files that indicate service/module boundaries.
320320
* More reliable than scanning node file paths since not all build files produce CodeNodes.
321321
*/
322+
/** Directories to skip entirely during filesystem walk. */
323+
private static final java.util.Set<String> SKIP_DIRS = java.util.Set.of(
324+
"node_modules", ".git", "target", "build", "dist", ".gradle",
325+
".idea", ".vscode", "__pycache__", ".tox", ".eggs", "venv",
326+
".venv", "vendor", ".bundle", "_build", "deps"
327+
);
328+
322329
private void scanFilesystemForBuildFiles(Path root, Path projectRoot, Map<String, ModuleInfo> modules) {
323-
try (var stream = Files.walk(root, 10)) {
324-
stream.filter(Files::isRegularFile)
325-
.filter(p -> {
326-
String name = p.getFileName().toString();
327-
return BUILD_FILES.containsKey(name)
328-
|| name.endsWith(CSPROJ_EXTENSION) || name.endsWith(FSPROJ_EXTENSION)
329-
|| name.endsWith(VBPROJ_EXTENSION) || name.endsWith(GEMSPEC_EXTENSION)
330-
|| name.endsWith(CABAL_EXTENSION) || name.endsWith(NIMBLE_EXTENSION);
331-
})
332-
.sorted() // deterministic
333-
.forEach(p -> {
334-
String name = p.getFileName().toString();
335-
String relDir = projectRoot.relativize(p.getParent()).toString()
336-
.replace('\\', '/');
337-
if (relDir.equals(".")) relDir = "";
338-
339-
// Skip node_modules, .git, target, build directories
340-
if (relDir.contains("node_modules") || relDir.contains(".git/")
341-
|| relDir.contains("/target/") || relDir.startsWith("target/")
342-
|| relDir.contains("/build/") || relDir.startsWith("build/")) {
343-
return;
344-
}
330+
try {
331+
Files.walkFileTree(root, java.util.EnumSet.noneOf(java.nio.file.FileVisitOption.class),
332+
10, new java.nio.file.SimpleFileVisitor<Path>() {
333+
334+
@Override
335+
public java.nio.file.FileVisitResult preVisitDirectory(Path dir, java.nio.file.attribute.BasicFileAttributes attrs) {
336+
String dirName = dir.getFileName() != null ? dir.getFileName().toString() : "";
337+
if (SKIP_DIRS.contains(dirName)) {
338+
return java.nio.file.FileVisitResult.SKIP_SUBTREE;
339+
}
340+
return java.nio.file.FileVisitResult.CONTINUE;
341+
}
345342

346-
if (name.endsWith(CSPROJ_EXTENSION) || name.endsWith(FSPROJ_EXTENSION)
347-
|| name.endsWith(VBPROJ_EXTENSION)) {
348-
modules.putIfAbsent(relDir, new ModuleInfo(relDir, "dotnet", name));
349-
} else if (name.endsWith(GEMSPEC_EXTENSION)) {
350-
modules.putIfAbsent(relDir, new ModuleInfo(relDir, "ruby", name));
351-
} else if (name.endsWith(CABAL_EXTENSION)) {
352-
modules.putIfAbsent(relDir, new ModuleInfo(relDir, "haskell", name));
353-
} else if (name.endsWith(NIMBLE_EXTENSION)) {
354-
modules.putIfAbsent(relDir, new ModuleInfo(relDir, "nim", name));
355-
} else {
356-
String buildTool = BUILD_FILES.get(name);
357-
if (buildTool != null) {
358-
registerModule(modules, relDir, buildTool, name);
359-
}
343+
@Override
344+
public java.nio.file.FileVisitResult visitFile(Path file, java.nio.file.attribute.BasicFileAttributes attrs) {
345+
String name = file.getFileName().toString();
346+
boolean isBuildFile = BUILD_FILES.containsKey(name)
347+
|| name.endsWith(CSPROJ_EXTENSION) || name.endsWith(FSPROJ_EXTENSION)
348+
|| name.endsWith(VBPROJ_EXTENSION) || name.endsWith(GEMSPEC_EXTENSION)
349+
|| name.endsWith(CABAL_EXTENSION) || name.endsWith(NIMBLE_EXTENSION);
350+
351+
if (!isBuildFile) return java.nio.file.FileVisitResult.CONTINUE;
352+
353+
String relDir = projectRoot.relativize(file.getParent()).toString()
354+
.replace('\\', '/');
355+
if (relDir.equals(".")) relDir = "";
356+
357+
if (name.endsWith(CSPROJ_EXTENSION) || name.endsWith(FSPROJ_EXTENSION)
358+
|| name.endsWith(VBPROJ_EXTENSION)) {
359+
modules.putIfAbsent(relDir, new ModuleInfo(relDir, "dotnet", name));
360+
} else if (name.endsWith(GEMSPEC_EXTENSION)) {
361+
modules.putIfAbsent(relDir, new ModuleInfo(relDir, "ruby", name));
362+
} else if (name.endsWith(CABAL_EXTENSION)) {
363+
modules.putIfAbsent(relDir, new ModuleInfo(relDir, "haskell", name));
364+
} else if (name.endsWith(NIMBLE_EXTENSION)) {
365+
modules.putIfAbsent(relDir, new ModuleInfo(relDir, "nim", name));
366+
} else {
367+
String buildTool = BUILD_FILES.get(name);
368+
if (buildTool != null) {
369+
registerModule(modules, relDir, buildTool, name);
360370
}
361-
});
371+
}
372+
return java.nio.file.FileVisitResult.CONTINUE;
373+
}
374+
});
362375
} catch (IOException e) {
363376
log.warn("Could not scan filesystem for build files: {}", e.getMessage());
364377
}

0 commit comments

Comments
 (0)