Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 Expand Up @@ -1150,7 +1150,7 @@ private List<ResolvedDependency> doResolveDependencies(Scope scope, Map<GroupArt
includedByMap.put(resolved, includedBy);
}

if (dd.getScope().transitiveOf(scope) == scope) {
if (dd.getScope().isDirectlyIncludedIn(scope)) {
dependencies.add(resolved);
} else {
continue;
Expand Down
25 changes: 25 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,31 @@ public boolean isInClasspathOf(@Nullable Scope scope) {
return this.transitiveOf(scope) != null;
}

/**
* 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.
* <p>
* this (rows) x target scope (columns):
* <pre>
* Compile Provided Runtime Test
* Compile: T T T T
* Provided: F T F F
* 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 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 <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 @@ -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();
}
}