Skip to content

Commit 549f135

Browse files
aksOpsclaude
andcommitted
feat: ServiceDetector supports all major build systems across all languages
Added support for 30+ build file types across all ecosystems: Java/JVM: Maven, Gradle (settings.gradle name extraction), Ant (build.xml), SBT (build.sbt), Leiningen (project.clj), Bazel (BUILD/BUILD.bazel) Python: pyproject.toml, setup.py, setup.cfg, Pipfile, requirements.txt, manage.py (Django) JS/TS: package.json, deno.json/jsonc Go: go.mod Rust: Cargo.toml Ruby: Gemfile, *.gemspec PHP: composer.json (name extraction) .NET: *.csproj, *.fsproj, *.vbproj, Directory.Build.props Swift: Package.swift Elixir: mix.exs (app name extraction) Dart/Flutter: pubspec.yaml (name extraction) Haskell: stack.yaml, *.cabal Zig: build.zig OCaml: dune-project Nim: *.nimble R: DESCRIPTION Mono-repo: nx.json, lerna.json, turbo.json, rush.json (supplemental) Docker: Dockerfile, docker-compose.yml/yaml, compose.yml/yaml (supplemental) Supplemental tools (docker, nx, lerna, turbo, rush) never override real build tools. Gradle settings files don't override build files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b020f3a commit 549f135

1 file changed

Lines changed: 130 additions & 14 deletions

File tree

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

Lines changed: 130 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,79 @@ public class ServiceDetector {
4848
* Maps filename to build tool name.
4949
*/
5050
private static final Map<String, String> BUILD_FILES = Map.ofEntries(
51+
// Java/JVM
5152
Map.entry("pom.xml", "maven"),
52-
Map.entry("package.json", "npm"),
53-
Map.entry("go.mod", "go"),
5453
Map.entry("build.gradle", "gradle"),
5554
Map.entry("build.gradle.kts", "gradle"),
55+
Map.entry("settings.gradle", "gradle"),
56+
Map.entry("settings.gradle.kts", "gradle"),
57+
Map.entry("build.xml", "ant"),
58+
Map.entry("build.sbt", "sbt"),
59+
Map.entry("project.clj", "leiningen"),
60+
// JavaScript / TypeScript
61+
Map.entry("package.json", "npm"),
62+
Map.entry("deno.json", "deno"),
63+
Map.entry("deno.jsonc", "deno"),
64+
// Go
65+
Map.entry("go.mod", "go"),
66+
// Rust
5667
Map.entry("Cargo.toml", "cargo"),
57-
Map.entry("requirements.txt", "python"),
58-
Map.entry("setup.py", "python"),
68+
// Python
5969
Map.entry("pyproject.toml", "python"),
70+
Map.entry("setup.py", "python"),
71+
Map.entry("setup.cfg", "python"),
72+
Map.entry("Pipfile", "python"),
73+
Map.entry("requirements.txt", "python"),
6074
Map.entry("manage.py", "django"),
61-
Map.entry("Dockerfile", "docker")
75+
// Ruby
76+
Map.entry("Gemfile", "ruby"),
77+
// PHP
78+
Map.entry("composer.json", "php"),
79+
// .NET (csproj handled by suffix match below)
80+
Map.entry("Directory.Build.props", "dotnet"),
81+
// Swift
82+
Map.entry("Package.swift", "swift"),
83+
// Elixir
84+
Map.entry("mix.exs", "elixir"),
85+
// Dart / Flutter
86+
Map.entry("pubspec.yaml", "dart"),
87+
// Scala (also build.sbt above)
88+
// Haskell
89+
Map.entry("stack.yaml", "haskell"),
90+
// Zig
91+
Map.entry("build.zig", "zig"),
92+
// OCaml
93+
Map.entry("dune-project", "ocaml"),
94+
// R
95+
Map.entry("DESCRIPTION", "r"),
96+
// Bazel
97+
Map.entry("BUILD", "bazel"),
98+
Map.entry("BUILD.bazel", "bazel"),
99+
// Mono-repo orchestrators (supplemental, like Docker)
100+
Map.entry("nx.json", "nx"),
101+
Map.entry("lerna.json", "lerna"),
102+
Map.entry("turbo.json", "turbo"),
103+
Map.entry("rush.json", "rush"),
104+
// Docker (supplemental -- doesn't override other tools)
105+
Map.entry("Dockerfile", "docker"),
106+
Map.entry("docker-compose.yml", "docker"),
107+
Map.entry("docker-compose.yaml", "docker"),
108+
Map.entry("compose.yml", "docker"),
109+
Map.entry("compose.yaml", "docker")
62110
);
63111

64-
/** File extension for .csproj files (matched by suffix). */
112+
/** File extensions matched by suffix (not exact name). */
65113
private static final String CSPROJ_EXTENSION = ".csproj";
114+
private static final String FSPROJ_EXTENSION = ".fsproj";
115+
private static final String VBPROJ_EXTENSION = ".vbproj";
116+
private static final String GEMSPEC_EXTENSION = ".gemspec";
117+
private static final String CABAL_EXTENSION = ".cabal";
118+
private static final String NIMBLE_EXTENSION = ".nimble";
119+
120+
/** Build tools that are supplemental (don't override real build tools). */
121+
private static final java.util.Set<String> SUPPLEMENTAL_TOOLS = java.util.Set.of(
122+
"docker", "nx", "lerna", "turbo", "rush"
123+
);
66124

67125
/** Python build files ranked by priority (first match wins for a directory). */
68126
private static final List<String> PYTHON_BUILD_FILES = List.of(
@@ -82,6 +140,16 @@ public class ServiceDetector {
82140
"^name\\s*=\\s*\"([^\"]+)\"", Pattern.MULTILINE);
83141
private static final Pattern SETUP_PY_NAME = Pattern.compile(
84142
"name\\s*=\\s*['\"]([^'\"]+)['\"]");
143+
private static final Pattern GRADLE_SETTINGS_NAME = Pattern.compile(
144+
"rootProject\\.name\\s*=\\s*['\"]([^'\"]+)['\"]");
145+
private static final Pattern SBT_NAME = Pattern.compile(
146+
"name\\s*:=\\s*\"([^\"]+)\"");
147+
private static final Pattern COMPOSER_NAME = Pattern.compile(
148+
"\"name\"\\s*:\\s*\"([^\"]+)\"");
149+
private static final Pattern MIX_APP_NAME = Pattern.compile(
150+
"app:\\s*:([\\w]+)");
151+
private static final Pattern PUBSPEC_NAME = Pattern.compile(
152+
"^name:\\s*(\\S+)", Pattern.MULTILINE);
85153

86154
/**
87155
* Detect service boundaries from the graph's nodes and create SERVICE nodes.
@@ -129,8 +197,15 @@ public ServiceDetectionResult detect(List<CodeNode> nodes, List<CodeEdge> edges,
129197
if (buildTool != null) {
130198
registerModule(modules, dirPath, buildTool, fileName);
131199
}
132-
if (fileName.endsWith(CSPROJ_EXTENSION)) {
200+
if (fileName.endsWith(CSPROJ_EXTENSION) || fileName.endsWith(FSPROJ_EXTENSION)
201+
|| fileName.endsWith(VBPROJ_EXTENSION)) {
133202
modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "dotnet", fileName));
203+
} else if (fileName.endsWith(GEMSPEC_EXTENSION)) {
204+
modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "ruby", fileName));
205+
} else if (fileName.endsWith(CABAL_EXTENSION)) {
206+
modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "haskell", fileName));
207+
} else if (fileName.endsWith(NIMBLE_EXTENSION)) {
208+
modules.putIfAbsent(dirPath, new ModuleInfo(dirPath, "nim", fileName));
134209
}
135210
}
136211

@@ -249,7 +324,10 @@ private void scanFilesystemForBuildFiles(Path root, Path projectRoot, Map<String
249324
stream.filter(Files::isRegularFile)
250325
.filter(p -> {
251326
String name = p.getFileName().toString();
252-
return BUILD_FILES.containsKey(name) || name.endsWith(CSPROJ_EXTENSION);
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);
253331
})
254332
.sorted() // deterministic
255333
.forEach(p -> {
@@ -265,8 +343,15 @@ private void scanFilesystemForBuildFiles(Path root, Path projectRoot, Map<String
265343
return;
266344
}
267345

268-
if (name.endsWith(CSPROJ_EXTENSION)) {
346+
if (name.endsWith(CSPROJ_EXTENSION) || name.endsWith(FSPROJ_EXTENSION)
347+
|| name.endsWith(VBPROJ_EXTENSION)) {
269348
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));
270355
} else {
271356
String buildTool = BUILD_FILES.get(name);
272357
if (buildTool != null) {
@@ -285,12 +370,12 @@ private void scanFilesystemForBuildFiles(Path root, Path projectRoot, Map<String
285370
private void registerModule(Map<String, ModuleInfo> modules, String dirPath,
286371
String buildTool, String fileName) {
287372
ModuleInfo existing = modules.get(dirPath);
288-
// Python doesn't override non-Python
289-
if (existing != null && isPythonTool(buildTool) && !isPythonTool(existing.buildTool())) {
373+
// Supplemental tools (docker, nx, lerna, turbo, rush) don't override real build tools
374+
if (SUPPLEMENTAL_TOOLS.contains(buildTool) && existing != null) {
290375
return;
291376
}
292-
// Docker doesn't override anything
293-
if ("docker".equals(buildTool) && existing != null) {
377+
// Python doesn't override non-Python
378+
if (existing != null && isPythonTool(buildTool) && !isPythonTool(existing.buildTool())) {
294379
return;
295380
}
296381
// Python priority: pyproject.toml > setup.py > requirements.txt > manage.py
@@ -299,6 +384,11 @@ private void registerModule(Map<String, ModuleInfo> modules, String dirPath,
299384
return;
300385
}
301386
}
387+
// Gradle settings files don't override gradle build files
388+
if ("gradle".equals(buildTool) && existing != null && "gradle".equals(existing.buildTool())
389+
&& fileName.startsWith("settings.")) {
390+
return;
391+
}
302392
modules.put(dirPath, new ModuleInfo(dirPath, buildTool, fileName));
303393
}
304394

@@ -333,11 +423,16 @@ private String readNameFromBuildFile(Path projectRoot, String dir, ModuleInfo in
333423
String content = Files.readString(buildFile, StandardCharsets.UTF_8);
334424
return switch (info.buildTool()) {
335425
case "maven" -> extractFromPom(content);
426+
case "gradle" -> extractFromGradleSettings(content, info.buildFile());
336427
case "npm" -> extractFromPackageJson(content);
337428
case "go" -> extractFromGoMod(content);
338429
case "cargo" -> extractFromCargoToml(content);
339430
case "python" -> extractFromPythonBuild(content, info.buildFile());
340-
case "django" -> null; // manage.py doesn't contain the name
431+
case "sbt" -> extractWithPattern(content, SBT_NAME);
432+
case "php" -> extractWithPattern(content, COMPOSER_NAME);
433+
case "elixir" -> extractWithPattern(content, MIX_APP_NAME);
434+
case "dart" -> extractWithPattern(content, PUBSPEC_NAME);
435+
case "django" -> null;
341436
default -> null;
342437
};
343438
} catch (IOException e) {
@@ -387,6 +482,27 @@ private String extractFromCargoToml(String content) {
387482
return m.find() ? m.group(1).trim() : null;
388483
}
389484

485+
private String extractFromGradleSettings(String content, String buildFile) {
486+
if (buildFile.startsWith("settings.")) {
487+
Matcher m = GRADLE_SETTINGS_NAME.matcher(content);
488+
return m.find() ? m.group(1).trim() : null;
489+
}
490+
return null; // build.gradle doesn't typically have the project name
491+
}
492+
493+
private String extractWithPattern(String content, Pattern pattern) {
494+
Matcher m = pattern.matcher(content);
495+
if (m.find()) {
496+
String name = m.group(1).trim();
497+
// Strip scope prefix for PHP composer (vendor/name -> name)
498+
if (name.contains("/")) {
499+
name = name.substring(name.lastIndexOf('/') + 1);
500+
}
501+
return name;
502+
}
503+
return null;
504+
}
505+
390506
private String extractFromPythonBuild(String content, String fileName) {
391507
if ("pyproject.toml".equals(fileName)) {
392508
Matcher m = PYPROJECT_NAME.matcher(content);

0 commit comments

Comments
 (0)