@@ -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