@@ -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
0 commit comments