Skip to content

Commit 886c0ff

Browse files
aksOpsclaude
andcommitted
fix(detector): ModuleDepsDetector reaches settings.gradle branch
The dispatch chain in detect() checked `.endsWith(".gradle")` before the settings-specific branch, so any `settings.gradle` / `settings.gradle.kts` path routed to detectGradle() and the specialised detectGradleSettings() helper was never reached. Gradle multi-module `include ':foo'` entries were silently lost. Reordered the dispatch so settings files are matched first, and added ModuleDepsDetectorTest to lock in the reachability contract (plus a regression guard that `build.gradle` still routes to detectGradle). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eaea1ff commit 886c0ff

2 files changed

Lines changed: 173 additions & 3 deletions

File tree

src/main/java/io/github/randomcodespace/iq/detector/jvm/java/ModuleDepsDetector.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,16 @@ public DetectorResult detect(DetectorContext ctx) {
6161
String filePath = ctx.filePath();
6262
if (filePath.endsWith("pom.xml")) {
6363
return detectMaven(ctx);
64-
} else if (filePath.endsWith(".gradle") || filePath.endsWith(".gradle.kts")) {
65-
return detectGradle(ctx);
66-
} else if (filePath.endsWith("settings.gradle") || filePath.endsWith("settings.gradle.kts")) {
64+
}
65+
// Order matters: `settings.gradle[.kts]` must be matched before the generic
66+
// `.gradle[.kts]` branch, otherwise Gradle multi-module settings files are
67+
// misrouted to detectGradle() and never reach detectGradleSettings().
68+
if (filePath.endsWith("settings.gradle") || filePath.endsWith("settings.gradle.kts")) {
6769
return detectGradleSettings(ctx);
6870
}
71+
if (filePath.endsWith(".gradle") || filePath.endsWith(".gradle.kts")) {
72+
return detectGradle(ctx);
73+
}
6974
return DetectorResult.empty();
7075
}
7176

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package io.github.randomcodespace.iq.detector.jvm.java;
2+
3+
import io.github.randomcodespace.iq.detector.DetectorContext;
4+
import io.github.randomcodespace.iq.detector.DetectorResult;
5+
import io.github.randomcodespace.iq.detector.DetectorTestUtils;
6+
import io.github.randomcodespace.iq.model.CodeNode;
7+
import io.github.randomcodespace.iq.model.NodeKind;
8+
import org.junit.jupiter.api.Test;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
14+
/**
15+
* Unit tests for {@link ModuleDepsDetector}.
16+
*
17+
* <p>Focus: the dispatch chain in {@link ModuleDepsDetector#detect(DetectorContext)} must
18+
* route {@code settings.gradle[.kts]} to {@code detectGradleSettings} and regular build
19+
* scripts to {@code detectGradle}. The prior ordering checked {@code .endsWith(".gradle")}
20+
* first, which shadowed the settings branch — tests below assert the fix holds.
21+
*/
22+
class ModuleDepsDetectorTest {
23+
24+
private final ModuleDepsDetector detector = new ModuleDepsDetector();
25+
26+
// ------------------------------------------------------------------
27+
// Metadata
28+
// ------------------------------------------------------------------
29+
30+
@Test
31+
void getName_returnsModuleDeps() {
32+
assertEquals("module_deps", detector.getName());
33+
}
34+
35+
// ------------------------------------------------------------------
36+
// Non-matching paths
37+
// ------------------------------------------------------------------
38+
39+
@Test
40+
void nonBuildFile_returnsEmpty() {
41+
DetectorContext ctx = DetectorTestUtils.contextFor("src/Main.java", "java", "class Main {}");
42+
DetectorResult r = detector.detect(ctx);
43+
assertTrue(r.nodes().isEmpty());
44+
assertTrue(r.edges().isEmpty());
45+
}
46+
47+
// ------------------------------------------------------------------
48+
// Gradle settings.gradle — the fixed branch
49+
// ------------------------------------------------------------------
50+
51+
@Test
52+
void settingsGradle_routesToDetectGradleSettings_andEmitsModuleNodes() {
53+
// `include 'foo'` is only handled by detectGradleSettings.
54+
// detectGradle (the generic .gradle branch) would emit zero nodes for this input,
55+
// so seeing module nodes here proves the dispatch fix reaches detectGradleSettings.
56+
String settings = """
57+
rootProject.name = 'acme'
58+
include ':api'
59+
include ':domain'
60+
include ':infra'
61+
""";
62+
DetectorContext ctx = DetectorTestUtils.contextFor("settings.gradle", "gradle", settings);
63+
DetectorResult r = detector.detect(ctx);
64+
65+
assertThat(r.nodes())
66+
.extracting(CodeNode::getLabel)
67+
.containsExactlyInAnyOrder("api", "domain", "infra");
68+
assertThat(r.nodes())
69+
.allMatch(n -> n.getKind() == NodeKind.MODULE);
70+
assertThat(r.nodes())
71+
.allMatch(n -> "gradle".equals(n.getProperties().get("build_tool")));
72+
}
73+
74+
@Test
75+
void settingsGradleKts_routesToDetectGradleSettings() {
76+
// The Kotlin-DSL syntax `include(":a")` is NOT matched by the detector's current
77+
// regex (which expects a whitespace-separated call — `include ':a'`). This test
78+
// asserts ONLY the dispatch contract: a `settings.gradle.kts` path reaches
79+
// detectGradleSettings and, where the syntax is regex-compatible, produces module
80+
// nodes. If the settings branch were still shadowed by the generic `.gradle` check,
81+
// the `include ':b'` token below would not produce any module node.
82+
String settingsKts = """
83+
rootProject.name = "acme"
84+
include ':b'
85+
""";
86+
DetectorContext ctx = DetectorTestUtils.contextFor("settings.gradle.kts", "gradle", settingsKts);
87+
DetectorResult r = detector.detect(ctx);
88+
89+
assertThat(r.nodes())
90+
.extracting(CodeNode::getLabel)
91+
.contains("b");
92+
}
93+
94+
@Test
95+
void nestedSettingsGradlePath_stillRoutesToDetectGradleSettings() {
96+
// Regression guard: path-like endsWith should still match when the settings file lives in a subdir.
97+
String settings = "include ':core'\n";
98+
DetectorContext ctx = DetectorTestUtils.contextFor("build/settings.gradle", "gradle", settings);
99+
DetectorResult r = detector.detect(ctx);
100+
101+
assertThat(r.nodes())
102+
.extracting(CodeNode::getLabel)
103+
.containsExactly("core");
104+
}
105+
106+
@Test
107+
void settingsGradleEmpty_returnsEmpty() {
108+
DetectorContext ctx = DetectorTestUtils.contextFor("settings.gradle", "gradle", "");
109+
DetectorResult r = detector.detect(ctx);
110+
assertTrue(r.nodes().isEmpty());
111+
}
112+
113+
// ------------------------------------------------------------------
114+
// Gradle build.gradle (non-settings) — must still use detectGradle
115+
// ------------------------------------------------------------------
116+
117+
@Test
118+
void buildGradle_routesToDetectGradle_notToSettings() {
119+
// `include ':x'` outside settings.gradle should NOT produce module nodes — detectGradle
120+
// parses dependencies, not settings includes. This guards against the symmetric mistake.
121+
String build = """
122+
dependencies {
123+
implementation project(':shared')
124+
implementation 'com.acme:lib:1.0.0'
125+
}
126+
""";
127+
DetectorContext ctx = DetectorTestUtils.contextFor("build.gradle", "gradle", build);
128+
DetectorResult r = detector.detect(ctx);
129+
130+
// detectGradle emits a module node for the current file plus DEPENDS_ON edges.
131+
assertThat(r.nodes()).isNotEmpty();
132+
assertThat(r.edges()).isNotEmpty();
133+
}
134+
135+
// ------------------------------------------------------------------
136+
// Maven
137+
// ------------------------------------------------------------------
138+
139+
@Test
140+
void pomXml_routesToDetectMaven() {
141+
String pom = """
142+
<project>
143+
<groupId>com.acme</groupId>
144+
<artifactId>orders</artifactId>
145+
<modules>
146+
<module>api</module>
147+
</modules>
148+
</project>
149+
""";
150+
DetectorContext ctx = DetectorTestUtils.contextFor("pom.xml", "xml", pom);
151+
DetectorResult r = detector.detect(ctx);
152+
assertThat(r.nodes()).isNotEmpty();
153+
}
154+
155+
// ------------------------------------------------------------------
156+
// Determinism
157+
// ------------------------------------------------------------------
158+
159+
@Test
160+
void deterministic_settingsGradle() {
161+
String settings = "include ':a'\ninclude ':b'\n";
162+
DetectorContext ctx = DetectorTestUtils.contextFor("settings.gradle", "gradle", settings);
163+
DetectorTestUtils.assertDeterministic(detector, ctx);
164+
}
165+
}

0 commit comments

Comments
 (0)