All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openrewrite.gradle.UpgradeDependencyVersion Maven / Gradle / Ivy

There is a newer version: 8.42.0
Show newest version
/*
 * Copyright 2022 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.GradleDependencyConfiguration; import org.openrewrite.gradle.marker.GradleProject; import org.openrewrite.gradle.search.FindGradleProject; import org.openrewrite.gradle.util.ChangeStringLiteral; import org.openrewrite.gradle.util.Dependency; import org.openrewrite.gradle.util.DependencyStringNotationConverter; import org.openrewrite.groovy.GroovyIsoVisitor; import org.openrewrite.groovy.GroovyVisitor; import org.openrewrite.groovy.tree.G; import org.openrewrite.internal.ListUtils; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.java.MethodMatcher; import org.openrewrite.java.tree.Expression; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaSourceFile; import org.openrewrite.java.tree.JavaType; import org.openrewrite.marker.Markup; import org.openrewrite.maven.MavenDownloadingException; import org.openrewrite.maven.MavenDownloadingExceptions; import org.openrewrite.maven.internal.MavenPomDownloader; import org.openrewrite.maven.table.MavenMetadataFailures; import org.openrewrite.maven.tree.*; import org.openrewrite.semver.*; import java.util.*; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; @Value @EqualsAndHashCode(callSuper = false) public class UpgradeDependencyVersion extends Recipe { private static final String VERSION_VARIABLE_KEY = "VERSION_VARIABLE"; private static final String NEW_VERSION_KEY = "NEW_VERSION"; @EqualsAndHashCode.Exclude MavenMetadataFailures metadataFailures = new MavenMetadataFailures(this); @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 = "New 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 newVersion; @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; @Override public String getDisplayName() { return "Upgrade Gradle dependency versions"; } @Override public String getInstanceNameSuffix() { return String.format("`%s:%s`", groupId, artifactId); } @Override public String getDescription() { //language=markdown return "Upgrade the version of a dependency in a build.gradle file. " + "Supports updating dependency declarations of various forms:\n" + "* `String` notation: `\"group:artifact:version\"` \n" + "* `Map` notation: `group: 'group', name: 'artifact', version: 'version'`\n" + "Can update version numbers which are defined earlier in the same file in variable declarations."; } @Override public Validated validate() { Validated validated = super.validate(); if (newVersion != null) { validated = validated.and(Semver.validate(newVersion, versionPattern)); } return validated; } private static final String UPDATE_VERSION_ERROR_KEY = "UPDATE_VERSION_ERROR_KEY"; @Override public TreeVisitor getVisitor() { MethodMatcher dependencyDsl = new MethodMatcher("DependencyHandlerSpec *(..)"); DependencyMatcher dependencyMatcher = new DependencyMatcher(groupId, artifactId, null); return Preconditions.check(new FindGradleProject(FindGradleProject.SearchCriteria.Marker), new GroovyVisitor() { @Override public J postVisit(J tree, ExecutionContext ctx) { if (tree instanceof JavaSourceFile) { JavaSourceFile cu = (JavaSourceFile) tree; Map>> variableNames = getCursor().getMessage(VERSION_VARIABLE_KEY); if (variableNames != null) { Optional maybeGp = cu.getMarkers() .findFirst(GradleProject.class); if (!maybeGp.isPresent()) { return cu; } cu = (JavaSourceFile) new UpdateVariable(variableNames, maybeGp.get()).visitNonNull(cu, ctx); } Map> versionUpdates = getCursor().getMessage(NEW_VERSION_KEY); if (versionUpdates != null) { Optional maybeGp = cu.getMarkers() .findFirst(GradleProject.class); if (!maybeGp.isPresent()) { return cu; } GradleProject newGp = maybeGp.get(); for (Map.Entry> gavToConfigurations : versionUpdates.entrySet()) { newGp = replaceVersion(newGp, ctx, gavToConfigurations.getKey(), gavToConfigurations.getValue()); } cu = cu.withMarkers(cu.getMarkers().removeByType(GradleProject.class).add(newGp)); } return cu; } return tree; } @Override public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, ctx); if (dependencyDsl.matches(m)) { List depArgs = m.getArguments(); 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"))) { m = m.withArguments(ListUtils.mapFirst(depArgs, platform -> updateDependency((J.MethodInvocation) platform, ctx))); } } return m; } private J.MethodInvocation updateDependency(J.MethodInvocation method, ExecutionContext ctx) { J.MethodInvocation m = method; m = m.withArguments(ListUtils.map(m.getArguments(), arg -> { if (arg instanceof G.GString) { G.GString gString = (G.GString) arg; List strings = gString.getStrings(); if (strings.size() != 2 || !(strings.get(0) instanceof J.Literal) || !(strings.get(1) instanceof G.GString.Value)) { return arg; } J.Literal groupArtifact = (J.Literal) strings.get(0); G.GString.Value versionValue = (G.GString.Value) strings.get(1); if (!(versionValue.getTree() instanceof J.Identifier) || !(groupArtifact.getValue() instanceof String)) { return arg; } Dependency dep = DependencyStringNotationConverter.parse((String) groupArtifact.getValue()); if (dependencyMatcher.matches(dep.getGroupId(), dep.getArtifactId())) { String versionVariableName = ((J.Identifier) versionValue.getTree()).getSimpleName(); getCursor().dropParentUntil(p -> p instanceof SourceFile) .computeMessageIfAbsent(VERSION_VARIABLE_KEY, v -> new HashMap>>()) .computeIfAbsent(versionVariableName, it -> new HashMap<>()) .computeIfAbsent(new GroupArtifact(dep.getGroupId(), dep.getArtifactId()), it -> new HashSet<>()) .add(method.getSimpleName()); } } else if (arg instanceof J.Literal) { J.Literal literal = (J.Literal) arg; String gav = (String) literal.getValue(); if (gav == null) { getCursor().putMessage(UPDATE_VERSION_ERROR_KEY, new IllegalStateException("Unable to update version")); return arg; } Dependency dep = DependencyStringNotationConverter.parse(gav); if (dependencyMatcher.matches(dep.getGroupId(), dep.getArtifactId()) && dep.getVersion() != null && !dep.getVersion().startsWith("$")) { GradleProject gradleProject = getCursor().firstEnclosingOrThrow(JavaSourceFile.class) .getMarkers() .findFirst(GradleProject.class) .orElseThrow(() -> new IllegalArgumentException("Gradle files are expected to have a GradleProject marker.")); 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)) { 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<>()) .add(method.getSimpleName()); String newGav = dep .withVersion(newVersion) .toStringNotation(); return literal .withValue(newGav) .withValueSource(requireNonNull(literal.getValueSource()).replace(gav, newGav)); } catch (MavenDownloadingException e) { getCursor().putMessage(UPDATE_VERSION_ERROR_KEY, e); } } } return arg; })); Exception err = getCursor().pollMessage(UPDATE_VERSION_ERROR_KEY); if(err != null) { m = Markup.warn(m, err); } List depArgs = m.getArguments(); if (depArgs.size() >= 3 && depArgs.get(0) instanceof G.MapEntry && depArgs.get(1) instanceof G.MapEntry && depArgs.get(2) instanceof G.MapEntry) { Expression groupValue = ((G.MapEntry) depArgs.get(0)).getValue(); Expression artifactValue = ((G.MapEntry) depArgs.get(1)).getValue(); if (!(groupValue instanceof J.Literal) || !(artifactValue instanceof J.Literal)) { return m; } J.Literal groupLiteral = (J.Literal) groupValue; J.Literal artifactLiteral = (J.Literal) artifactValue; //noinspection DataFlowIssue if (!dependencyMatcher.matches((String) groupLiteral.getValue(), (String) artifactLiteral.getValue())) { return m; } G.MapEntry versionEntry = (G.MapEntry) depArgs.get(2); Expression versionExp = versionEntry.getValue(); if (versionExp instanceof J.Literal && ((J.Literal) versionExp).getValue() instanceof String) { GradleProject gradleProject = getCursor().firstEnclosingOrThrow(JavaSourceFile.class) .getMarkers() .findFirst(GradleProject.class) .orElseThrow(() -> new IllegalArgumentException("Gradle files are expected to have a GradleProject marker.")); J.Literal versionLiteral = (J.Literal) versionExp; String version = (String) versionLiteral.getValue(); if (version.startsWith("$")) { return m; } String newVersion; 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); } catch (MavenDownloadingException e) { return e.warn(m); } if (newVersion == null || version.equals(newVersion)) { return m; } List newArgs = new ArrayList<>(3); newArgs.add(depArgs.get(0)); newArgs.add(depArgs.get(1)); newArgs.add(versionEntry.withValue( versionLiteral .withValueSource(requireNonNull(versionLiteral.getValueSource()).replace(version, newVersion)) .withValue(newVersion))); newArgs.addAll(depArgs.subList(3, depArgs.size())); return m.withArguments(newArgs); } else if (versionExp instanceof J.Identifier) { String versionVariableName = ((J.Identifier) versionExp).getSimpleName(); getCursor().dropParentUntil(p -> p instanceof SourceFile) .computeMessageIfAbsent(VERSION_VARIABLE_KEY, v -> new HashMap>>()) .computeIfAbsent(versionVariableName, it -> new HashMap<>()) .computeIfAbsent(new GroupArtifact((String) groupLiteral.getValue(), (String) artifactLiteral.getValue()), it -> new HashSet<>()) .add(m.getSimpleName()); } } return m; } }); } @Value @EqualsAndHashCode(callSuper = true) private class UpdateVariable extends GroovyIsoVisitor { Map>> versionVariableNames; GradleProject gradleProject; @Override public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations.NamedVariable variable, ExecutionContext ctx) { J.VariableDeclarations.NamedVariable v = super.visitVariable(variable, ctx); boolean noneMatch = true; Map> gaToConfigurations = null; for (Map.Entry>> versionVariableNameEntry : versionVariableNames.entrySet()) { if (versionVariableNameEntry.getKey().equals((v.getSimpleName()))) { noneMatch = false; gaToConfigurations = versionVariableNameEntry.getValue(); break; } } if (noneMatch) { return v; } if (!(v.getInitializer() instanceof J.Literal)) { return v; } J.Literal initializer = (J.Literal) v.getInitializer(); if (initializer.getType() != JavaType.Primitive.String) { return v; } String version = (String) initializer.getValue(); if (version == null) { return v; } 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) { 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<>()) .addAll(gaEntry.getValue()); J.Literal newVersionLiteral = ChangeStringLiteral.withStringValue(initializer, newVersion); v = v.withInitializer(newVersionLiteral); } } catch (MavenDownloadingException e) { return e.warn(v); } return v; } @Override public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ctx) { J.Assignment a = super.visitAssignment(assignment, ctx); if (!(a.getVariable() instanceof J.Identifier)) { return a; } J.Identifier identifier = (J.Identifier) a.getVariable(); Map> gaToConfigurations = null; boolean noneMatch = true; for (Map.Entry>> versionVariableNameEntry : versionVariableNames.entrySet()) { if (versionVariableNameEntry.getKey().equals(identifier.getSimpleName())) { noneMatch = false; gaToConfigurations = versionVariableNameEntry.getValue(); break; } } if (noneMatch) { return a; } if (!(a.getAssignment() instanceof J.Literal)) { return a; } J.Literal literal = (J.Literal) a.getAssignment(); if (literal.getType() != JavaType.Primitive.String) { return a; } String version = (String) literal.getValue(); if (version == null) { return a; } 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) { 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<>()) .addAll(gaEntry.getValue()); J.Literal newVersionLiteral = ChangeStringLiteral.withStringValue(literal, newVersion); a = a.withAssignment(newVersionLiteral); } } catch (MavenDownloadingException e) { return e.warn(a); } return a; } } @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) { return gp; } Set remainingConfigurations = new HashSet<>(configurations); remainingConfigurations.remove("classpath"); if (remainingConfigurations.isEmpty()) { return gp; } MavenPomDownloader mpd = new MavenPomDownloader(ctx); Pom pom = mpd.download(gav, null, null, gp.getMavenRepositories()); ResolvedPom resolvedPom = pom.resolve(emptyList(), mpd, gp.getMavenRepositories(), ctx); ResolvedGroupArtifactVersion resolvedGav = resolvedPom.getGav(); List transitiveDependencies = resolvedPom.resolveDependencies(Scope.Runtime, mpd, ctx); Map nameToConfiguration = gp.getNameToConfiguration(); Map newNameToConfiguration = new HashMap<>(nameToConfiguration.size()); boolean anyChanged = false; for (GradleDependencyConfiguration gdc : nameToConfiguration.values()) { GradleDependencyConfiguration newGdc = gdc; newGdc = newGdc.withRequested(ListUtils.map(gdc.getRequested(), requested -> { if (!Objects.equals(requested.getGroupId(), gav.getGroupId()) || !Objects.equals(requested.getArtifactId(), gav.getArtifactId())) { return requested; } return requested.withGav(gav); })); newGdc = newGdc.withResolved(ListUtils.map(gdc.getResolved(), resolved -> { if (!Objects.equals(resolved.getGroupId(), resolvedGav.getGroupId()) || !Objects.equals(resolved.getArtifactId(), resolvedGav.getArtifactId())) { return resolved; } return resolved.withGav(resolvedGav) .withDependencies(transitiveDependencies); })); anyChanged |= newGdc != gdc; newNameToConfiguration.put(newGdc.getName(), newGdc); } if (anyChanged) { gp = gp.withNameToConfiguration(newNameToConfiguration); } } catch (MavenDownloadingException | MavenDownloadingExceptions e) { return gp; } return gp; } }