From e0fd2959e2f52dc11c58a7852b59080e70c3ccb9 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Tue, 21 Jul 2026 17:48:45 -0700 Subject: [PATCH 1/5] Fix provided deps resolution for maven --- .../src/main/java/org/openrewrite/maven/tree/ResolvedPom.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java index ca50fb0ec8..edb6bc0cc9 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java @@ -1028,7 +1028,7 @@ private List doResolveDependencies(Scope scope, Map Date: Tue, 21 Jul 2026 18:19:56 -0700 Subject: [PATCH 2/5] Fix provided deps resolution for maven --- .../openrewrite/maven/tree/ResolvedPom.java | 4 +- .../org/openrewrite/maven/tree/Scope.java | 25 ++++++++ .../openrewrite/maven/MavenParserTest.java | 62 +++++++++++++++++++ .../org/openrewrite/maven/tree/ScopeTest.java | 27 ++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java index edb6bc0cc9..7b0fe8898c 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java @@ -1028,7 +1028,7 @@ private List doResolveDependencies(Scope scope, Map doResolveDependencies(Scope scope, Map + * this (rows) x target scope (columns): + *
+     *           Compile   Provided   Runtime   Test
+     * Compile:    T         T          T         T
+     * Provided:   F         T          F         F
+     * Runtime:    F         F          T         T
+     * Test:       F         F          F         T
+     * 
+ * + * @param scope The target scope whose dependency set is being assembled. + * @return If a dependency declared with this scope belongs in that target scope's dependency set. + */ + public boolean isDirectlyIncludedIn(Scope scope) { + if (this == scope || this == Compile) { + return true; + } + return this == Runtime && scope == Test; + } + /** * See the table at https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#dependency-scope. * this represents the scope on the top row of the table. diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java index 34996218f8..6b9edd8dc4 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java @@ -3259,6 +3259,68 @@ void runtimeClasspathOnly() { ); } + @Test + void runtimeScopedDirectDependencyIsNotOnProvidedClasspath() { + rewriteRun( + pomXml( + """ + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-core + 2.15.0 + runtime + + + + """, + spec -> spec.afterRecipe(pomXml -> { + MavenResolutionResult resolution = pomXml.getMarkers().findFirst(MavenResolutionResult.class).orElseThrow(); + assertThat(resolution.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Runtime)).isNotEmpty(); + assertThat(resolution.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Test)).isNotEmpty(); + assertThat(resolution.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Compile)).isEmpty(); + assertThat(resolution.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Provided)).isEmpty(); + }) + ) + ); + } + + @Test + void transitiveRuntimeScopedDependencyIsNotOnProvidedClasspath() { + // org.openrewrite:rewrite-maven declares com.github.ben-manes.caffeine:caffeine with + // runtime in its own pom; it should not leak onto the compile-time + // "provided" classpath of a project that merely compile-depends on rewrite-maven. + rewriteRun( + pomXml( + """ + + com.mycompany.app + my-app + 1 + + + + org.openrewrite + rewrite-maven + 8.81.0 + + + + """, + spec -> spec.afterRecipe(pomXml -> { + MavenResolutionResult resolution = pomXml.getMarkers().findFirst(MavenResolutionResult.class).orElseThrow(); + assertThat(resolution.findDependencies("com.github.ben-manes.caffeine", "caffeine", Scope.Runtime)).isNotEmpty(); + assertThat(resolution.findDependencies("com.github.ben-manes.caffeine", "caffeine", Scope.Provided)).isEmpty(); + }) + ) + ); + } + @Issue("https://github.com/openrewrite/rewrite/issues/4093") @Test void circularImportDependency() { diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ScopeTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ScopeTest.java index 8e3cee3783..d25762987f 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ScopeTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ScopeTest.java @@ -27,4 +27,31 @@ void isInClasspathOf() { assertThat(Scope.Runtime.isInClasspathOf(Scope.Compile)).isTrue(); assertThat(Scope.Test.isInClasspathOf(Scope.Compile)).isFalse(); } + + @Test + void isDirectlyIncludedIn() { + // compile is available everywhere + assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Compile)).isTrue(); + assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Provided)).isTrue(); + assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Runtime)).isTrue(); + assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Test)).isTrue(); + + // provided is only available in its own bucket (compile classpath) + assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Compile)).isFalse(); + assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Provided)).isTrue(); + assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Runtime)).isFalse(); + assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Test)).isFalse(); + + // runtime is available at runtime and test, but not compile or provided + assertThat(Scope.Runtime.isDirectlyIncludedIn(Scope.Compile)).isFalse(); + assertThat(Scope.Runtime.isDirectlyIncludedIn(Scope.Provided)).isFalse(); + assertThat(Scope.Runtime.isDirectlyIncludedIn(Scope.Runtime)).isTrue(); + assertThat(Scope.Runtime.isDirectlyIncludedIn(Scope.Test)).isTrue(); + + // test is only available in its own bucket + assertThat(Scope.Test.isDirectlyIncludedIn(Scope.Compile)).isFalse(); + assertThat(Scope.Test.isDirectlyIncludedIn(Scope.Provided)).isFalse(); + assertThat(Scope.Test.isDirectlyIncludedIn(Scope.Runtime)).isFalse(); + assertThat(Scope.Test.isDirectlyIncludedIn(Scope.Test)).isTrue(); + } } From 2446fc858aac222b132389ee030b66864862cc7b Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Wed, 22 Jul 2026 14:11:35 -0700 Subject: [PATCH 3/5] comment added --- .../src/main/java/org/openrewrite/maven/tree/ResolvedPom.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java index 7b0fe8898c..9b26acb73b 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java @@ -1150,6 +1150,9 @@ private List doResolveDependencies(Scope scope, Map Date: Thu, 23 Jul 2026 14:27:51 -0700 Subject: [PATCH 4/5] Corrections --- .../openrewrite/maven/tree/ResolvedPom.java | 5 +- .../org/openrewrite/maven/tree/Scope.java | 22 ++- .../maven/tree/ResolvedPomTest.java | 133 ++++++++++++++++++ .../org/openrewrite/maven/tree/ScopeTest.java | 10 +- 4 files changed, 157 insertions(+), 13 deletions(-) diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java index 9b26acb73b..0a629cb4fb 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java @@ -1150,10 +1150,7 @@ private List doResolveDependencies(Scope scope, Map + * There are only three real classpaths - compile, runtime, and test - "provided" is not itself a classpath, + * but a scope that contributes to the compile and test classpaths while being withheld from the runtime + * classpath. This matches the scope table at + * Dependency Scope. + *

* this (rows) x target scope (columns): *

      *           Compile   Provided   Runtime   Test
-     * Compile:    T         T          T         T
-     * Provided:   F         T          F         F
+     * Compile:    T         F          T         T
+     * Provided:   T         T          F         T
      * Runtime:    F         F          T         T
      * Test:       F         F          F         T
      * 
@@ -54,10 +59,19 @@ public boolean isInClasspathOf(@Nullable Scope scope) { * @return If a dependency declared with this scope belongs in that target scope's dependency set. */ public boolean isDirectlyIncludedIn(Scope scope) { - if (this == scope || this == Compile) { + if (this == scope) { return true; } - return this == Runtime && scope == Test; + switch (this) { + case Compile: + return scope == Runtime || scope == Test; + case Provided: + return scope == Compile || scope == Test; + case Runtime: + return scope == Test; + default: + return false; + } } /** diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ResolvedPomTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ResolvedPomTest.java index eb0bf9e5d2..d6608480aa 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ResolvedPomTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ResolvedPomTest.java @@ -32,6 +32,9 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -918,4 +921,134 @@ private static MavenRepository createMavenRepository(Path localRepository, Strin .knownToExist(true) .build(); } + + @Test + void dependencyResolution() { + // A minimal, fully synthetic dependency graph exercising every interesting case for scope-bucket + // membership without a large, real-world transitive closure to enumerate: + // app -[compile]-> lib -[runtime]-> runtime-transitive (a transitive dependency that is + // "runtime" in its own pom, reached + // beneath a "compile" root) + // app -[provided]-> provided-leaf (a direct "provided" dependency also belongs on the + // compile and test classpaths, but not runtime) + // app -[runtime]-> runtime-leaf + // app -[test]-> test-leaf + rewriteRun( + pomXml(""" + + com.example + app + 1 + + + com.example + lib + 1 + + + com.example + provided-leaf + 1 + provided + + + com.example + runtime-leaf + 1 + runtime + + + com.example + test-leaf + 1 + test + + + + """, + spec -> spec.path("pom.xml").afterRecipe(pom -> { + MavenResolutionResult resolution = pom.getMarkers().findFirst(MavenResolutionResult.class).orElseThrow(); + assertThat(coordinates(resolution, Scope.Compile)).isEqualTo(Set.of( + "com.example:lib:1", + "com.example:provided-leaf:1" + )); + assertThat(coordinates(resolution, Scope.Provided)).isEqualTo(Set.of( + "com.example:provided-leaf:1" + )); + assertThat(coordinates(resolution, Scope.Runtime)).isEqualTo(Set.of( + "com.example:lib:1", + "com.example:runtime-leaf:1", + "com.example:runtime-transitive:1" + )); + assertThat(coordinates(resolution, Scope.Test)).isEqualTo(Set.of( + "com.example:lib:1", + "com.example:provided-leaf:1", + "com.example:runtime-leaf:1", + "com.example:runtime-transitive:1", + "com.example:test-leaf:1" + )); + }) + ), + pomXml(""" + + com.example + lib + 1 + + + com.example + runtime-transitive + 1 + runtime + + + + """, + spec -> spec.path("lib/pom.xml") + ), + pomXml(""" + + com.example + runtime-transitive + 1 + + """, + spec -> spec.path("runtime-transitive/pom.xml") + ), + pomXml(""" + + com.example + provided-leaf + 1 + + """, + spec -> spec.path("provided-leaf/pom.xml") + ), + pomXml(""" + + com.example + runtime-leaf + 1 + + """, + spec -> spec.path("runtime-leaf/pom.xml") + ), + pomXml(""" + + com.example + test-leaf + 1 + + """, + spec -> spec.path("test-leaf/pom.xml") + ) + ); + } + + private static Set coordinates(MavenResolutionResult resolution, Scope scope) { + return resolution.getDependencies().get(scope).stream() + .map(d -> d.getGav().toString()) + .collect(Collectors.toCollection(TreeSet::new)); + } + } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ScopeTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ScopeTest.java index d25762987f..05f9d01c90 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ScopeTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ScopeTest.java @@ -30,17 +30,17 @@ void isInClasspathOf() { @Test void isDirectlyIncludedIn() { - // compile is available everywhere + // compile is available on the compile, runtime, and test classpaths assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Compile)).isTrue(); - assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Provided)).isTrue(); + assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Provided)).isFalse(); assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Runtime)).isTrue(); assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Test)).isTrue(); - // provided is only available in its own bucket (compile classpath) - assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Compile)).isFalse(); + // provided is available on the compile and test classpaths, but withheld from runtime + assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Compile)).isTrue(); assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Provided)).isTrue(); assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Runtime)).isFalse(); - assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Test)).isFalse(); + assertThat(Scope.Provided.isDirectlyIncludedIn(Scope.Test)).isTrue(); // runtime is available at runtime and test, but not compile or provided assertThat(Scope.Runtime.isDirectlyIncludedIn(Scope.Compile)).isFalse(); From 767bb03bab5a65ab7a69df7b3b2c09be08f15a55 Mon Sep 17 00:00:00 2001 From: BoykoAlex Date: Thu, 23 Jul 2026 15:55:29 -0700 Subject: [PATCH 5/5] Comment update --- .../main/java/org/openrewrite/maven/tree/Scope.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/Scope.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/Scope.java index bfc1fbc518..398df067e0 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/Scope.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/Scope.java @@ -36,10 +36,10 @@ public boolean isInClasspathOf(@Nullable Scope scope) { } /** - * Unlike {@link #transitiveOf}/{@link #isInClasspathOf}, which govern how a dependency's scope degrades one hop - * further down its own transitive dependencies, this answers a single-hop question: does a dependency declared - * with this scope belong in the dependency set being assembled for the given target scope - regardless of what - * scope included the dependency one level further up the tree. + * Used to decide which of a project's own directly-requested dependencies seed the resolution of a given + * target scope. Unlike {@link #transitiveOf}/{@link #isInClasspathOf}, which govern how a dependency's scope + * degrades one hop further down its own transitive dependencies, this answers a single-hop question: does a + * dependency declared with this scope belong in the dependency set being assembled for the given target scope. *

* There are only three real classpaths - compile, runtime, and test - "provided" is not itself a classpath, * but a scope that contributes to the compile and test classpaths while being withheld from the runtime @@ -56,7 +56,8 @@ public boolean isInClasspathOf(@Nullable Scope scope) { * * * @param scope The target scope whose dependency set is being assembled. - * @return If a dependency declared with this scope belongs in that target scope's dependency set. + * @return If a directly-requested dependency declared with this scope belongs in that target scope's + * dependency set. */ public boolean isDirectlyIncludedIn(Scope scope) { if (this == scope) {