Skip to content

Convert ChangeDependency to ScanningRecipe to preserve GString/StringTemplate versions#6858

Merged
Jenson3210 merged 15 commits intomainfrom
Jenson3210/scanning-change-dependency
Mar 5, 2026
Merged

Convert ChangeDependency to ScanningRecipe to preserve GString/StringTemplate versions#6858
Jenson3210 merged 15 commits intomainfrom
Jenson3210/scanning-change-dependency

Conversation

@Jenson3210
Copy link
Contributor

@Jenson3210 Jenson3210 commented Mar 3, 2026

Summary

  • Convert ChangeDependency from Recipe to ScanningRecipe to reliably handle version variables in GString and Kotlin StringTemplate dependency declarations
  • Simplify ChangeDependency by leveraging the GradleDependency trait's withDeclaredGroupId, withDeclaredArtifactId, and withDeclaredVersion methods
  • Fix bugs in the GradleDependency trait where withDeclaredGroupId/withDeclaredArtifactId on K.StringTemplate collapsed the template to a literal (destroying version interpolation), and withDeclaredVersion on G.GString converted to J.Literal (causing the Groovy printer to add unwanted parentheses)

Why ScanningRecipe?

When a dependency uses a version variable (e.g., implementation "group:artifact:${springBootVersion}"), we want to:

  1. Preserve the interpolated string structure — only update group/artifact in the literal prefix, keep ${variable} intact
  2. Resolve the new version once and update the variable at its definition site (gradle.properties or local def/val)

This requires two passes: a scan phase to discover which variables need updating and resolve the target version, then a visit phase to apply changes. A single-pass Recipe cannot reliably do this because it processes files independently without shared context.

For dependencies without version variables (plain string literals, map notation, multi-component literals), the scanning phase simply collects no variable information, and the visitor applies changes directly — exactly as the old non-scanning Recipe did. The ScanningRecipe conversion has no behavioral impact on these cases.

This approach was recommended in PR #6830 review feedback as the reliable alternative to the ConcurrentHashMap-based approach that depended on file processing order.

What changed

ChangeDependency.java

  • Converted from Recipe to ScanningRecipe with an Accumulator that maps version variable names to resolved versions (or MavenDownloadingException)
  • Replaced ~400 lines of manual notation-specific code (GString element manipulation, StringTemplate extraction, platform unwrapping) with calls to the GradleDependency trait: withDeclaredGroupId(), withDeclaredArtifactId(), withDeclaredVersion(), getVersionVariable(), getDeclaredVersion(), isPlatform()
  • Scanner detects shared version variables (used by both matching and non-matching dependencies) and falls back to collapsing interpolation to a literal in those cases

GradleDependency.java (trait)

  • Fixed withDeclaredGroupId and withDeclaredArtifactId for K.StringTemplate: now preserves template structure (updates only the literal prefix) instead of collapsing to a plain J.Literal
  • Fixed withDeclaredVersion for G.GString: collapses to a single-element GString instead of J.Literal, which preserves Groovy command expression formatting (implementation "..." stays without parentheses)

Tests

  • 3 new ChangeDependencyTest tests for shared version variable detection and collapse
  • 6 new GradleDependencyTest tests covering withDeclaredGroupId/withDeclaredArtifactId/withDeclaredVersion on GString and K.StringTemplate
  • Updated existing interpolation tests to expect preserved variable references

Test plan

  • All 26 ChangeDependencyTest tests pass (23 existing + 3 new shared variable tests)
  • All 6 new GradleDependencyTest trait tests pass
  • All existing GradleDependencyTest tests pass

Fixes moderneinc/customer-requests#1920

…pendency

When a dependency version comes from a gradle.properties variable,
ChangeDependency should preserve the GString/StringTemplate structure
and update the property value instead of collapsing to a literal.
… preservation

When a dependency version comes from a gradle.properties variable (e.g.,
"group:artifact:${springBootVersion}"), ChangeDependency now preserves the
interpolated string structure instead of collapsing it to a literal with a
pinned version. The version property is updated in gradle.properties.

This uses the ScanningRecipe pattern (consistent with UpgradeDependencyVersion
and UpgradeTransitiveDependencyVersion) to:

1. Scan phase: Collect property keys from gradle.properties and identify
   version variable names used in GString/StringTemplate dependencies
2. Edit phase: Update group:artifact prefix in the interpolated string while
   preserving the variable reference, and update the property value in
   gradle.properties

For local def variables (not backed by gradle.properties), the existing
behavior of collapsing to a string literal is preserved.

Fixes moderneinc/customer-requests#1920
…le.properties

Always preserve interpolated dependency strings regardless of where the version
variable is defined. Local def/val variable declarations are now updated in-place
alongside gradle.properties entries, removing the previous distinction that only
handled gradle.properties-backed variables.
Follow the same pattern as UpgradeDependencyVersion: store the
exception in the accumulator during the scanning phase, then check for
it in the visitor phase and apply Markup.warn() to the affected tree
nodes (variable declarations and gradle.properties entries).
Match the UpgradeDependencyVersion pattern: use a JavaVisitor with
isAcceptable to filter to .gradle/.gradle.kts files instead of a
raw TreeVisitor wrapping a separate JavaIsoVisitor. This removes the
manual instanceof checks and nested visitor, making the scanner a
direct participant in the visit tree.
When a version variable (e.g. springVersion) is used by multiple
dependencies but only some match the old group:artifact, updating the
variable would break unrelated dependencies. The scanner now tracks
ALL dependencies using each version variable. In the visitor, if any
usage doesn't match oldGroupId:oldArtifactId, the GString/StringTemplate
is collapsed to a plain literal with the resolved version instead of
preserving the interpolation.
Replace manual notation-specific code in scanner and visitor with trait
methods: getVersionVariable(), withDeclaredGroupId(), withDeclaredArtifactId(),
withDeclaredVersion(), getDeclaredVersion(), getConfigurationName().

Fix trait bugs: K.StringTemplate withDeclaredGroupId/withDeclaredArtifactId
now preserve template structure instead of collapsing to literal, and
GString withDeclaredVersion preserves command expression formatting.
- Revert GString withDeclaredVersion to convert to J.Literal (original
  intent), update test expectations for parenthesized output
- Remove redundant version resolution in visitor — scanner already
  resolves and records the version
…sion

Converting GString to J.Literal causes the Groovy printer to add
parentheses around the argument, breaking command expression formatting.
Collapsing to a single-element GString preserves the original formatting.
Test that withDeclaredGroupId and withDeclaredArtifactId preserve
GString and K.StringTemplate structure (version interpolation kept),
and that withDeclaredVersion correctly collapses interpolated strings
to concrete values.
@Jenson3210 Jenson3210 marked this pull request as ready for review March 4, 2026 13:06
@github-project-automation github-project-automation bot moved this from In Progress to Ready to Review in OpenRewrite Mar 5, 2026
@Jenson3210 Jenson3210 merged commit e7b0d17 into main Mar 5, 2026
1 check passed
@Jenson3210 Jenson3210 deleted the Jenson3210/scanning-change-dependency branch March 5, 2026 10:23
@github-project-automation github-project-automation bot moved this from Ready to Review to Done in OpenRewrite Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants