Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ private List<ResolvedDependency> doResolveDependencies(Scope scope, Map<GroupArt
for (Dependency requestedDependency : getRequestedDependencies()) {
Dependency d = getValues(requestedDependency, 0);
Scope dScope = Scope.fromName(d.getScope());
if (dScope == scope || dScope.transitiveOf(scope) == scope) {
if (dScope.isDirectlyIncludedIn(scope)) {
// For direct dependencies that are duplicated, last declaration wins
// TODO: We could introduce a ResolutionStrategy to handle this differently for Gradle which uses highest version
rootDependencies.put(d.getGav().asGroupArtifact(), new DependencyAndDependent(requestedDependency, Scope.Compile, null, requestedDependency, this));
Expand Down
40 changes: 40 additions & 0 deletions rewrite-maven/src/main/java/org/openrewrite/maven/tree/Scope.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,46 @@ public boolean isInClasspathOf(@Nullable Scope scope) {
return this.transitiveOf(scope) != null;
}

/**
* 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.
* <p>
* 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
* <a href="https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#dependency-scope">Dependency Scope</a>.
* <p>
* this (rows) x target scope (columns):
* <pre>
* Compile Provided Runtime Test
* Compile: T F T T
* Provided: T T F T

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure provided dependencies are included automatically in test classpaths? I thought they weren't but I'm not confident

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided scope atrifacts seem to leak into test classpath. I was checking this via dependency:build-classpath -DincludeScope=<scope> goal.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>scope-example</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.0.3</version>
  </parent>
  
  <dependencies>
  	<dependency>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-core</artifactId>
  	</dependency>
  	<dependency>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-beans</artifactId>
  		<scope>provided</scope>
  	</dependency>
  </dependencies>

</project>

For mvn dependency:build-classpath -DincludeScope=test (same for compile) I got:

/Users/aboyko/.m2/repository/org/springframework/spring-core/7.0.5/spring-core-7.0.5.jar:/Users/aboyko/.m2/repository/commons-logging/commons-logging/1.3.5/commons-logging-1.3.5.jar:/Users/aboyko/.m2/repository/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar:/Users/aboyko/.m2/repository/org/springframework/spring-beans/7.0.5/spring-beans-7.0.5.jar

(spring-beans are in the list)

For mvn dependency:build-classpath -DincludeScope=runtime I got:

/Users/aboyko/.m2/repository/org/springframework/spring-core/7.0.5/spring-core-7.0.5.jar:/Users/aboyko/.m2/repository/commons-logging/commons-logging/1.3.5/commons-logging-1.3.5.jar:/Users/aboyko/.m2/repository/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar

For mvn dependency:build-classpath -DincludeScope=provided I got:

/Users/aboyko/.m2/repository/org/springframework/spring-beans/7.0.5/spring-beans-7.0.5.jar

* Runtime: F F T T
* Test: F F F T
* </pre>
*
* @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 <a href="Dependency Scope">https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#dependency-scope</a>.
* <code>this</code> represents the scope on the top row of the table.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3259,6 +3259,68 @@ void runtimeClasspathOnly() {
);
}

@Test
void runtimeScopedDirectDependencyIsNotOnProvidedClasspath() {
rewriteRun(
pomXml(
"""
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1</version>

<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.15.0</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
""",
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
// <scope>runtime</scope> 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(
"""
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1</version>

<dependencies>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-maven</artifactId>
<version>8.81.0</version>
</dependency>
</dependencies>
</project>
""",
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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("""
<project>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>1</version>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>lib</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>provided-leaf</artifactId>
<version>1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>runtime-leaf</artifactId>
<version>1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>test-leaf</artifactId>
<version>1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
""",
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("""
<project>
<groupId>com.example</groupId>
<artifactId>lib</artifactId>
<version>1</version>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>runtime-transitive</artifactId>
<version>1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
""",
spec -> spec.path("lib/pom.xml")
),
pomXml("""
<project>
<groupId>com.example</groupId>
<artifactId>runtime-transitive</artifactId>
<version>1</version>
</project>
""",
spec -> spec.path("runtime-transitive/pom.xml")
),
pomXml("""
<project>
<groupId>com.example</groupId>
<artifactId>provided-leaf</artifactId>
<version>1</version>
</project>
""",
spec -> spec.path("provided-leaf/pom.xml")
),
pomXml("""
<project>
<groupId>com.example</groupId>
<artifactId>runtime-leaf</artifactId>
<version>1</version>
</project>
""",
spec -> spec.path("runtime-leaf/pom.xml")
),
pomXml("""
<project>
<groupId>com.example</groupId>
<artifactId>test-leaf</artifactId>
<version>1</version>
</project>
""",
spec -> spec.path("test-leaf/pom.xml")
)
);
}

private static Set<String> coordinates(MavenResolutionResult resolution, Scope scope) {
return resolution.getDependencies().get(scope).stream()
.map(d -> d.getGav().toString())
.collect(Collectors.toCollection(TreeSet::new));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}