Skip to content

Commit decb8db

Browse files
sambsnydtimtebeekTeamModernesteve-aom-elliottJenson3210
authored
Include in Dependency Report the path to the build file declaring the dependencies being reported on (#189)
* Include in Dependency Report the path to the build file declaring the dependencies being reported on * OpenRewrite recipe best practices Use this link to re-run the recipe: https://app.moderne.io/recipes/org.openrewrite.recipes.rewrite.OpenRewriteRecipeBestPractices?organizationId=QUxML01vZGVybmUvTW9kZXJuZSArIE9wZW5SZXdyaXRl Co-authored-by: Moderne <team@moderne.io> * Pin empty MavenSettings in DependencyResolutionDiagnosticTest.maven() (#178) openrewrite/gh-automation#95 installs ~/.m2/settings.xml with `<mirrorOf>*</mirrorOf>` to route Maven through `artifactory.moderne.ninja` and avoid HTTP 429 from Maven Central. The mirror rewrites every repository request — including the test pom's deliberately-unreachable `<repository><url>https://nonexistent.moderne.io/maven2</url></repository>` — so the diagnostic recipe only sees the mirror and the local file cache. Neither Maven Central nor the test repo appear in the data table, and both assertions fail. Inject an explicit empty `MavenSettings` via `MavenExecutionContextView` (same pattern as `mavenSettingsWithMirrors`) so the recipe ignores `~/.m2/settings.xml` and sees the pom-declared repositories directly. The original three assertions are restored unchanged. * Speed up ModuleHasDependency and RepositoryHasDependency by removing DependencyInsight (#176) * Speed up ModuleHasDependency and RepositoryHasDependency by removing DependencyInsight Both recipes ran a fresh DependencyInsight visitor against every source file during scanning, which performs exhaustive Gradle + Maven dependency analysis just to answer a binary yes/no question about whether the module contains a given GAV. Replace the visitor with a direct check against MavenResolutionResult.findDependencies for Maven modules, and against GradleProject's configurations + ResolvedDependency.findDependency for Gradle modules. This mirrors the pattern Sam applied to the single-build-system versions of ModuleHasDependency in openrewrite/rewrite (commit 919c9f5 / PR #6664). These recipes are used as preconditions in many declarative migration recipes. Avoiding the DependencyInsight allocation + traversal per source file substantially reduces scanner time on repositories with many Java files. * Skip dependency scan in ModuleHasDependency once project is known to match Avoids re-running the per-tree dependency lookup for every source file in a module after the JavaProject has already been added to the accumulator. --------- Co-authored-by: Tim te Beek <tim@moderne.io> * Match requested dependencies in ModuleHasDependency and RepositoryHasDependency (#179) * Regenerate recipes.csv (#180) * Update Gradle wrapper 9.5.1 Use this link to re-run the recipe: https://app.moderne.io/recipes/org.openrewrite.gradle.UpdateGradleWrapper?organizationId=QUxML01vZGVybmUgKyBPcGVuUmV3cml0ZS9PcGVuUmV3cml0ZQ%3D%3D#defaults=W3sidmFsdWUiOiI5LjUuMSIsIm5hbWUiOiJ2ZXJzaW9uIn0seyJ2YWx1ZSI6ImJpbiIsIm5hbWUiOiJkaXN0cmlidXRpb24ifSx7InZhbHVlIjoiYmFmYzE0MWI2MTlhZDYzNTBmZDk3NWZjOTAzMTU2ZGQ1YzE1MTk5OGNjOGIwNThlOGMxMDQ0YWI1ZjdiMDMxZiIsIm5hbWUiOiJkaXN0cmlidXRpb25DaGVja3N1bSJ9XQ== Co-authored-by: Moderne <team@moderne.io> * Trust resolved version over declared dep in dependency search (#181) * Trust resolved version over declared dep in dependency search PR #179 extended ModuleHasDependency / RepositoryHasDependency to also match against requested/declared dependencies so they still match when resolution fails. This introduced false positives whenever a version range is supplied: the declared version string could satisfy the comparator even when the resolved version did not (e.g. a Gradle resolutionStrategy.force or platform alignment overriding the declared coordinate). - When a coordinate is present in the resolved dependencies, trust the resolved check and skip the declared-dependency fallback for that group:artifact. Only fall back for declared deps not already resolved. - Tighten versionMatches so a null declared version no longer auto-matches when a version constraint is supplied (same treatment as a ${...} property reference). Adds regression tests covering both rules in ModuleHasDependencyTest and RepositoryHasDependencyTest. * Apply suggestions from code review Co-authored-by: Jente Sondervorst <jentesondervorst@gmail.com> * Add Maven regression tests for BOM-managed dependency version range --------- Co-authored-by: Jente Sondervorst <jentesondervorst@gmail.com> * git-ignore .context/ Use this link to re-run the recipe: https://app.moderne.io/recipes/org.openrewrite.AddToGitignore?organizationId=QUxML01vZGVybmUgKyBPcGVuUmV3cml0ZQ%3D%3D#defaults=W3sidmFsdWUiOiIuY29udGV4dC8iLCJuYW1lIjoiZW50cmllcyJ9XQ== Co-authored-by: Moderne <team@moderne.io> * Fix RemoveDependency.unlessUsing for multi-module Maven projects (#183) * Fix `RemoveDependency.unlessUsing` for multi-module Maven projects `RemoveDependency` scans every source file and records whether `unlessUsing` matched against the file's `JavaProject` marker. In a multi-module Maven project the parent pom carries its own `JavaProject` distinct from the children, so the parent module's accumulator entry never reflects type usages found in child modules. The visitor would then strip the `<dependency>` from the parent pom even though child modules in the same reactor still relied on inheriting it. Now the scanner additionally records the `MavenResolutionResult.id -> JavaProject` mapping for every pom it visits. When the visitor is about to remove a `<dependency>` from a pom, it walks that pom's descendant modules via `MavenResolutionResult.getModules()` and preserves the declaration if any descendant's `JavaProject` shows the type in use. * Allow `RemoveDependency` to remove ancestor decl when descendants self-declare The descendant-aware preservation introduced in the previous commit was intentionally conservative: any descendant module whose Java sources used the `unlessUsing` type blocked removal of the dep from an ancestor pom. That over-preserves when a descendant module also declares the same dependency directly in its own `<dependencies>` — in that case the descendant is self-sufficient and the ancestor's declaration is dead weight. Refine the check: a descendant only blocks removal if it uses the type AND does not declare the dependency itself. The self-declaration check looks at the raw `requested` pom (as-written), not the resolved/inherited dependency list, so a child that inherits the dep from the ancestor we might modify still correctly blocks removal. * Remove out-of-date OWASP suppressions Use this link to re-run the recipe: https://app.moderne.io/recipes/org.openrewrite.xml.security.RemoveOwaspSuppressions?organizationId=QUxML01vZGVybmUgKyBPcGVuUmV3cml0ZQ%3D%3D#defaults=W3sidmFsdWUiOiIyMDI2LTA2LTE3IiwibmFtZSI6ImN1dE9mZkRhdGUifV0= Co-authored-by: Moderne <team@moderne.io> * Use GitHub actions/checkout@v7 Use this link to re-run the recipe: https://app.moderne.io/recipes/org.openrewrite.github.ChangeActionVersion?organizationId=QUxML01vZGVybmUgKyBPcGVuUmV3cml0ZQ%3D%3D#defaults=W3sidmFsdWUiOiJhY3Rpb25zL2NoZWNrb3V0IiwibmFtZSI6ImFjdGlvbiJ9LHsidmFsdWUiOiJ2NyIsIm5hbWUiOiJ2ZXJzaW9uIn1d Co-authored-by: Moderne <team@moderne.io> * Cache delegate recipes per recipe instance to avoid repeated construction (#184) The Gradle/Maven dependency wrapper recipes (`AddDependency`, `ChangeDependency`, `UpgradeDependencyVersion`, `UpgradeTransitiveDependencyVersion`) each delegate to a Gradle and a Maven `ScanningRecipe`. Previously a fresh delegate instance was constructed on every `getInitialValue`/`getScanner`/`getVisitor`/`validate` call (and, in `AddDependency`, once per source file inside the scanner's `visit`). Each `ScanningRecipe` construction runs `UUID.randomUUID()` for its accumulator-message key, which draws from `SecureRandom` and dominated profiles of recipe-heavy runs. Memoize each delegate lazily on the wrapper recipe instance (a `final transient AtomicReference` that is excluded from the generated constructor and from `equals`/`hashCode`) so it is built at most once per wrapper instance and reused across the scanning and editing phases. The delegate is derived from the wrapper's options, so it belongs to the recipe instance rather than the accumulator: callers such as `rewrite-spring` deliberately reuse a single accumulator across two wrapper instances that have different options, so caching the delegate on the accumulator would reuse the wrong options. The `Accumulator` types are left unchanged, preserving their public constructors for downstream callers that build them directly. * Add software.amazon.ion:ion-java -> com.amazon.ion:ion-java relocation (stopgap) (#185) * [Auto] Old GroupId migrations as of 2026-06-23T1723 * Update findRelocatedIonJava test for new relocation context (#186) The [Auto] Old GroupId migrations update (5253cf5) added a context note to the software.amazon.ion:ion-java relocation in migrations.csv. The RelocatedDependencyCheck recipe now surfaces that context both in the RelocatedDependencyReport data table row and in the inline search-result marker comment, so update the test expectations to match. Fixes the scheduled CI failure in run 28122079598. * Keep direct dependencies whose scope or exclusions differ from the transitive one (#188) * Keep direct dependencies whose scope or exclusions differ from the transitive one RemoveRedundantDependencies removed a direct dependency whenever a matched parent provided the same coordinate transitively, ignoring scope and exclusions. This silently changed the effective classpath when the direct declaration carried information the transitive entry did not. A direct dependency is now only removed when the transitive one provides it at the exact same effective scope and with the same exclusions: - Track each transitive dependency's effective exclusions and only treat a direct dependency as redundant when its exclusions match exactly. - Key Maven transitives by the parent dependency's own effective scope and compare against the direct dependency's effective scope, rather than treating a broader transitive scope as covering a narrower direct one. Fixes the five scenarios from openrewrite/rewrite#8235. * Simplify test POMs: drop XML declaration and project attributes * Extract handleGradle/handleMaven and trim comments * Rename getCompatibleTransitives to getCompatibleGradleTransitives * Polish --------- Co-authored-by: Tim te Beek <timtebeek@gmail.com> Co-authored-by: Moderne <team@moderne.io> Co-authored-by: Tim te Beek <tim@moderne.io> Co-authored-by: Steve Elliott <steve@moderne.io> Co-authored-by: Jente Sondervorst <jentesondervorst@gmail.com> Co-authored-by: Greg Oledzki <greg.oledzki@moderne.io> Co-authored-by: Knut Wannheden <knut@moderne.io>
1 parent 0371a6c commit decb8db

4 files changed

Lines changed: 36 additions & 42 deletions

File tree

src/main/java/org/openrewrite/java/dependencies/DependencyList.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.Set;
3737

3838
import static java.util.Collections.emptyMap;
39+
import static org.openrewrite.PathUtils.separatorsToUnix;
3940

4041
@EqualsAndHashCode(callSuper = false)
4142
@Value
@@ -89,6 +90,7 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
8990
if (tree == null) {
9091
return null;
9192
}
93+
String path = tree instanceof SourceFile ? separatorsToUnix(((SourceFile) tree).getSourcePath().toString()) : "";
9294
Markers m = tree.getMarkers();
9395
Set<ResolvedGroupArtifactVersion> seen = new HashSet<>();
9496
m.findFirst(GradleProject.class)
@@ -100,7 +102,7 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
100102
if (dep.getDepth() > 0) {
101103
continue;
102104
}
103-
insertDependency(ctx, gradle, seen, dep, true);
105+
insertDependency(ctx, gradle, path, seen, dep, true);
104106
}
105107
}
106108
});
@@ -109,7 +111,7 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
109111
if (dep.getDepth() > 0) {
110112
continue;
111113
}
112-
insertDependency(ctx, maven, seen, dep, true);
114+
insertDependency(ctx, maven, path, seen, dep, true);
113115
}
114116
});
115117
return tree;
@@ -121,14 +123,13 @@ private Scope scope() {
121123
return scope == null ? Scope.Compile : scope;
122124
}
123125

124-
private void insertDependency(ExecutionContext ctx, GradleProject gradle, Set<ResolvedGroupArtifactVersion> seen, ResolvedDependency dep, boolean direct) {
126+
private void insertDependency(ExecutionContext ctx, GradleProject gradle, String path, Set<ResolvedGroupArtifactVersion> seen, ResolvedDependency dep, boolean direct) {
125127
if (!seen.add(dep.getGav())) {
126128
return;
127129
}
128130
String resolutionFailure = "";
129131
if (validateResolvable) {
130132
try {
131-
//noinspection DataFlowIssue
132133
metadataFailures.insertRows(ctx, () -> new MavenPomDownloader(
133134
emptyMap(), ctx,
134135
null,
@@ -140,6 +141,7 @@ private void insertDependency(ExecutionContext ctx, GradleProject gradle, Set<Re
140141
}
141142
report.insertRow(ctx, new DependencyListReport.Row(
142143
"Gradle",
144+
path,
143145
gradle.getGroup(),
144146
gradle.getName(),
145147
gradle.getVersion(),
@@ -151,12 +153,12 @@ private void insertDependency(ExecutionContext ctx, GradleProject gradle, Set<Re
151153
));
152154
if (includeTransitive) {
153155
for (ResolvedDependency transitive : dep.getDependencies()) {
154-
insertDependency(ctx, gradle, seen, transitive, false);
156+
insertDependency(ctx, gradle, path, seen, transitive, false);
155157
}
156158
}
157159
}
158160

159-
private void insertDependency(ExecutionContext ctx, MavenResolutionResult maven, Set<ResolvedGroupArtifactVersion> seen, ResolvedDependency dep, boolean direct) {
161+
private void insertDependency(ExecutionContext ctx, MavenResolutionResult maven, String path, Set<ResolvedGroupArtifactVersion> seen, ResolvedDependency dep, boolean direct) {
160162
if (!seen.add(dep.getGav())) {
161163
return;
162164
}
@@ -180,6 +182,7 @@ private void insertDependency(ExecutionContext ctx, MavenResolutionResult maven,
180182
}
181183
report.insertRow(ctx, new DependencyListReport.Row(
182184
"Maven",
185+
path,
183186
maven.getPom().getGroupId(),
184187
maven.getPom().getArtifactId(),
185188
maven.getPom().getVersion(),
@@ -191,7 +194,7 @@ private void insertDependency(ExecutionContext ctx, MavenResolutionResult maven,
191194
));
192195
if (includeTransitive) {
193196
for (ResolvedDependency transitive : dep.getDependencies()) {
194-
insertDependency(ctx, maven, seen, transitive, false);
197+
insertDependency(ctx, maven, path, seen, transitive, false);
195198
}
196199
}
197200
}

src/main/java/org/openrewrite/java/dependencies/table/DependencyListReport.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ public static class Row {
3636
description = "The build tool used to manage dependencies (Gradle or Maven).")
3737
String buildTool;
3838

39+
@Column(displayName = "Path",
40+
description = "Path to the build file declaring the dependency")
41+
String path;
42+
3943
@Column(displayName = "Group id",
4044
description = "The Group ID of the Gradle project or Maven module requesting the dependency.")
4145
String groupId;

src/test/java/org/openrewrite/java/dependencies/DependencyListTest.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,10 @@ void directOnly() {
9393
rewriteRun(
9494
spec -> spec.recipe(new DependencyList(DependencyList.Scope.Compile, false, false))
9595
.beforeRecipe(withToolingApi())
96-
.dataTable(DependencyListReport.Row.class, rows -> {
97-
assertThat(rows)
98-
.containsExactlyInAnyOrder(
99-
new DependencyListReport.Row("Gradle", "com.test", "test", "1.0.0", "io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true, ""),
100-
new DependencyListReport.Row("Maven", "com.test", "test", "1.0.0", "io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true, ""));
101-
}),
96+
.dataTable(DependencyListReport.Row.class, rows -> assertThat(rows)
97+
.containsExactlyInAnyOrder(
98+
new DependencyListReport.Row("Gradle", "build.gradle", "com.test", "test", "1.0.0", "io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true, ""),
99+
new DependencyListReport.Row("Maven", "pom.xml", "com.test", "test", "1.0.0", "io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true, ""))),
102100
settingsGradle("rootProject.name = 'test'"),
103101
buildGradle(
104102
//language=groovy

src/test/java/org/openrewrite/java/dependencies/DependencyResolutionDiagnosticTest.java

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ void gradle() {
9191
url "https://nonexistent.moderne.io/maven2"
9292
}
9393
}
94-
94+
9595
dependencies {
9696
implementation("org.openrewrite.nonexistent:nonexistent:0.0.0")
9797
}
@@ -107,14 +107,12 @@ void gradleNoDefaultRepos() {
107107
spec -> spec.beforeRecipe(withToolingApi())
108108
// It is a limitation of the tooling API which prevents configuration-granularity error information from being collected.
109109
// So the GradleDependencyConfigurationErrors table will never be populated in unit tests.
110-
.dataTable(RepositoryAccessibilityReport.Row.class, rows -> {
111-
assertThat(rows)
112-
.hasSize(2)
113-
.contains(
114-
new RepositoryAccessibilityReport.Row("https://plugins.gradle.org/m2", "", "", 200, "", ""))
115-
.contains(
116-
new RepositoryAccessibilityReport.Row("https://nonexistent.moderne.io/maven2", "java.net.UnknownHostException", "nonexistent.moderne.io", null, "", ""));
117-
}),
110+
.dataTable(RepositoryAccessibilityReport.Row.class, rows -> assertThat(rows)
111+
.hasSize(2)
112+
.contains(
113+
new RepositoryAccessibilityReport.Row("https://plugins.gradle.org/m2", "", "", 200, "", ""))
114+
.contains(
115+
new RepositoryAccessibilityReport.Row("https://nonexistent.moderne.io/maven2", "java.net.UnknownHostException", "nonexistent.moderne.io", null, "", ""))),
118116
//language=groovy
119117
buildGradle(
120118
"""
@@ -126,7 +124,7 @@ void gradleNoDefaultRepos() {
126124
url "https://nonexistent.moderne.io/maven2"
127125
}
128126
}
129-
127+
130128
dependencies {
131129
implementation("org.openrewrite.nonexistent:nonexistent:0.0.0")
132130
}
@@ -158,13 +156,11 @@ void mavenSettingsWithMirrors() {
158156
""".getBytes())), ctx);
159157
ctx.setMavenSettings(settings);
160158
spec.beforeRecipe(withToolingApi())
161-
.dataTable(RepositoryAccessibilityReport.Row.class, rows -> {
162-
assertThat(rows)
163-
.contains(
164-
new RepositoryAccessibilityReport.Row("https://nonexistent.moderne.io/maven2", "java.net.UnknownHostException", "nonexistent.moderne.io", null, "", "")
165-
)
166-
.noneMatch(repo -> repo.getUri().contains("https://repo.maven.apache.org/maven2"));
167-
})
159+
.dataTable(RepositoryAccessibilityReport.Row.class, rows -> assertThat(rows)
160+
.contains(
161+
new RepositoryAccessibilityReport.Row("https://nonexistent.moderne.io/maven2", "java.net.UnknownHostException", "nonexistent.moderne.io", null, "", "")
162+
)
163+
.noneMatch(repo -> repo.getUri().contains("https://repo.maven.apache.org/maven2")))
168164
.executionContext(ctx);
169165
},
170166
//language=xml
@@ -189,24 +185,17 @@ void maven() {
189185
// nor the test's `nonexistent.moderne.io` repo appear in the diagnostic. Force empty settings
190186
// here so the recipe sees the pom-declared repositories directly.
191187
MavenExecutionContextView ctx = MavenExecutionContextView.view(new InMemoryExecutionContext());
192-
MavenSettings emptySettings = MavenSettings.parse(new Parser.Input(Path.of("settings.xml"), () -> new ByteArrayInputStream(
193-
//language=xml
194-
"""
195-
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
196-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
197-
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd"/>
198-
""".getBytes())), ctx);
199-
ctx.setMavenSettings(emptySettings);
200-
spec.beforeRecipe(withToolingApi())
188+
ctx.setMavenSettings(new MavenSettings(null, null, null, null, null, null));
189+
spec
190+
.executionContext(ctx)
201191
.dataTable(RepositoryAccessibilityReport.Row.class, rows -> {
202192
assertThat(rows).contains(
203193
new RepositoryAccessibilityReport.Row("https://repo.maven.apache.org/maven2", "", "", 200, "", ""));
204194
assertThat(rows).filteredOn(row -> row.getUri().startsWith("file:/") && "".equals(row.getPingExceptionMessage())).hasSize(1);
205195
assertThat(rows).contains(
206196
new RepositoryAccessibilityReport.Row("https://nonexistent.moderne.io/maven2", "java.net.UnknownHostException", "nonexistent.moderne.io", null, "", "")
207197
);
208-
})
209-
.executionContext(ctx);
198+
});
210199
},
211200
//language=xml
212201
pomXml(
@@ -215,7 +204,7 @@ void maven() {
215204
<groupId>com.example</groupId>
216205
<artifactId>test</artifactId>
217206
<version>0.1.0</version>
218-
207+
219208
<repositories>
220209
<repository>
221210
<id>nonexistent</id>

0 commit comments

Comments
 (0)