From ce6e1f3ae5cb84fbddf6bf04c52f0ec1ef972c42 Mon Sep 17 00:00:00 2001 From: Jonathan Schneider Date: Mon, 19 Feb 2024 12:59:28 -0500 Subject: [PATCH] Extract DependencyVersionSelector --- .../gradle/AddDependencyVisitor.java | 44 +----- .../openrewrite/gradle/ChangeDependency.java | 63 ++++---- .../gradle/UpgradeDependencyVersion.java | 112 +++++++------- .../UpgradeTransitiveDependencyVersion.java | 107 +++++++++++++ .../dependency/DependencyVersionSelector.java | 141 ++++++++++++++++++ .../openrewrite/gradle/util/Dependency.java | 5 + 6 files changed, 337 insertions(+), 135 deletions(-) create mode 100644 rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeTransitiveDependencyVersion.java create mode 100644 rewrite-gradle/src/main/java/org/openrewrite/gradle/dependency/DependencyVersionSelector.java diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/AddDependencyVisitor.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/AddDependencyVisitor.java index c3028e0a769..36db4837d78 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/AddDependencyVisitor.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/AddDependencyVisitor.java @@ -20,6 +20,7 @@ import org.openrewrite.ExecutionContext; import org.openrewrite.InMemoryExecutionContext; import org.openrewrite.SourceFile; +import org.openrewrite.gradle.dependency.DependencyVersionSelector; import org.openrewrite.gradle.internal.InsertDependencyComparator; import org.openrewrite.gradle.marker.GradleDependencyConfiguration; import org.openrewrite.gradle.marker.GradleProject; @@ -38,7 +39,6 @@ import org.openrewrite.maven.internal.MavenPomDownloader; import org.openrewrite.maven.table.MavenMetadataFailures; import org.openrewrite.maven.tree.*; -import org.openrewrite.semver.*; import org.openrewrite.tree.ParseError; import java.util.*; @@ -231,8 +231,8 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu resolvedVersion = version; } else { try { - resolvedVersion = resolveDependencyVersion(groupId, artifactId, "0", version, versionPattern, gp.getMavenRepositories(), metadataFailures, ctx) - .orElse(null); + resolvedVersion = new DependencyVersionSelector(metadataFailures, gp) + .select(new GroupArtifact(groupId, artifactId), configuration, version, versionPattern, ctx); } catch (MavenDownloadingException e) { return e.warn(m); } @@ -365,42 +365,4 @@ private DependencyStyle autodetectDependencyStyle(List statements) { return string >= map ? DependencyStyle.String : DependencyStyle.Map; } - - public static Optional resolveDependencyVersion(String groupId, String artifactId, String currentVersion, @Nullable String newVersion, @Nullable String versionPattern, - List repositories, @Nullable MavenMetadataFailures metadataFailures, ExecutionContext ctx) throws MavenDownloadingException { - VersionComparator versionComparator = StringUtils.isBlank(newVersion) ? - new LatestRelease(versionPattern) : - requireNonNull(Semver.validate(newVersion, versionPattern).getValue()); - - Optional version; - if (versionComparator instanceof ExactVersion) { - version = versionComparator.upgrade(currentVersion, singletonList(newVersion)); - } else if (versionComparator instanceof LatestPatch && !versionComparator.isValid(currentVersion, currentVersion)) { - // in the case of "latest.patch", a new version can only be derived if the - // current version is a semantic version - return Optional.empty(); - } else { - version = findNewerVersion(groupId, artifactId, currentVersion, versionComparator, repositories, metadataFailures, ctx); - } - return version; - } - - private static Optional findNewerVersion(String groupId, String artifactId, String version, VersionComparator versionComparator, - List repositories, @Nullable MavenMetadataFailures metadataFailures, ExecutionContext ctx) throws MavenDownloadingException { - try { - MavenMetadata mavenMetadata = metadataFailures == null ? - downloadMetadata(groupId, artifactId, repositories, ctx) : - metadataFailures.insertRows(ctx, () -> downloadMetadata(groupId, artifactId, repositories, ctx)); - return versionComparator.upgrade(version, mavenMetadata.getVersioning().getVersions()); - } catch (IllegalStateException e) { - // this can happen when we encounter exotic versions - return Optional.empty(); - } - } - - private static MavenMetadata downloadMetadata(String groupId, String artifactId, List repositories, ExecutionContext ctx) throws MavenDownloadingException { - return new MavenPomDownloader(ctx) - .downloadMetadata(new GroupArtifact(groupId, artifactId), null, - repositories); - } } diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependency.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependency.java index f5e827010c4..c93581ec334 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependency.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependency.java @@ -19,6 +19,7 @@ import lombok.EqualsAndHashCode; import lombok.Value; import org.openrewrite.*; +import org.openrewrite.gradle.dependency.DependencyVersionSelector; import org.openrewrite.gradle.marker.GradleDependencyConfiguration; import org.openrewrite.gradle.marker.GradleProject; import org.openrewrite.gradle.search.FindGradleProject; @@ -34,8 +35,8 @@ import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; import org.openrewrite.maven.MavenDownloadingException; +import org.openrewrite.maven.tree.GroupArtifact; import org.openrewrite.maven.tree.GroupArtifactVersion; -import org.openrewrite.maven.tree.MavenRepository; import org.openrewrite.maven.tree.ResolvedGroupArtifactVersion; import org.openrewrite.semver.DependencyMatcher; import org.openrewrite.semver.Semver; @@ -83,7 +84,7 @@ public class ChangeDependency extends Recipe { @Option(displayName = "Version pattern", description = "Allows version selection to be extended beyond the original Node Semver semantics. So for example," + - "Setting 'version' to \"25-29\" can be paired with a metadata pattern of \"-jre\" to select Guava 29.0-jre", + "Setting 'version' to \"25-29\" can be paired with a metadata pattern of \"-jre\" to select Guava 29.0-jre", example = "-jre", required = false) @Nullable @@ -91,14 +92,15 @@ public class ChangeDependency extends Recipe { @Option(displayName = "Override managed version", description = "If the old dependency has a managed version, this flag can be used to explicitly set the version on the new dependency. " + - "WARNING: No check is done on the NEW dependency to verify if it is managed, it relies on whether the OLD dependency had a managed version. " + - "The default for this flag is `false`.", + "WARNING: No check is done on the NEW dependency to verify if it is managed, it relies on whether the OLD dependency had a managed version. " + + "The default for this flag is `false`.", required = false) @Nullable Boolean overrideManagedVersion; /** * Keeping this constructor just for compatibility purposes + * * @deprecated Use {@link ChangeDependency#ChangeDependency(String, String, String, String, String, String, Boolean)} */ @Deprecated @@ -187,8 +189,8 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Execu if (depArgs.get(0) instanceof J.Literal || depArgs.get(0) instanceof G.GString || depArgs.get(0) instanceof G.MapEntry) { m = updateDependency(m, ctx); } else if (depArgs.get(0) instanceof J.MethodInvocation && - (((J.MethodInvocation) depArgs.get(0)).getSimpleName().equals("platform") || - ((J.MethodInvocation) depArgs.get(0)).getSimpleName().equals("enforcedPlatform"))) { + (((J.MethodInvocation) depArgs.get(0)).getSimpleName().equals("platform") || + ((J.MethodInvocation) depArgs.get(0)).getSimpleName().equals("enforcedPlatform"))) { m = m.withArguments(ListUtils.mapFirst(depArgs, platform -> updateDependency((J.MethodInvocation) platform, ctx))); } @@ -210,13 +212,10 @@ private J.MethodInvocation updateDependency(J.MethodInvocation m, ExecutionConte updated = updated.withArtifactId(newArtifactId); } if (!StringUtils.isBlank(newVersion) && (!StringUtils.isBlank(original.getVersion()) || Boolean.TRUE.equals(overrideManagedVersion))) { - List repositories = "classpath".equals(m.getSimpleName()) ? - gradleProject.getMavenPluginRepositories() : - gradleProject.getMavenRepositories(); String resolvedVersion; try { - resolvedVersion = AddDependencyVisitor.resolveDependencyVersion(updated.getGroupId(), updated.getArtifactId(), "0", newVersion, versionPattern, repositories, null, ctx) - .orElse(null); + resolvedVersion = new DependencyVersionSelector(null, gradleProject) + .select(new GroupArtifact(updated.getGroupId(), updated.getArtifactId()), m.getSimpleName(), newVersion, versionPattern, ctx); } catch (MavenDownloadingException e) { return e.warn(m); } @@ -233,9 +232,11 @@ private J.MethodInvocation updateDependency(J.MethodInvocation m, ExecutionConte } else if (m.getArguments().get(0) instanceof G.GString) { G.GString gstring = (G.GString) depArgs.get(0); List strings = gstring.getStrings(); - if (strings.size() >= 2 && strings.get(0) instanceof J.Literal) { + if (strings.size() >= 2 && strings.get(0) instanceof J.Literal && + ((J.Literal) strings.get(0)).getValue() != null) { + J.Literal literal = (J.Literal) strings.get(0); - Dependency original = DependencyStringNotationConverter.parse((String)literal.getValue()); + Dependency original = DependencyStringNotationConverter.parse((String) requireNonNull(literal.getValue())); if (depMatcher.matches(original.getGroupId(), original.getArtifactId())) { Dependency updated = original; if (!StringUtils.isBlank(newGroupId) && !updated.getGroupId().equals(newGroupId)) { @@ -245,13 +246,10 @@ private J.MethodInvocation updateDependency(J.MethodInvocation m, ExecutionConte updated = updated.withArtifactId(newArtifactId); } if (!StringUtils.isBlank(newVersion)) { - List repositories = "classpath".equals(m.getSimpleName()) ? - gradleProject.getMavenPluginRepositories() : - gradleProject.getMavenRepositories(); String resolvedVersion; try { - resolvedVersion = AddDependencyVisitor.resolveDependencyVersion(updated.getGroupId(), updated.getArtifactId(), "0", newVersion, versionPattern, repositories, null, ctx) - .orElse(null); + resolvedVersion = new DependencyVersionSelector(null, gradleProject) + .select(new GroupArtifact(updated.getGroupId(), updated.getArtifactId()), m.getSimpleName(), newVersion, versionPattern, ctx); } catch (MavenDownloadingException e) { return e.warn(m); } @@ -290,15 +288,19 @@ private J.MethodInvocation updateDependency(J.MethodInvocation m, ExecutionConte } String keyValue = (String) key.getValue(); String valueValue = (String) value.getValue(); - if ("group".equals(keyValue)) { - groupEntry = arg; - groupId = valueValue; - } else if ("name".equals(keyValue)) { - artifactEntry = arg; - artifactId = valueValue; - } else if ("version".equals(keyValue)) { - versionEntry = arg; - version = valueValue; + switch (keyValue) { + case "group": + groupEntry = arg; + groupId = valueValue; + break; + case "name": + artifactEntry = arg; + artifactId = valueValue; + break; + case "version": + versionEntry = arg; + version = valueValue; + break; } } if (groupId == null || artifactId == null) { @@ -317,13 +319,10 @@ private J.MethodInvocation updateDependency(J.MethodInvocation m, ExecutionConte } String updatedVersion = version; if (!StringUtils.isBlank(newVersion) && (!StringUtils.isBlank(version) || Boolean.TRUE.equals(overrideManagedVersion))) { - List repositories = "classpath".equals(m.getSimpleName()) ? - gradleProject.getMavenPluginRepositories() : - gradleProject.getMavenRepositories(); String resolvedVersion; try { - resolvedVersion = AddDependencyVisitor.resolveDependencyVersion(updatedGroupId, updatedArtifactId, "0", newVersion, versionPattern, repositories, null, ctx) - .orElse(null); + resolvedVersion = new DependencyVersionSelector(null, gradleProject) + .select(new GroupArtifact(updatedGroupId, updatedArtifactId), m.getSimpleName(), newVersion, versionPattern, ctx); } catch (MavenDownloadingException e) { return e.warn(m); } 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 1eb10d8e6b8..75b545916b0 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeDependencyVersion.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeDependencyVersion.java @@ -16,8 +16,10 @@ package org.openrewrite.gradle; import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; import lombok.Value; import org.openrewrite.*; +import org.openrewrite.gradle.dependency.DependencyVersionSelector; import org.openrewrite.gradle.marker.GradleDependencyConfiguration; import org.openrewrite.gradle.marker.GradleProject; import org.openrewrite.gradle.search.FindGradleProject; @@ -123,11 +125,14 @@ public Validated validate() { private static final String UPDATE_VERSION_ERROR_KEY = "UPDATE_VERSION_ERROR_KEY"; - @Value public static class DependencyVersionState { Map versionPropNameToGA = new HashMap<>(); - // The value is either a String representing the resolved version or a MavenDownloadingException representing an error during resolution. + + /** + * The value is either a String representing the resolved version + * or a MavenDownloadingException representing an error during resolution. + */ Map gaToNewVersion = new HashMap<>(); } @@ -193,12 +198,16 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) continue; } String keyValue = (String) key.getValue(); - if ("group".equals(keyValue)) { - groupId = valueValue; - } else if ("name".equals(keyValue)) { - artifactId = valueValue; - } else if ("version".equals(keyValue)) { - version = valueValue; + switch (keyValue) { + case "group": + groupId = valueValue; + break; + case "name": + artifactId = valueValue; + break; + case "version": + version = valueValue; + break; } } if (groupId == null || artifactId == null) { @@ -214,10 +223,8 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) return m; } try { - String resolvedVersion = "classpath".equals(m.getSimpleName()) ? - findNewerPluginVersion(groupId, artifactId, "0", gradleProject, ctx) : - findNewerProjectDependencyVersion(groupId, artifactId, "0", gradleProject, ctx); - + String resolvedVersion = new DependencyVersionSelector(metadataFailures, gradleProject) + .select(new GroupArtifact(groupId, artifactId), m.getSimpleName(), newVersion, versionPattern, ctx); acc.versionPropNameToGA.put(versionVariableName, ga); acc.gaToNewVersion.put(ga, resolvedVersion); } catch (MavenDownloadingException e) { @@ -247,10 +254,8 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) continue; } try { - String resolvedVersion = "classpath".equals(m.getSimpleName()) ? - findNewerPluginVersion(dep.getGroupId(), dep.getArtifactId(), "0", gradleProject, ctx) : - findNewerProjectDependencyVersion(dep.getGroupId(), dep.getArtifactId(), "0", gradleProject, ctx); - + String resolvedVersion = new DependencyVersionSelector(metadataFailures, gradleProject) + .select(new GroupArtifact(dep.getGroupId(), dep.getArtifactId()), m.getSimpleName(), newVersion, versionPattern, ctx); acc.versionPropNameToGA.put(versionVariableName, ga); acc.gaToNewVersion.put(ga, resolvedVersion); } catch (MavenDownloadingException e) { @@ -391,21 +396,19 @@ private J.MethodInvocation updateDependency(J.MethodInvocation method, Execution return arg; } - String version = dep.getVersion(); try { - String newVersion = "classpath".equals(method.getSimpleName()) ? - findNewerPluginVersion(dep.getGroupId(), dep.getArtifactId(), version, gradleProject, ctx) : - findNewerProjectDependencyVersion(dep.getGroupId(), dep.getArtifactId(), version, gradleProject, ctx); - if (newVersion == null || version.equals(newVersion)) { + String selectedVersion = new DependencyVersionSelector(metadataFailures, gradleProject) + .select(dep.getGav(), method.getSimpleName(), newVersion, versionPattern, ctx); + if (selectedVersion == null || dep.getVersion().equals(selectedVersion)) { return arg; } getCursor().dropParentUntil(p -> p instanceof SourceFile) .computeMessageIfAbsent(NEW_VERSION_KEY, it -> new HashMap>()) - .computeIfAbsent(new GroupArtifactVersion(dep.getGroupId(), dep.getArtifactId(), newVersion), it -> new HashSet<>()) + .computeIfAbsent(new GroupArtifactVersion(dep.getGroupId(), dep.getArtifactId(), selectedVersion), it -> new HashSet<>()) .add(method.getSimpleName()); String newGav = dep - .withVersion(newVersion) + .withVersion(selectedVersion) .toStringNotation(); return literal .withValue(newGav) @@ -448,15 +451,15 @@ private J.MethodInvocation updateDependency(J.MethodInvocation method, Execution if (version.startsWith("$")) { return m; } - String newVersion; + String selectedVersion; try { - newVersion = "classpath".equals(m.getSimpleName()) ? - findNewerPluginVersion((String) groupLiteral.getValue(), (String) artifactLiteral.getValue(), version, gradleProject, ctx) : - findNewerProjectDependencyVersion((String) groupLiteral.getValue(), (String) artifactLiteral.getValue(), version, gradleProject, ctx); + GroupArtifactVersion gav = new GroupArtifactVersion((String) groupLiteral.getValue(), (String) artifactLiteral.getValue(), version); + selectedVersion = new DependencyVersionSelector(metadataFailures, gradleProject) + .select(gav, m.getSimpleName(), newVersion, versionPattern, ctx); } catch (MavenDownloadingException e) { return e.warn(m); } - if (newVersion == null || version.equals(newVersion)) { + if (selectedVersion == null || version.equals(selectedVersion)) { return m; } List newArgs = new ArrayList<>(3); @@ -464,8 +467,8 @@ private J.MethodInvocation updateDependency(J.MethodInvocation method, Execution newArgs.add(depArgs.get(1)); newArgs.add(versionEntry.withValue( versionLiteral - .withValueSource(requireNonNull(versionLiteral.getValueSource()).replace(version, newVersion)) - .withValue(newVersion))); + .withValueSource(requireNonNull(versionLiteral.getValueSource()).replace(version, selectedVersion)) + .withValue(selectedVersion))); newArgs.addAll(depArgs.subList(3, depArgs.size())); return m.withArguments(newArgs); @@ -486,11 +489,10 @@ private J.MethodInvocation updateDependency(J.MethodInvocation method, Execution ); } - @Value - @EqualsAndHashCode(callSuper = false) + @RequiredArgsConstructor private class UpdateVariable extends GroovyIsoVisitor { - Map>> versionVariableNames; - GradleProject gradleProject; + private final Map>> versionVariableNames; + private final GradleProject gradleProject; @Override public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations.NamedVariable variable, ExecutionContext ctx) { @@ -522,19 +524,19 @@ public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations try { for (Map.Entry> gaEntry : gaToConfigurations.entrySet()) { GroupArtifact ga = gaEntry.getKey(); - String newVersion = findNewerProjectDependencyVersion(ga.getGroupId(), ga.getArtifactId(), version, gradleProject, ctx); - if (newVersion == null) { - newVersion = findNewerPluginVersion(ga.getGroupId(), ga.getArtifactId(), version, gradleProject, ctx); - } - if (newVersion == null) { + GroupArtifactVersion gav = new GroupArtifactVersion(ga.getGroupId(), ga.getArtifactId(), version); + DependencyVersionSelector selector = new DependencyVersionSelector(metadataFailures, gradleProject); + String selectedVersion = Optional.ofNullable(selector.select(gav, null, newVersion, versionPattern, ctx)) + .orElse(selector.select(gav, "classpath", newVersion, versionPattern, ctx)); + if (selectedVersion == null) { return v; } getCursor().dropParentUntil(p -> p instanceof SourceFile) .computeMessageIfAbsent(NEW_VERSION_KEY, m -> new HashMap>()) - .computeIfAbsent(new GroupArtifactVersion(ga.getGroupId(), ga.getArtifactId(), newVersion), it -> new HashSet<>()) + .computeIfAbsent(new GroupArtifactVersion(ga.getGroupId(), ga.getArtifactId(), selectedVersion), it -> new HashSet<>()) .addAll(gaEntry.getValue()); - J.Literal newVersionLiteral = ChangeStringLiteral.withStringValue(initializer, newVersion); + J.Literal newVersionLiteral = ChangeStringLiteral.withStringValue(initializer, selectedVersion); v = v.withInitializer(newVersionLiteral); } } catch (MavenDownloadingException e) { @@ -577,19 +579,19 @@ public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ct try { for (Map.Entry> gaEntry : gaToConfigurations.entrySet()) { GroupArtifact ga = gaEntry.getKey(); - String newVersion = findNewerProjectDependencyVersion(ga.getGroupId(), ga.getArtifactId(), version, gradleProject, ctx); - if (newVersion == null) { - newVersion = findNewerPluginVersion(ga.getGroupId(), ga.getArtifactId(), version, gradleProject, ctx); - } - if (newVersion == null) { + GroupArtifactVersion gav = new GroupArtifactVersion(ga.getGroupId(), ga.getArtifactId(), version); + DependencyVersionSelector selector = new DependencyVersionSelector(metadataFailures, gradleProject); + String selectedVersion = Optional.ofNullable(selector.select(gav, null, newVersion, versionPattern, ctx)) + .orElse(selector.select(gav, "classpath", newVersion, versionPattern, ctx)); + if (selectedVersion == null) { return a; } getCursor().dropParentUntil(p -> p instanceof SourceFile) .computeMessageIfAbsent(NEW_VERSION_KEY, m -> new HashMap>()) - .computeIfAbsent(new GroupArtifactVersion(ga.getGroupId(), ga.getArtifactId(), newVersion), it -> new HashSet<>()) + .computeIfAbsent(new GroupArtifactVersion(ga.getGroupId(), ga.getArtifactId(), selectedVersion), it -> new HashSet<>()) .addAll(gaEntry.getValue()); - J.Literal newVersionLiteral = ChangeStringLiteral.withStringValue(literal, newVersion); + J.Literal newVersionLiteral = ChangeStringLiteral.withStringValue(literal, selectedVersion); a = a.withAssignment(newVersionLiteral); } } catch (MavenDownloadingException e) { @@ -599,20 +601,6 @@ public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ct } } - @Nullable - private String findNewerPluginVersion(String groupId, String artifactId, String version, GradleProject gradleProject, - ExecutionContext ctx) throws MavenDownloadingException { - return AddDependencyVisitor.resolveDependencyVersion(groupId, artifactId, version, newVersion, versionPattern, gradleProject.getMavenPluginRepositories(), metadataFailures, ctx) - .orElse(null); - } - - @Nullable - private String findNewerProjectDependencyVersion(String groupId, String artifactId, String version, GradleProject gradleProject, - ExecutionContext ctx) throws MavenDownloadingException { - return AddDependencyVisitor.resolveDependencyVersion(groupId, artifactId, version, newVersion, versionPattern, gradleProject.getMavenRepositories(), metadataFailures, ctx) - .orElse(null); - } - static GradleProject replaceVersion(GradleProject gp, ExecutionContext ctx, GroupArtifactVersion gav, Set configurations) { try { if (gav.getGroupId() == null || gav.getArtifactId() == null) { diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeTransitiveDependencyVersion.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeTransitiveDependencyVersion.java new file mode 100644 index 00000000000..ee2e3efe99c --- /dev/null +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeTransitiveDependencyVersion.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 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.gradle; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.*; +import org.openrewrite.gradle.marker.GradleProject; +import org.openrewrite.gradle.search.FindGradleProject; +import org.openrewrite.groovy.GroovyVisitor; +import org.openrewrite.groovy.tree.G; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.tree.J; +import org.openrewrite.semver.Semver; + +@Value +@EqualsAndHashCode(callSuper = false) +public class UpgradeTransitiveDependencyVersion extends Recipe { + + @Option(displayName = "Group", + description = "The first part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression.", + example = "com.fasterxml.jackson*") + String groupId; + + @Option(displayName = "Artifact", + description = "The second part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression.", + example = "jackson-module*") + String artifactId; + + @Option(displayName = "Version", + description = "An exact version number or node-style semver selector used to select the version number. " + + "You can also use `latest.release` for the latest available version and `latest.patch` if " + + "the current version is a valid semantic version. For more details, you can look at the documentation " + + "page of [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors). " + + "Defaults to `latest.release`.", + example = "29.X", + required = false) + @Nullable + String version; + + @Option(displayName = "Version pattern", + description = "Allows version selection to be extended beyond the original Node Semver semantics. So for example," + + "Setting 'newVersion' to \"25-29\" can be paired with a metadata pattern of \"-jre\" to select Guava 29.0-jre", + example = "-jre", + required = false) + @Nullable + String versionPattern; + + @Option(displayName = "Strategy", + description = "The technique used to upgrade the dependency.", + valid = {"direct", "resolutionStrategy"}) + String strategy; + + @Override + public String getDisplayName() { + return "Upgrade transitive Gradle dependencies"; + } + + @Override + public String getDescription() { + return "Upgrades the version of a transitive dependency in a Gradle build file. " + + "There are many ways to do this in Gradle, so the mechanism for upgrading a " + + "transitive dependency must be considered carefully depending on your style " + + "of dependency management."; + } + + @Override + public Validated validate() { + Validated validated = super.validate(); + if (version != null) { + validated = validated.and(Semver.validate(version, versionPattern)); + } + return validated; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new FindGradleProject(FindGradleProject.SearchCriteria.Marker), new GroovyVisitor() { + GradleProject gradleProject; + + @Override + public J visitCompilationUnit(G.CompilationUnit cu, ExecutionContext ctx) { + gradleProject = cu.getMarkers().findFirst(GradleProject.class) + .orElseThrow(() -> new IllegalStateException("Unable to find GradleProject marker.")); + + if (strategy.equals("direct")) { + + } + + return super.visitCompilationUnit(cu, ctx); + } + }); + } +} diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/dependency/DependencyVersionSelector.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/dependency/DependencyVersionSelector.java new file mode 100644 index 00000000000..7488336954e --- /dev/null +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/dependency/DependencyVersionSelector.java @@ -0,0 +1,141 @@ +/* + * Copyright 2024 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.gradle.dependency; + +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Incubating; +import org.openrewrite.gradle.marker.GradleProject; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.maven.MavenDownloadingException; +import org.openrewrite.maven.internal.MavenPomDownloader; +import org.openrewrite.maven.table.MavenMetadataFailures; +import org.openrewrite.maven.tree.*; +import org.openrewrite.semver.*; + +import java.util.List; +import java.util.Optional; + +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; + +/** + * Selects versions for new or existing dependencies based on a node-semver selector + * by inspecting available versions in the Maven metadata from a set of Maven repositories. + */ +@Incubating(since = "8.17.0") +@Value +public class DependencyVersionSelector { + @Nullable + MavenMetadataFailures metadataFailures; + + GradleProject gradleProject; + + /** + * Used to select a version for a new dependency that has no prior version. + * + * @param ga The group and artifact of the new dependency. + * @param configuration The configuration to select the version for. The configuration influences + * which set of Maven repositories (either plugin repositories or regular repositories) + * are used to resolve Maven metadata from. + * @param version The version to select, in node-semver format. + * @param versionPattern The version pattern to select, if any. + * @param ctx The execution context, which can influence dependency resolution. + * @return The selected version, if any. + * @throws MavenDownloadingException If there is a problem downloading metadata for the dependency. + */ + @Nullable + public String select(GroupArtifact ga, + String configuration, + @Nullable String version, + @Nullable String versionPattern, + ExecutionContext ctx) throws MavenDownloadingException { + return select( + new GroupArtifactVersion(ga.getGroupId(), ga.getArtifactId(), "0"), + configuration, + // we don't want to select the latest patch in the 0.x line... + "latest.patch".equalsIgnoreCase(version) ? "latest.release" : version, + versionPattern, + ctx + ); + } + + /** + * Used to upgrade a version for a dependency that already has a version. + * + * @param gav The group, artifact, and version of the existing dependency. + * @param configuration The configuration to select the version for. The configuration influences + * which set of Maven repositories (either plugin repositories or regular repositories) + * are used to resolve Maven metadata from. * @param version The version to select, in node-semver format. + * @param versionPattern The version pattern to select, if any. + * @param ctx The execution context, which can influence dependency resolution. + * @return The selected version, if any. + * @throws MavenDownloadingException If there is a problem downloading metadata for the dependency. + */ + @Nullable + public String select(GroupArtifactVersion gav, + String configuration, + @Nullable String version, + @Nullable String versionPattern, + ExecutionContext ctx) throws MavenDownloadingException { + if (gav.getVersion() == null) { + throw new IllegalArgumentException("Version must be specified. Call the select method " + + "that accepts a GroupArtifact instead if there is no " + + "current version."); + } + + VersionComparator versionComparator = StringUtils.isBlank(version) ? + new LatestRelease(versionPattern) : + requireNonNull(Semver.validate(version, versionPattern).getValue()); + + if (versionComparator instanceof ExactVersion) { + return versionComparator.upgrade(gav.getVersion(), singletonList(version)).orElse(null); + } else if (versionComparator instanceof LatestPatch && + !versionComparator.isValid(gav.getVersion(), gav.getVersion())) { + // in the case of "latest.patch", a new version can only be derived if the + // current version is a semantic version + return null; + } else { + return findNewerVersion(gav, configuration, versionComparator, ctx).orElse(null); + } + } + + private Optional findNewerVersion(GroupArtifactVersion gav, + String configuration, + VersionComparator versionComparator, + ExecutionContext ctx) throws MavenDownloadingException { + try { + List repos = "classpath".equals(configuration) ? + gradleProject.getMavenPluginRepositories() : + gradleProject.getMavenRepositories(); + MavenMetadata mavenMetadata = metadataFailures == null ? + downloadMetadata(gav.getGroupId(), gav.getArtifactId(), repos, ctx) : + metadataFailures.insertRows(ctx, () -> downloadMetadata(gav.getGroupId(), gav.getArtifactId(), + repos, ctx)); + return versionComparator.upgrade(requireNonNull(gav.getVersion()), + mavenMetadata.getVersioning().getVersions()); + } catch (IllegalStateException e) { + // this can happen when we encounter exotic versions + return Optional.empty(); + } + } + + private MavenMetadata downloadMetadata(String groupId, String artifactId, List repositories, ExecutionContext ctx) throws MavenDownloadingException { + return new MavenPomDownloader(ctx).downloadMetadata( + new GroupArtifact(groupId, artifactId), null, repositories); + } +} diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/util/Dependency.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/util/Dependency.java index 820b8d50494..9e114832b32 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/util/Dependency.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/util/Dependency.java @@ -19,6 +19,7 @@ import lombok.Value; import lombok.With; import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.maven.tree.GroupArtifactVersion; @Value @With @@ -36,6 +37,10 @@ public class Dependency { @Nullable String ext; + public GroupArtifactVersion getGav() { + return new GroupArtifactVersion(groupId, artifactId, version); + } + public String toStringNotation() { StringBuilder builder = new StringBuilder(); builder.append(groupId).append(':').append(artifactId);