Skip to content

Commit 5a28936

Browse files
authored
UpgradeDependencyVersion (Gradle): respect shared version variables (#7491)
* Add failing reproducers for shared-variable corruption (customer-requests #2207) * Do not corrupt shared version variables when new version does not resolve for all sharers * Simplify scanner: single trait walk, drop redundant gatherVariables and dual-write
1 parent 550c442 commit 5a28936

3 files changed

Lines changed: 267 additions & 75 deletions

File tree

rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeDependencyVersion.java

Lines changed: 117 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,21 @@ public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) {
164164
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
165165
J.MethodInvocation m = (J.MethodInvocation) super.visitMethodInvocation(method, ctx);
166166

167-
// Check for any dependency (single or varargs with literal strings)
167+
// Resolve the multi-dependency once and apply the recipe's matcher inline. The
168+
// unfiltered pass also records variable usages for unmatched neighbours so a
169+
// shared variable backing dependencies the upgrade does not target is detected
170+
// before we corrupt those neighbours.
168171
GradleMultiDependency.matcher()
169-
.matcher(new DependencyMatcher(groupId, artifactId, getVersionComparator()))
170172
.get(getCursor())
171-
.ifPresent(multiDependency ->
172-
multiDependency.forEach(dep -> scanDependency(dep, ctx)));
173+
.ifPresent(multiDependency -> {
174+
DependencyMatcher matcher = new DependencyMatcher(groupId, artifactId, getVersionComparator());
175+
multiDependency.forEach(dep -> {
176+
trackVariableUsage(dep);
177+
if (matcher.matches(dep.getGroupId(), dep.getArtifactId())) {
178+
scanDependency(dep, ctx);
179+
}
180+
});
181+
});
173182

174183
new SpringDependencyManagementPluginEntry.Matcher()
175184
.groupId(groupId)
@@ -179,6 +188,26 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx)
179188
return m;
180189
}
181190

191+
private void trackVariableUsage(GradleDependency gradleDependency) {
192+
String versionVariableName = gradleDependency.getVersionVariable();
193+
if (versionVariableName == null) {
194+
return;
195+
}
196+
String depGroupId = gradleDependency.getGroupId();
197+
String depArtifactId = gradleDependency.getArtifactId();
198+
if (depGroupId == null || depArtifactId == null) {
199+
return;
200+
}
201+
GroupArtifact ga = new GroupArtifact(depGroupId, depArtifactId);
202+
Set<String> configs = acc.variableNames
203+
.computeIfAbsent(versionVariableName, k -> new HashMap<>())
204+
.computeIfAbsent(ga, k -> new HashSet<>());
205+
String configName = gradleDependency.getConfigurationName();
206+
if (configName != null) {
207+
configs.add(configName);
208+
}
209+
}
210+
182211
private void scanSpringDependencyManagementEntry(SpringDependencyManagementPluginEntry entry, ExecutionContext ctx) {
183212
String entryGroup = entry.getGroup();
184213
for (String entryArtifact : entry.getArtifacts()) {
@@ -219,7 +248,6 @@ private void scanSpringDependencyManagementEntry(SpringDependencyManagementPlugi
219248
* Scans a single dependency and records its information for later processing.
220249
*/
221250
private void scanDependency(GradleDependency gradleDependency, ExecutionContext ctx) {
222-
gatherVariables(gradleDependency);
223251
String groupId = gradleDependency.getGroupId();
224252
String artifactId = gradleDependency.getArtifactId();
225253

@@ -264,25 +292,6 @@ private boolean shouldResolveVersion(String declaredGroupId, String declaredArti
264292
new DependencyMatcher(groupId, artifactId, null).matches(declaredGroupId, declaredArtifactId);
265293
}
266294

267-
/**
268-
* Gathers version variable names for dependencies
269-
*/
270-
private void gatherVariables(GradleDependency gradleDependency) {
271-
String versionVariableName = gradleDependency.getVersionVariable();
272-
if (versionVariableName == null) {
273-
return;
274-
}
275-
276-
String groupId = gradleDependency.getGroupId();
277-
String artifactId = gradleDependency.getArtifactId();
278-
279-
if (shouldResolveVersion(groupId, artifactId)) {
280-
J.MethodInvocation method = gradleDependency.getTree();
281-
acc.variableNames.computeIfAbsent(versionVariableName, it -> new HashMap<>())
282-
.computeIfAbsent(new GroupArtifact(groupId, artifactId), it -> new HashSet<>())
283-
.add(method.getSimpleName());
284-
}
285-
}
286295
};
287296
}
288297

@@ -378,19 +387,15 @@ public Properties visitFile(Properties.File file, ExecutionContext ctx) {
378387
@Override
379388
public org.openrewrite.properties.tree.Properties visitEntry(Properties.Entry entry, ExecutionContext ctx) {
380389
if (acc.versionPropNameToGA.containsKey(entry.getKey())) {
381-
GroupArtifact ga = acc.versionPropNameToGA.get(entry.getKey()).keySet().stream().findFirst().orElse(null);
382-
if (ga == null || !dependencyMatcher.matches(ga.getGroupId(), ga.getArtifactId())) {
383-
return entry;
384-
}
385-
Object result = acc.gaToNewVersion.get(ga);
386-
if (result == null || result instanceof Exception) {
390+
String agreedVersion = safeUpdatedVersion(entry.getKey(), dependencyMatcher, acc, null, ctx);
391+
if (agreedVersion == null) {
387392
return entry;
388393
}
389394
VersionComparator versionComparator = getVersionComparator();
390395
if (versionComparator == null) {
391396
return entry;
392397
}
393-
Optional<String> finalVersion = versionComparator.upgrade(entry.getValue().getText(), singletonList((String) result));
398+
Optional<String> finalVersion = versionComparator.upgrade(entry.getValue().getText(), singletonList(agreedVersion));
394399
return finalVersion.map(v -> entry.withValue(entry.getValue().withText(v))).orElse(entry);
395400
}
396401
return entry;
@@ -401,6 +406,64 @@ public org.openrewrite.properties.tree.Properties visitEntry(Properties.Entry en
401406
return Semver.validate(StringUtils.isBlank(newVersion) ? "latest.release" : newVersion, versionPattern).getValue();
402407
}
403408

409+
/**
410+
* Returns the agreed-upon resolved version when it is safe to update the shared variable,
411+
* or null otherwise. A variable can only be updated when every dependency using it is
412+
* targeted by the recipe's matcher AND every targeted dependency resolves to the same
413+
* concrete version. Otherwise the matched dependency is detached to a literal version
414+
* via {@code updateDependency} and the variable is left untouched so other neighbours
415+
* sharing it stay on a working version.
416+
*/
417+
private @Nullable String safeUpdatedVersion(
418+
String varName,
419+
DependencyMatcher dependencyMatcher,
420+
DependencyVersionState acc,
421+
@Nullable GradleProject gradleProject,
422+
ExecutionContext ctx) {
423+
Map<GroupArtifact, Set<String>> usages = acc.variableNames.get(varName);
424+
if (usages == null || usages.isEmpty()) {
425+
return null;
426+
}
427+
DependencyVersionSelector selector = null;
428+
String agreedVersion = null;
429+
for (Map.Entry<GroupArtifact, Set<String>> entry : usages.entrySet()) {
430+
GroupArtifact ga = entry.getKey();
431+
if (!dependencyMatcher.matches(ga.getGroupId(), ga.getArtifactId())) {
432+
return null;
433+
}
434+
Object cached = acc.gaToNewVersion.get(ga);
435+
if (cached instanceof Exception) {
436+
return null;
437+
}
438+
String selected;
439+
if (cached instanceof String) {
440+
selected = (String) cached;
441+
} else {
442+
// Not cached: happens for GAs the matcher targets but which were not encountered
443+
// in a regular dependencies {} block (only via a shared variable). Resolve now.
444+
Set<String> configs = entry.getValue();
445+
String configName = configs == null ? null : configs.stream().findFirst().orElse(null);
446+
if (selector == null) {
447+
selector = new DependencyVersionSelector(metadataFailures, gradleProject, null);
448+
}
449+
try {
450+
selected = selector.select(ga, configName, newVersion, versionPattern, ctx);
451+
} catch (MavenDownloadingException e) {
452+
return null;
453+
}
454+
}
455+
if (selected == null) {
456+
return null;
457+
}
458+
if (agreedVersion == null) {
459+
agreedVersion = selected;
460+
} else if (!agreedVersion.equals(selected)) {
461+
return null;
462+
}
463+
}
464+
return agreedVersion;
465+
}
466+
404467
@RequiredArgsConstructor
405468
private class UpdateGradle extends JavaVisitor<ExecutionContext> {
406469
final DependencyVersionState acc;
@@ -559,40 +622,19 @@ public J postVisit(J tree, ExecutionContext ctx) {
559622
if (!acc.variableNames.containsKey(variableName)) {
560623
return prop.getTree();
561624
}
562-
563-
Map.Entry<GroupArtifact, Set<String>> gaWithConfigs =
564-
acc.variableNames.get(variableName).entrySet().iterator().next();
565-
566-
try {
567-
GroupArtifact ga = gaWithConfigs.getKey();
568-
DependencyVersionSelector dependencyVersionSelector =
569-
new DependencyVersionSelector(metadataFailures, gradleProject, null);
570-
571-
String selectedVersion;
572-
try {
573-
selectedVersion = dependencyVersionSelector.select(ga, null, newVersion, versionPattern, execCtx);
574-
} catch (MavenDownloadingException e) {
575-
if (!gaWithConfigs.getValue().contains("classpath")) {
576-
throw e;
577-
}
578-
selectedVersion = dependencyVersionSelector.select(ga, "classpath", newVersion, versionPattern, execCtx);
579-
}
580-
if (selectedVersion == null) {
625+
String selectedVersion = safeUpdatedVersion(variableName, dependencyMatcher, acc, gradleProject, execCtx);
626+
if (selectedVersion == null) {
627+
return prop.getTree();
628+
}
629+
VersionComparator versionComparator = getVersionComparator();
630+
if (versionComparator != null) {
631+
Optional<String> finalVersion = versionComparator.upgrade(prop.getValue(), singletonList(selectedVersion));
632+
if (!finalVersion.isPresent()) {
581633
return prop.getTree();
582634
}
583-
VersionComparator versionComparator = getVersionComparator();
584-
if (versionComparator != null) {
585-
Optional<String> finalVersion = versionComparator.upgrade(prop.getValue(), singletonList(selectedVersion));
586-
if (!finalVersion.isPresent()) {
587-
return prop.getTree();
588-
}
589-
selectedVersion = finalVersion.get();
590-
}
591-
return prop.withValue(selectedVersion).getTree();
592-
} catch (MavenDownloadingException e) {
593-
// No change on error
594-
return prop.getTree();
635+
selectedVersion = finalVersion.get();
595636
}
637+
return prop.withValue(selectedVersion).getTree();
596638
})
597639
.visitNonNull(cu, ctx);
598640
}
@@ -640,16 +682,12 @@ public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ex
640682
if (!(a.getVariable() instanceof J.Identifier)) {
641683
return a;
642684
}
643-
Map<GroupArtifact, Set<String>> groupArtifactSetMap = acc.versionPropNameToGA.get("gradle." + a.getVariable());
685+
String varName = "gradle." + a.getVariable();
644686
// Guard to ensure that an unsupported notation doesn't throw an exception
645-
if (groupArtifactSetMap == null) {
687+
if (!acc.versionPropNameToGA.containsKey(varName)) {
646688
return a;
647689
}
648-
GroupArtifact ga = groupArtifactSetMap.entrySet().stream().findFirst().map(Map.Entry::getKey).orElse(null);
649-
if (ga == null) {
650-
return a;
651-
}
652-
String newVersion = (String) acc.gaToNewVersion.get(ga);
690+
String newVersion = safeUpdatedVersion(varName, dependencyMatcher, acc, gradleProject, executionContext);
653691
if (newVersion == null) {
654692
return a;
655693
}
@@ -714,7 +752,16 @@ private J.MethodInvocation updateDependency(GradleDependency dependency, Executi
714752
// Handle variable references
715753
String versionVariable = dependency.getVersionVariable();
716754
if (versionVariable != null) {
717-
// Variable updates are handled separately
755+
if (safeUpdatedVersion(versionVariable, dependencyMatcher, acc, gradleProject, ctx) != null) {
756+
// Variable is safe to update — leave the call site referencing it; postVisit / UpdateProperties will rewrite the variable.
757+
return dependency.getTree();
758+
}
759+
// The variable is shared with another dependency that this upgrade does not target,
760+
// or the new version does not resolve uniformly across all sharers. Detach this
761+
// call site to a literal version so its neighbours stay on a working version.
762+
if (scanResult instanceof String) {
763+
return dependency.withDeclaredVersion((String) scanResult).getTree();
764+
}
718765
return dependency.getTree();
719766
}
720767

rewrite-gradle/src/main/java/org/openrewrite/gradle/trait/GradleDependency.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,12 +1240,28 @@ public GradleDependency withDeclaredVersion(@Nullable String newVersion) {
12401240
updated = m.withArguments(ListUtils.mapFirst(m.getArguments(),
12411241
arg -> ChangeStringLiteral.withStringValue((J.Literal) arg, DependencyNotation.toStringNotation(updatedDep))));
12421242
} else if (dep == null && isMultiComponentDefinition(m.getArguments())) {
1243-
// Multi-component form: update literal version; variable versions are handled by UpdateProperties/UpdateVariable
1244-
if (m.getArguments().size() >= 3 && m.getArguments().get(2) instanceof J.Literal) {
1245-
String currentVersion = (String) ((J.Literal) m.getArguments().get(2)).getValue();
1246-
if (!newVersion.equals(currentVersion)) {
1243+
// Multi-component form: update literal version, or detach a variable reference to a literal.
1244+
if (m.getArguments().size() >= 3) {
1245+
Expression versionArg = m.getArguments().get(2);
1246+
if (versionArg instanceof J.Literal) {
1247+
String currentVersion = (String) ((J.Literal) versionArg).getValue();
1248+
if (!newVersion.equals(currentVersion)) {
1249+
updated = m.withArguments(ListUtils.map(m.getArguments(), (i, arg) ->
1250+
i == 2 ? ChangeStringLiteral.withStringValue((J.Literal) arg, newVersion) : arg));
1251+
}
1252+
} else if (versionArg instanceof J.Identifier) {
1253+
String delimiter = "\"";
1254+
if (m.getArguments().get(1) instanceof J.Literal) {
1255+
String src = ((J.Literal) m.getArguments().get(1)).getValueSource();
1256+
if (src != null && !src.isEmpty()) {
1257+
delimiter = src.substring(0, 1);
1258+
}
1259+
}
1260+
J.Literal replacement = new J.Literal(
1261+
Tree.randomId(), versionArg.getPrefix(), versionArg.getMarkers(),
1262+
newVersion, delimiter + newVersion + delimiter, null, JavaType.Primitive.String);
12471263
updated = m.withArguments(ListUtils.map(m.getArguments(), (i, arg) ->
1248-
i == 2 ? ChangeStringLiteral.withStringValue((J.Literal) arg, newVersion) : arg));
1264+
i == 2 ? replacement : arg));
12491265
}
12501266
}
12511267
}

0 commit comments

Comments
 (0)