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 ca50fb0ec8f..0a629cb4fb8 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 + * 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         F          T         T
+     * Provided:   T         T          F         T
+     * Runtime:    F         F          T         T
+     * Test:       F         F          F         T
+     * 
+ * + * @param scope The target scope whose dependency set is being assembled. + * @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) { + return true; + } + switch (this) { + case Compile: + return scope == Runtime || scope == Test; + case Provided: + return scope == Compile || scope == Test; + case Runtime: + return scope == Test; + default: + return false; + } + } + /** * 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 34996218f8c..6b9edd8dc4f 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/ResolvedPomTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/ResolvedPomTest.java index eb0bf9e5d28..d6608480aac 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 8e3cee37833..05f9d01c90b 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 on the compile, runtime, and test classpaths + assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Compile)).isTrue(); + assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Provided)).isFalse(); + assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Runtime)).isTrue(); + assertThat(Scope.Compile.isDirectlyIncludedIn(Scope.Test)).isTrue(); + + // 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)).isTrue(); + + // 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(); + } }