From 7b6ce79c003e6b51f2223a0d301b66275b6df4ca Mon Sep 17 00:00:00 2001 From: Jente Sondervorst Date: Mon, 11 Aug 2025 09:28:27 +0200 Subject: [PATCH] Show impact of a ResolutionStrategy implementation --- .../RemoveRedundantDependencyVersions.java | 2 +- .../gradle/UpgradeDependencyVersion.java | 4 +- .../gradle/internal/AddDependencyVisitor.java | 2 +- .../org/openrewrite/maven/MavenParser.java | 7 +- .../openrewrite/maven/UpdateMavenModel.java | 2 +- .../maven/internal/NearestWins.java | 187 +++++++++ .../maven/internal/NewestWins.java | 128 ++++++ .../maven/internal/VersionRequirement.java | 213 ++-------- .../maven/tree/MavenResolutionResult.java | 6 +- .../maven/tree/ProfileActivation.java | 2 +- .../maven/tree/ResolutionStrategy.java | 6 + .../openrewrite/maven/tree/ResolvedPom.java | 73 +++- .../internal/MavenPomDownloaderTest.java | 2 +- .../internal/VersionRequirementTest.java | 106 +++-- .../maven/tree/MavenResolutionResultTest.java | 387 ++++++++++++++++++ 15 files changed, 889 insertions(+), 238 deletions(-) create mode 100644 rewrite-maven/src/main/java/org/openrewrite/maven/internal/NearestWins.java create mode 100644 rewrite-maven/src/main/java/org/openrewrite/maven/internal/NewestWins.java create mode 100644 rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolutionStrategy.java create mode 100644 rewrite-maven/src/test/java/org/openrewrite/maven/tree/MavenResolutionResultTest.java diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/RemoveRedundantDependencyVersions.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/RemoveRedundantDependencyVersions.java index 064364c412..eb77a33b54 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/RemoveRedundantDependencyVersions.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/RemoveRedundantDependencyVersions.java @@ -341,7 +341,7 @@ private boolean dependsOnNewerVersion(GroupArtifactVersion searchGav, GroupArtif try { List resolved = mpd.download(toSearch, null, null, repositories) .resolve(emptyList(), mpd, repositories, ctx) - .resolveDependencies(Scope.Runtime, mpd, ctx); + .resolveDependencies(Scope.Runtime, mpd, ResolutionStrategy.NEWEST_WINS, ctx); for (ResolvedDependency r : resolved) { if (Objects.equals(searchGav.getGroupId(), r.getGroupId()) && Objects.equals(searchGav.getArtifactId(), r.getArtifactId())) { return searchGav.getVersion() == null || matchesComparator(r.getVersion(), searchGav.getVersion()); diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeDependencyVersion.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeDependencyVersion.java index 4c31b88587..58372a5e9f 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeDependencyVersion.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeDependencyVersion.java @@ -998,7 +998,7 @@ public static GradleProject replaceVersion(GradleProject gp, ExecutionContext ct MavenPomDownloader mpd = new MavenPomDownloader(ctx); Pom pom = mpd.download(gav, null, null, gp.getMavenRepositories()); ResolvedPom resolvedPom = pom.resolve(emptyList(), mpd, gp.getMavenRepositories(), ctx); - List transitiveDependencies = resolvedPom.resolveDependencies(Scope.Runtime, mpd, ctx); + List transitiveDependencies = resolvedPom.resolveDependencies(Scope.Runtime, mpd, ResolutionStrategy.NEWEST_WINS, ctx); org.openrewrite.maven.tree.Dependency newRequested = org.openrewrite.maven.tree.Dependency.builder() .gav(gav) .build(); @@ -1170,7 +1170,7 @@ private static List updateDirectResolved( } List transitiveDependencies = filterTransitiveDependencies( - resolvedPom.resolveDependencies(Scope.Runtime, mpd, ctx), + resolvedPom.resolveDependencies(Scope.Runtime, mpd, ResolutionStrategy.NEWEST_WINS, ctx), existingTransitiveGAs, newTransitiveGAs ); diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/internal/AddDependencyVisitor.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/internal/AddDependencyVisitor.java index 55a9c1f78c..7e47c8835f 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/internal/AddDependencyVisitor.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/internal/AddDependencyVisitor.java @@ -241,7 +241,7 @@ public static JavaSourceFile addDependency( Pom pom = mpd.download(gav, null, null, gp.getMavenRepositories()); ResolvedPom resolvedPom = pom.resolve(emptyList(), mpd, gp.getMavenRepositories(), ctx); resolvedGav = resolvedPom.getGav(); - transitiveDependencies = resolvedPom.resolveDependencies(Scope.Runtime, mpd, ctx); + transitiveDependencies = resolvedPom.resolveDependencies(Scope.Runtime, mpd, ResolutionStrategy.NEWEST_WINS, ctx); } Map nameToConfiguration = gp.getNameToConfiguration(); Map newNameToConfiguration = new HashMap<>(nameToConfiguration.size()); diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/MavenParser.java b/rewrite-maven/src/main/java/org/openrewrite/maven/MavenParser.java index 8f43a66c66..f3d7787986 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/MavenParser.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/MavenParser.java @@ -21,10 +21,7 @@ import org.openrewrite.*; import org.openrewrite.maven.internal.MavenPomDownloader; import org.openrewrite.maven.internal.RawPom; -import org.openrewrite.maven.tree.MavenResolutionResult; -import org.openrewrite.maven.tree.Parent; -import org.openrewrite.maven.tree.Pom; -import org.openrewrite.maven.tree.ResolvedPom; +import org.openrewrite.maven.tree.*; import org.openrewrite.tree.ParseError; import org.openrewrite.xml.XmlParser; import org.openrewrite.xml.tree.Xml; @@ -118,7 +115,7 @@ public Stream parseInputs(Iterable sources, @Nullable Path re effectivelyActiveProfiles, properties); if (!skipDependencyResolution) { - model = model.resolveDependencies(downloader, ctx); + model = model.resolveDependencies(downloader, ResolutionStrategy.NEAREST_WINS, ctx); } parsed.add(docToPom.getKey().withMarkers(docToPom.getKey().getMarkers().compute(model, (old, n) -> n))); } catch (MavenDownloadingExceptions e) { diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/UpdateMavenModel.java b/rewrite-maven/src/main/java/org/openrewrite/maven/UpdateMavenModel.java index 657772fc50..794ed78729 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/UpdateMavenModel.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/UpdateMavenModel.java @@ -181,7 +181,7 @@ private MavenResolutionResult updateResult(ExecutionContext ctx, MavenResolution return module; } })) - .resolveDependencies(downloader, ctx); + .resolveDependencies(downloader, ResolutionStrategy.NEAREST_WINS, ctx); if (exceptions.get() != null) { throw exceptions.get(); } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/internal/NearestWins.java b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/NearestWins.java new file mode 100644 index 0000000000..2048b3492f --- /dev/null +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/NearestWins.java @@ -0,0 +1,187 @@ +/* + * Copyright 2020 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.maven.internal; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.jspecify.annotations.Nullable; +import org.openrewrite.maven.MavenDownloadingException; +import org.openrewrite.maven.internal.grammar.VersionRangeLexer; +import org.openrewrite.maven.internal.grammar.VersionRangeParser; +import org.openrewrite.maven.internal.grammar.VersionRangeParserBaseVisitor; +import org.openrewrite.maven.tree.Version; + +import java.util.Iterator; + +import static java.util.stream.Collectors.toList; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class NearestWins extends VersionRequirement { + + @Nullable + private final NearestWins nearer; + + private final VersionSpec versionSpec; + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (nearer != null) { + builder.append(nearer).append(", "); + } + builder.append(versionSpec); + return builder.toString(); + } + + public NearestWins addRequirement(String requested) { + if (versionSpec instanceof DirectRequirement) { + return this; + } + + VersionSpec newRequirement = buildVersionSpec(requested, false); + + NearestWins next = this; + while (next != null) { + if (next.versionSpec.equals(newRequirement)) { + return this; + } + next = next.nearer; + } + + return new NearestWins(this, newRequirement); + } + + static VersionSpec buildVersionSpec(String requested, boolean direct) { + if ("LATEST".equals(requested)) { + return DynamicVersion.LATEST; + } else if ("RELEASE".equals(requested)) { + return DynamicVersion.RELEASE; + } else if (requested.contains("[") || requested.contains("(")) { + // for things like the profile activation block of where the range is unclosed but maven still handles it, e.g. + // https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.12.0-rc2/jackson-databind-2.12.0-rc2.pom + if (!(requested.contains("]") || requested.contains(")"))) { + requested += "]"; + } + + VersionRangeParser parser = new VersionRangeParser(new CommonTokenStream(new VersionRangeLexer( + CharStreams.fromString(requested)))); + + parser.removeErrorListeners(); + parser.addErrorListener(new PrintingErrorListener()); + + return new VersionRangeParserBaseVisitor() { + @Override + public VersionSpec visitVersionRequirement(VersionRangeParser.VersionRequirementContext ctx) { + return new RangeSet(ctx.range().stream() + .map(range -> { + Version lower, upper; + if (range.bounds().boundedLower() != null) { + Iterator versionIter = range.bounds().boundedLower().Version().iterator(); + lower = versionIter.hasNext() ? toVersion(versionIter.next()) : null; + upper = versionIter.hasNext() ? toVersion(versionIter.next()) : null; + } else if (range.bounds().unboundedLower() != null) { + TerminalNode upperVersionNode = range.bounds().unboundedLower().Version(); + lower = null; + upper = upperVersionNode != null ? toVersion(upperVersionNode) : null; + } else { + lower = toVersion(range.bounds().exactly().Version()); + upper = toVersion(range.bounds().exactly().Version()); + } + return new Range( + range.CLOSED_RANGE_OPEN() != null, lower, + range.CLOSED_RANGE_CLOSE() != null, upper + ); + }) + .collect(toList()) + ); + } + + private Version toVersion(TerminalNode version) { + return new Version(version.getText()); + } + }.visit(parser.versionRequirement()); + } + return direct ? + new DirectRequirement(requested) : + new SoftRequirement(requested); + } + + @Value + @EqualsAndHashCode(callSuper = true) + private static class DirectRequirement extends SoftRequirement { + private DirectRequirement(String version) { + super(version); + } + + @Override + public String toString() { + return "Version='" + version + "'"; + } + } + + protected @Nullable String cacheResolved(DownloadOperation> availableVersions) throws MavenDownloadingException { + String nearestSoftRequirement = null; + NearestWins next = this; + NearestWins nearestHardRequirement = null; + + while (next != null) { + VersionSpec spec = next.versionSpec; + if (spec instanceof DirectRequirement) { + // dependencies defined in the project POM always win + return ((DirectRequirement) spec).getVersion(); + } else if (spec instanceof SoftRequirement) { + nearestSoftRequirement = ((SoftRequirement) spec).version; + } else { + nearestHardRequirement = next; + } + next = next.nearer; + } + + if (nearestHardRequirement == null) { + return nearestSoftRequirement; + } + VersionSpec hardRequirement = nearestHardRequirement.versionSpec; + Version latest = null; + for (String availableVersion : availableVersions.call()) { + Version version = new Version(availableVersion); + + if ((hardRequirement instanceof DynamicVersion || hardRequirement instanceof RangeSet) && hardRequirement.matches(version)) { + if (latest == null || version.compareTo(latest) > 0) { + latest = version; + } + } + } + + if (latest == null) { + // No version matches the hard requirement. + return null; + } + + return latest.toString(); + } + + private static class PrintingErrorListener extends BaseErrorListener { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, + int line, int charPositionInLine, String msg, RecognitionException e) { + System.out.printf("Syntax error at line %d:%d %s%n", line, charPositionInLine, msg); + } + } +} diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/internal/NewestWins.java b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/NewestWins.java new file mode 100644 index 0000000000..522f07755a --- /dev/null +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/NewestWins.java @@ -0,0 +1,128 @@ +/* + * Copyright 2020 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.maven.internal; + +import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.jspecify.annotations.Nullable; +import org.openrewrite.maven.MavenDownloadingException; +import org.openrewrite.maven.internal.grammar.VersionRangeLexer; +import org.openrewrite.maven.internal.grammar.VersionRangeParser; +import org.openrewrite.maven.internal.grammar.VersionRangeParserBaseVisitor; +import org.openrewrite.maven.tree.Version; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +public class NewestWins extends VersionRequirement { + + private final Set requestedVersions = new HashSet<>(); + + NewestWins(VersionSpec requestedVersions) { + this.requestedVersions.add(requestedVersions); + } + + @Override + public String toString() { + return requestedVersions.stream().map(Object::toString).collect(Collectors.joining(", ")); + } + + public NewestWins addRequirement(String requested) { + VersionSpec requestedSpec = buildVersionSpec(requested); + if (requestedVersions.add(requestedSpec)) { + super.selected = null; + } + return this; + } + + static VersionSpec buildVersionSpec(String requested) { + if ("LATEST".equals(requested)) { + return DynamicVersion.LATEST; + } else if ("RELEASE".equals(requested)) { + return DynamicVersion.RELEASE; + } else if (requested.contains("[") || requested.contains("(")) { + // for things like the profile activation block of where the range is unclosed but maven still handles it, e.g. + // https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.12.0-rc2/jackson-databind-2.12.0-rc2.pom + if (!(requested.contains("]") || requested.contains(")"))) { + requested += "]"; + } + + VersionRangeParser parser = new VersionRangeParser(new CommonTokenStream(new VersionRangeLexer( + CharStreams.fromString(requested)))); + + parser.removeErrorListeners(); + parser.addErrorListener(new PrintingErrorListener()); + + return new VersionRangeParserBaseVisitor() { + @Override + public VersionSpec visitVersionRequirement(VersionRangeParser.VersionRequirementContext ctx) { + return new RangeSet(ctx.range().stream() + .map(range -> { + Version lower, upper; + if (range.bounds().boundedLower() != null) { + Iterator versionIter = range.bounds().boundedLower().Version().iterator(); + lower = versionIter.hasNext() ? toVersion(versionIter.next()) : null; + upper = versionIter.hasNext() ? toVersion(versionIter.next()) : null; + } else if (range.bounds().unboundedLower() != null) { + TerminalNode upperVersionNode = range.bounds().unboundedLower().Version(); + lower = null; + upper = upperVersionNode != null ? toVersion(upperVersionNode) : null; + } else { + lower = toVersion(range.bounds().exactly().Version()); + upper = toVersion(range.bounds().exactly().Version()); + } + return new Range( + range.CLOSED_RANGE_OPEN() != null, lower, + range.CLOSED_RANGE_CLOSE() != null, upper + ); + }) + .collect(toList()) + ); + } + + private Version toVersion(TerminalNode version) { + return new Version(version.getText()); + } + }.visit(parser.versionRequirement()); + } + return new SoftRequirement(requested); + } + + protected @Nullable String cacheResolved(DownloadOperation> availableVersions) throws MavenDownloadingException { + Version latestVersion = null; + for (String availableVersion : availableVersions.call()) { + Version version = new Version(availableVersion); + if (requestedVersions.stream().anyMatch(spec -> spec.matches(version))) { + if (latestVersion == null || version.compareTo(latestVersion) > 0) { + latestVersion = version; + } + } + } + return latestVersion != null ? latestVersion.toString() : null; + } + + private static class PrintingErrorListener extends BaseErrorListener { + @Override + public void syntaxError(Recognizer recognizer, Object offendingSymbol, + int line, int charPositionInLine, String msg, RecognitionException e) { + System.out.printf("Syntax error at line %d:%d %s%n", line, charPositionInLine, msg); + } + } +} diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/internal/VersionRequirement.java b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/VersionRequirement.java index 5a2a26ce0e..421a416252 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/internal/VersionRequirement.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/VersionRequirement.java @@ -16,151 +16,66 @@ package org.openrewrite.maven.internal; import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.RequiredArgsConstructor; import lombok.Value; -import org.antlr.v4.runtime.*; -import org.antlr.v4.runtime.tree.TerminalNode; import org.jspecify.annotations.Nullable; import org.openrewrite.maven.MavenDownloadingException; -import org.openrewrite.maven.internal.grammar.VersionRangeLexer; -import org.openrewrite.maven.internal.grammar.VersionRangeParser; -import org.openrewrite.maven.internal.grammar.VersionRangeParserBaseVisitor; -import org.openrewrite.maven.tree.GroupArtifact; -import org.openrewrite.maven.tree.MavenMetadata; -import org.openrewrite.maven.tree.MavenRepository; -import org.openrewrite.maven.tree.Version; +import org.openrewrite.maven.tree.*; -import java.util.Iterator; import java.util.List; import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; -@RequiredArgsConstructor -public class VersionRequirement { +public abstract class VersionRequirement { @Nullable - private final VersionRequirement nearer; + protected volatile transient String selected; - private final VersionSpec versionSpec; - - @Nullable - private volatile transient String selected; - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - if (nearer != null) { - builder.append(nearer).append(", "); + public static VersionRequirement fromVersion(String requested, ResolutionStrategy resolutionStrategy, int depth) { + if (resolutionStrategy == ResolutionStrategy.NEAREST_WINS) { + return new NearestWins(null, NearestWins.buildVersionSpec(requested, depth == 0)); } - builder.append(versionSpec); - return builder.toString(); - } - - public static VersionRequirement fromVersion(String requested, int depth) { - return new VersionRequirement(null, VersionSpec.build(requested, depth == 0)); + return new NewestWins(NewestWins.buildVersionSpec(requested)); } - public VersionRequirement addRequirement(String requested) { - if (versionSpec instanceof DirectRequirement) { - return this; - } - - VersionSpec newRequirement = VersionSpec.build(requested, false); - - VersionRequirement next = this; - while (next != null) { - if (next.versionSpec.equals(newRequirement)) { - return this; - } - next = next.nearer; - } + public abstract VersionRequirement addRequirement(String requested); - return new VersionRequirement(this, newRequirement); + public @Nullable String resolve(GroupArtifact groupArtifact, MavenPomDownloader downloader, List repositories) throws MavenDownloadingException { + return resolve(() -> { + MavenMetadata metadata = downloader.downloadMetadata(groupArtifact, null, repositories); + return metadata.getVersioning().getVersions(); + }); } - interface VersionSpec { - static VersionSpec build(String requested, boolean direct) { - if ("LATEST".equals(requested)) { - return DynamicVersion.LATEST; - } else if ("RELEASE".equals(requested)) { - return DynamicVersion.RELEASE; - } else if (requested.contains("[") || requested.contains("(")) { - // for things like the profile activation block of where the range is unclosed but maven still handles it, e.g. - // https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.12.0-rc2/jackson-databind-2.12.0-rc2.pom - if (!(requested.contains("]") || requested.contains(")"))) { - requested += "]"; - } - - VersionRangeParser parser = new VersionRangeParser(new CommonTokenStream(new VersionRangeLexer( - CharStreams.fromString(requested)))); - - parser.removeErrorListeners(); - parser.addErrorListener(new PrintingErrorListener()); - - return new VersionRangeParserBaseVisitor() { - @Override - public VersionSpec visitVersionRequirement(VersionRangeParser.VersionRequirementContext ctx) { - return new RangeSet(ctx.range().stream() - .map(range -> { - Version lower, upper; - if (range.bounds().boundedLower() != null) { - Iterator versionIter = range.bounds().boundedLower().Version().iterator(); - lower = versionIter.hasNext() ? toVersion(versionIter.next()) : null; - upper = versionIter.hasNext() ? toVersion(versionIter.next()) : null; - } else if (range.bounds().unboundedLower() != null) { - TerminalNode upperVersionNode = range.bounds().unboundedLower().Version(); - lower = null; - upper = upperVersionNode != null ? toVersion(upperVersionNode) : null; - } else { - lower = toVersion(range.bounds().exactly().Version()); - upper = toVersion(range.bounds().exactly().Version()); - } - return new Range( - range.CLOSED_RANGE_OPEN() != null, lower, - range.CLOSED_RANGE_CLOSE() != null, upper - ); - }) - .collect(toList()) - ); - } - - private Version toVersion(TerminalNode version) { - return new Version(version.getText()); - } - }.visit(parser.versionRequirement()); - } - return direct ? - new DirectRequirement(requested) : - new SoftRequirement(requested); + public @Nullable String resolve(DownloadOperation> availableVersions) throws MavenDownloadingException { + if (selected == null) { + //TODO Is it bad that LATEST and RELEASE are also cached -> if a new version is released, it won't be picked up in the current cached state. Existing behavior to cache them though. + selected = cacheResolved(availableVersions); } + return selected; } - @Value - @EqualsAndHashCode(callSuper = true) - private static class DirectRequirement extends SoftRequirement { - private DirectRequirement(String version) { - super(version); - } + protected abstract @Nullable String cacheResolved(DownloadOperation> availableVersions) throws MavenDownloadingException; - @Override - public String toString() { - return "Version='" + version + "'"; - } + interface VersionSpec { + + boolean matches(Version version); } @Data - private static class SoftRequirement implements VersionSpec { + protected static class SoftRequirement implements VersionSpec { final String version; + public boolean matches(Version version) { + return this.version.equals(version.toString()); + } + @Override public String toString() { return "Soft version='" + version + "'"; } } - private enum DynamicVersion implements VersionSpec { + protected enum DynamicVersion implements VersionSpec { LATEST, RELEASE; @@ -176,7 +91,7 @@ public String toString() { } @Value - private static class RangeSet implements VersionSpec { + protected static class RangeSet implements VersionSpec { List ranges; public boolean matches(Version version) { @@ -209,7 +124,7 @@ public String toString() { } @Value - private static class Range { + protected static class Range { boolean lowerClosed; @Nullable @@ -222,73 +137,7 @@ private static class Range { @Override public String toString() { - return (lowerClosed ? "[" : "(") + lower + "," + upper + - (upperClosed ? ']' : ')'); - } - } - - public @Nullable String resolve(DownloadOperation> availableVersions) throws MavenDownloadingException { - if (selected == null) { - selected = cacheResolved(availableVersions); - } - return selected; - } - - private @Nullable String cacheResolved(DownloadOperation> availableVersions) throws MavenDownloadingException { - String nearestSoftRequirement = null; - VersionRequirement next = this; - VersionRequirement nearestHardRequirement = null; - - while (next != null) { - VersionSpec spec = next.versionSpec; - if (spec instanceof DirectRequirement) { - // dependencies defined in the project POM always win - return ((DirectRequirement) spec).getVersion(); - } else if (spec instanceof SoftRequirement) { - nearestSoftRequirement = ((SoftRequirement) spec).version; - } else { - nearestHardRequirement = next; - } - next = next.nearer; - } - - if (nearestHardRequirement == null) { - return nearestSoftRequirement; - } - VersionSpec hardRequirement = nearestHardRequirement.versionSpec; - Version latest = null; - for (String availableVersion : availableVersions.call()) { - Version version = new Version(availableVersion); - - if ((hardRequirement instanceof DynamicVersion && ((DynamicVersion) hardRequirement).matches(version)) || - (hardRequirement instanceof RangeSet && ((RangeSet) hardRequirement).matches(version))) { - - if (latest == null || version.compareTo(latest) > 0) { - latest = version; - } - } - } - - if (latest == null) { - // No version matches the hard requirement. - return null; - } - - return latest.toString(); - } - - public @Nullable String resolve(GroupArtifact groupArtifact, MavenPomDownloader downloader, List repositories) throws MavenDownloadingException { - return resolve(() -> { - MavenMetadata metadata = downloader.downloadMetadata(groupArtifact, null, repositories); - return metadata.getVersioning().getVersions(); - }); - } - - private static class PrintingErrorListener extends BaseErrorListener { - @Override - public void syntaxError(Recognizer recognizer, Object offendingSymbol, - int line, int charPositionInLine, String msg, RecognitionException e) { - System.out.printf("Syntax error at line %d:%d %s%n", line, charPositionInLine, msg); + return (lowerClosed ? "[" : "(") + lower + "," + upper + (upperClosed ? ']' : ')'); } } } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenResolutionResult.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenResolutionResult.java index 5da040a529..09f74059ff 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenResolutionResult.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenResolutionResult.java @@ -180,13 +180,17 @@ public void unsafeSetModules(@Nullable List modules) { private static final Scope[] RESOLVE_SCOPES = new Scope[]{Scope.Compile, Scope.Runtime, Scope.Test, Scope.Provided}; public MavenResolutionResult resolveDependencies(MavenPomDownloader downloader, ExecutionContext ctx) throws MavenDownloadingExceptions { + return resolveDependencies(downloader, ResolutionStrategy.NEAREST_WINS, ctx); + } + + public MavenResolutionResult resolveDependencies(MavenPomDownloader downloader, ResolutionStrategy resolutionStrategy, ExecutionContext ctx) throws MavenDownloadingExceptions { Map> dependencies = new HashMap<>(); MavenDownloadingExceptions exceptions = null; Map> exceptionsInLowerScopes = new HashMap<>(); for (Scope scope : RESOLVE_SCOPES) { try { - dependencies.put(scope, pom.resolveDependencies(scope, downloader, ctx)); + dependencies.put(scope, pom.resolveDependencies(scope, downloader, resolutionStrategy, ctx)); } catch (MavenDownloadingExceptions e) { for (MavenDownloadingException exception : e.getExceptions()) { if (exceptionsInLowerScopes.computeIfAbsent(new GroupArtifact( diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ProfileActivation.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ProfileActivation.java index 441cf922fd..632979d368 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ProfileActivation.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ProfileActivation.java @@ -67,7 +67,7 @@ private boolean isActiveByJdk() { } try { - return version.equals(VersionRequirement.fromVersion(jdk, 0).resolve(() -> singletonList(version))); + return version.equals(VersionRequirement.fromVersion(jdk, ResolutionStrategy.NEAREST_WINS, 0).resolve(() -> singletonList(version))); } catch (MavenDownloadingException e) { // unreachable return false; diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolutionStrategy.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolutionStrategy.java new file mode 100644 index 0000000000..d8263bf26d --- /dev/null +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolutionStrategy.java @@ -0,0 +1,6 @@ +package org.openrewrite.maven.tree; + +public enum ResolutionStrategy { + NEAREST_WINS, //Maven style, where the nearest dependency in the tree is used + NEWEST_WINS //Gradle style, where the latest version is used +} 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 e0157070e4..c0a99fb1a4 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 @@ -29,6 +29,7 @@ import lombok.experimental.NonFinal; import org.jspecify.annotations.Nullable; import org.openrewrite.ExecutionContext; +import org.openrewrite.Validated; import org.openrewrite.internal.ListUtils; import org.openrewrite.internal.PropertyPlaceholderHelper; import org.openrewrite.maven.MavenDownloadingException; @@ -41,6 +42,8 @@ import org.openrewrite.maven.tree.ManagedDependency.Defined; import org.openrewrite.maven.tree.ManagedDependency.Imported; import org.openrewrite.maven.tree.Plugin.Execution; +import org.openrewrite.semver.Semver; +import org.openrewrite.semver.VersionComparator; import java.util.*; import java.util.function.Function; @@ -551,7 +554,7 @@ private Pom resolveParentPom(Pom pom) throws MavenDownloadingException { throw new MavenParsingException("Parent version must always specify a version " + gav); } - VersionRequirement newRequirement = VersionRequirement.fromVersion(gav.getVersion(), 0); + VersionRequirement newRequirement = VersionRequirement.fromVersion(gav.getVersion(), ResolutionStrategy.NEAREST_WINS, 0); GroupArtifact ga = new GroupArtifact(gav.getGroupId(), gav.getArtifactId()); String newRequiredVersion = newRequirement.resolve(ga, downloader, getRepositories()); if (newRequiredVersion == null) { @@ -967,11 +970,17 @@ private boolean isAlreadyResolved(GroupArtifactVersion groupArtifactVersion, Lis } public List resolveDependencies(Scope scope, MavenPomDownloader downloader, ExecutionContext ctx) throws MavenDownloadingExceptions { - return resolveDependencies(scope, new HashMap<>(), downloader, ctx); + return resolveDependencies(scope, downloader, ResolutionStrategy.NEAREST_WINS, ctx); + } + + public List resolveDependencies(Scope scope, MavenPomDownloader downloader, ResolutionStrategy resolutionStrategy, ExecutionContext ctx) throws MavenDownloadingExceptions { + return resolveDependencies(scope, new HashMap<>(), downloader, resolutionStrategy, ctx); } public List resolveDependencies(Scope scope, Map requirements, - MavenPomDownloader downloader, ExecutionContext ctx) throws MavenDownloadingExceptions { + MavenPomDownloader downloader, ResolutionStrategy resolutionStrategy, + ExecutionContext ctx) throws MavenDownloadingExceptions { + MavenDownloadingExceptions exceptions = null; List dependencies = new ArrayList<>(); Map rootDependencies = new HashMap<>(); @@ -979,13 +988,55 @@ public List resolveDependencies(Scope scope, Map validatedComparator = Semver.validate(previousResolvedVersion, null); + if (validatedComparator.isValid() && validatedComparator.getValue() != null) { + if (validatedComparator.getValue().compare(null, previousResolvedVersion, lastResolvedVersion) < 0) { + rootDependencies.put(key, value); + } + } else { + throw new MavenParsingException("Could not resolve version for [" + key + "]"); + } + } + } } } - MavenDownloadingExceptions exceptions = null; int depth = 0; Collection dependenciesAtDepth = rootDependencies.values(); while (!dependenciesAtDepth.isEmpty()) { @@ -1012,7 +1063,7 @@ public List resolveDependencies(Scope scope, Map resolveDependencies(Scope scope, Map resolveDependencies(Scope scope, Map """).toList().getFirst(); MavenResolutionResult resolutionResult = doc.getMarkers().findFirst(MavenResolutionResult.class).orElseThrow(); - resolutionResult = resolutionResult.resolveDependencies(new MavenPomDownloader(emptyMap(), new InMemoryExecutionContext(), null, null), new InMemoryExecutionContext()); + resolutionResult = resolutionResult.resolveDependencies(new MavenPomDownloader(emptyMap(), new InMemoryExecutionContext(), null, null), ResolutionStrategy.NEAREST_WINS, new InMemoryExecutionContext()); List deps = resolutionResult.getDependencies().get(Scope.Compile); assertThat(deps).hasSize(35); } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/internal/VersionRequirementTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/internal/VersionRequirementTest.java index dc9d636a62..a22fd723d5 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/internal/VersionRequirementTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/internal/VersionRequirementTest.java @@ -15,54 +15,96 @@ */ package org.openrewrite.maven.internal; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.openrewrite.maven.MavenDownloadingException; +import org.openrewrite.maven.tree.ResolutionStrategy; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; class VersionRequirementTest { - private Iterable available() { + static Iterable available() { return List.of("1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); } - @Test - void rangeSet() throws MavenDownloadingException { - assertThat(VersionRequirement.fromVersion("[1,11)", 0).resolve(this::available)) - .isEqualTo("10"); - } + @Nested + class NearestWins { - @Test - void multipleSoftRequirements() throws MavenDownloadingException { - assertThat(VersionRequirement.fromVersion("1", 1).addRequirement("2").resolve(this::available)) - .isEqualTo("1"); - } + @Test + void rangeSet() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("[1,11)", ResolutionStrategy.NEAREST_WINS, 0).resolve(VersionRequirementTest::available)) + .isEqualTo("10"); + } - @Test - void softRequirementThenHardRequirement() throws MavenDownloadingException { - assertThat(VersionRequirement.fromVersion("1", 1).addRequirement("[1,11]") - .resolve(this::available)) - .isEqualTo("10"); - } + @Test + void multipleSoftRequirements() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("1", ResolutionStrategy.NEAREST_WINS, 1).addRequirement("2").resolve(VersionRequirementTest::available)) + .isEqualTo("1"); + } - @Test - void hardRequirementThenSoftRequirement() throws MavenDownloadingException { - assertThat(VersionRequirement.fromVersion("[1,11]", 1).addRequirement("1") - .resolve(this::available)) - .isEqualTo("10"); - } + @Test + void softRequirementThenHardRequirement() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("1", ResolutionStrategy.NEAREST_WINS, 1).addRequirement("[1,11]") + .resolve(VersionRequirementTest::available)) + .isEqualTo("10"); + } + + @Test + void hardRequirementThenSoftRequirement() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("[1,11]", ResolutionStrategy.NEAREST_WINS, 1).addRequirement("1") + .resolve(VersionRequirementTest::available)) + .isEqualTo("10"); + } + + @Test + void nearestRangeWins() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("[1,2]", ResolutionStrategy.NEAREST_WINS, 1).addRequirement("[9,10]") + .resolve(VersionRequirementTest::available)) + .isEqualTo("2"); + } - @Test - void nearestRangeWins() throws MavenDownloadingException { - assertThat(VersionRequirement.fromVersion("[1,2]", 1).addRequirement("[9,10]") - .resolve(this::available)) - .isEqualTo("2"); + @Test + void emptyUnboundedRange() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("(,)", ResolutionStrategy.NEAREST_WINS, 0).resolve(VersionRequirementTest::available)) + .isEqualTo("10"); + } } - @Test - void emptyUnboundedRange() throws MavenDownloadingException { - assertThat(VersionRequirement.fromVersion("(,)", 0).resolve(this::available)) - .isEqualTo("10"); + @Nested + class NewestWins { + + @Test + void rangeSet() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("[1,11)", ResolutionStrategy.NEWEST_WINS, 0).resolve(VersionRequirementTest::available)) + .isEqualTo("10"); + } + + @Test + void multipleSpecificVersions() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("1", ResolutionStrategy.NEWEST_WINS, 0).addRequirement("2").resolve(VersionRequirementTest::available)) + .isEqualTo("2"); + } + + @Test + void versionAndRange() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("1", ResolutionStrategy.NEWEST_WINS, 0).addRequirement("[1,11]") + .resolve(VersionRequirementTest::available)) + .isEqualTo("10"); + } + + @Test + void highestRangeWins() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("[1,2]", ResolutionStrategy.NEWEST_WINS, 0).addRequirement("[9,10]") + .resolve(VersionRequirementTest::available)) + .isEqualTo("10"); + } + + @Test + void emptyUnboundedRange() throws MavenDownloadingException { + assertThat(VersionRequirement.fromVersion("(,)", ResolutionStrategy.NEWEST_WINS, 0).resolve(VersionRequirementTest::available)) + .isEqualTo("10"); + } } } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/tree/MavenResolutionResultTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/MavenResolutionResultTest.java new file mode 100644 index 0000000000..acb64e09d7 --- /dev/null +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/MavenResolutionResultTest.java @@ -0,0 +1,387 @@ +package org.openrewrite.maven.tree; + +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.maven.MavenExecutionContextView; +import org.openrewrite.maven.MavenSettings; +import org.openrewrite.maven.internal.MavenPomDownloader; +import org.openrewrite.maven.internal.RawPom; + +import java.io.ByteArrayInputStream; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.Tree.randomId; + +class MavenResolutionResultTest { + @Nested + class ResolveDependencies { + + @Nested + // This is the strategy that Maven normally uses + class NearestWins { + + @CsvSource({"2.15.0,2.13.0", "2.13.0,2.15.0", "2.13.0,2.13.0"}) + @ParameterizedTest + void latestMentionedWinsResolution(String firstVersion, String secondVersion) { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-core + %s + + + com.fasterxml.jackson.core + jackson-core + %s + + + + """.formatted(firstVersion, secondVersion), ResolutionStrategy.NEAREST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly(secondVersion); + } + + @CsvSource(value = {"[2.13.0,2.15.1);[2.10.0,2.13.0];2.13.0", "(2.10.0,2.13.0];[1.0.0,2.15.1);2.15.0", "[2.13.0];[2.13.0];2.13.0"}, delimiter = ';') + @ParameterizedTest + void canHandleRanges(String firstVersion, String secondVersion, String expected) { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-core + %s + + + com.fasterxml.jackson.core + jackson-core + %s + + + + """.formatted(firstVersion, secondVersion), ResolutionStrategy.NEAREST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly(expected); + } + + @CsvSource({"2.15.0,2.13.0", "2.13.0,2.15.0", "2.13.0,2.13.0"}) + @ParameterizedTest + void canHandlePropertyVersions(String firstVersion, String secondVersion) { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + %s + %s + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version.first} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version.second} + + + + """.formatted(firstVersion, secondVersion), ResolutionStrategy.NEAREST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly(secondVersion); + } + + @Test + void depth0First() { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + + + + org.openrewrite + rewrite-java + 7.0.0 + + + com.fasterxml.jackson.core + jackson-core + 2.12.0 + + + + """, ResolutionStrategy.NEAREST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly("2.12.0"); + } + + @Test + void shallowestFirst() { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + + + + org.openrewrite + rewrite-java + 7.0.0 + + + + org.springframework.boot + spring-boot-starter-web + 3.3.9 + + + + """, ResolutionStrategy.NEAREST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-annotations", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly("2.12.2"); + } + } + + @Nested + // This is the strategy that Gradle normally uses, but we're using maven poms to have reusability in the tests as they internally map to the same Dependency classes + class NewestWins { + + @CsvSource({"2.15.0,2.13.0", "2.13.0,2.15.0", "2.15.0,2.15.0"}) + @ParameterizedTest + void newestWinsResolution(String firstVersion, String secondVersion) { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-core + %s + + + com.fasterxml.jackson.core + jackson-core + %s + + + + """.formatted(firstVersion, secondVersion), ResolutionStrategy.NEWEST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly("2.15.0"); + } + + @CsvSource(value = {"[2.13.0,2.15.1);[2.10.0,2.13.0];2.15.0", "(2.10.0,2.13.0];[1.0.0,2.15.1);2.15.0", "[2.13.0];[2.13.0];2.13.0"}, delimiter = ';') + @ParameterizedTest + void canHandleRanges(String firstVersion, String secondVersion, String expected) { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + + + com.fasterxml.jackson.core + jackson-core + %s + + + com.fasterxml.jackson.core + jackson-core + %s + + + + """.formatted(firstVersion, secondVersion), ResolutionStrategy.NEWEST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly(expected); + } + + @CsvSource({"2.15.0,2.13.0", "2.13.0,2.15.0", "2.15.0,2.15.0"}) + @ParameterizedTest + void canHandlePropertyVersions(String firstVersion, String secondVersion) { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + %s + %s + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version.first} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version.second} + + + + """.formatted(firstVersion, secondVersion), ResolutionStrategy.NEWEST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly("2.15.0"); + } + + @Test + void transitiveOverDirect() { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + + + + org.openrewrite + rewrite-java + 7.0.0 + + + com.fasterxml.jackson.core + jackson-core + 2.12.0 + + + + """, ResolutionStrategy.NEWEST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-core", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly("2.12.2"); + } + + @Test + void newestVersionWins() { + MavenResolutionResult result = resolvePom(""" + + com.mycompany.app + my-app + 1 + + + + + org.openrewrite + rewrite-java + 7.0.0 + + + + org.springframework.boot + spring-boot-starter-web + 3.3.9 + + + + """, ResolutionStrategy.NEWEST_WINS); + + assertThat(result.findDependencies("com.fasterxml.jackson.core", "jackson-annotations", Scope.Compile)) + .hasSize(1) + .extracting(ResolvedDependency::getGav) + .extracting(ResolvedGroupArtifactVersion::getVersion) + .containsExactly("2.17.3"); + } + } + + private MavenResolutionResult resolvePom(@Language("xml") String pomContent, ResolutionStrategy resolutionStrategy, String... activeProfiles) { + return resolvePom(pomContent, resolutionStrategy, emptyMap(), activeProfiles); + } + + private MavenResolutionResult resolvePom(@Language("xml") String pomContent, ResolutionStrategy resolutionStrategy, Map properties, String... activeProfiles) { + try { + ExecutionContext ctx = new InMemoryExecutionContext(); + Pom pom = RawPom.parse(new ByteArrayInputStream(pomContent.getBytes()), null).toPom(null, null); + + if (pom.getProperties().isEmpty()) { + pom = pom.withProperties(new LinkedHashMap<>()); + } + pom.getProperties().putAll(properties); + + MavenPomDownloader downloader = new MavenPomDownloader(singletonMap(null, pom), ctx); + + MavenExecutionContextView mavenCtx = MavenExecutionContextView.view(ctx); + MavenSettings sanitizedSettings = mavenCtx.getSettings() == null ? null : mavenCtx.getSettings().withServers(null); + List effectivelyActiveProfiles = Arrays.stream(activeProfiles).toList(); + + ResolvedPom resolvedPom = pom.resolve(effectivelyActiveProfiles, downloader, ctx); + return new MavenResolutionResult(randomId(), null, resolvedPom, emptyList(), null, emptyMap(), sanitizedSettings, effectivelyActiveProfiles, properties) + .resolveDependencies(downloader, resolutionStrategy, ctx); + } catch (Throwable e) { + throw new RuntimeException("Failed to resolve POM", e); + } + } + } +} \ No newline at end of file