Gradle version catalogs: one model for settings and TOML - #8864
Merged
Merged
Conversation
Adds version catalog support to UpgradeDependencyVersion for catalogs declared in
settings.gradle through dependencyResolutionManagement { versionCatalogs { ... } },
in both the Groovy and Kotlin DSL.
Several libraries commonly share one version declaration through versionRef. When every
library sharing a reference is targeted, the shared declaration is bumped once; when only
some are, the targeted library is detached to its own literal and the reference is left
alone for its remaining members.
Adds traits for library and plugin entries in a gradle/libs.versions.toml catalog, covering string notation and inline tables with either module or group/name coordinates, and preserving the original quote style when rewriting an entry. Adds TomlTableValue and TableRowMatcher to rewrite-toml for the table and key lookups these traits need.
jkschneider
force-pushed
the
gradle-version-catalogs
branch
from
September 12, 2026 14:33
e95da17 to
97441fc
Compare
jkschneider
commented
Sep 12, 2026
jkschneider
commented
Sep 12, 2026
jkschneider
force-pushed
the
gradle-version-catalogs
branch
from
September 12, 2026 17:41
97441fc to
61afa78
Compare
A version catalog has two homes, a `dependencyResolutionManagement { versionCatalogs { ... } }`
block in `settings.gradle(.kts)` and a `gradle/libs.versions.toml` file, and the two preceding
commits each modeled one of them. This makes them the two implementations of one `VersionCatalog`:
`SettingsVersionCatalog` and `TomlVersionCatalog`. `VersionCatalog.Matcher` finds either, in the
way `SpringBean` finds a bean in XML or in a `@Bean` method, so a recipe starts from one trait
visitor for both forms rather than a TOML visitor and a branch inside its script visitor.
Where new versions go is decided once, on `VersionCatalog`, from everything a run is moving: a
shared version declaration changes in place when every library referring to it is moving to the
same version, and otherwise only the libraries that are moving get a version of their own. That
replaces the detach-and-restore of the first commit, which remembered the original sharing in a
marker added during the run. A marker added at run time changes the tree without changing the
source, and nothing carries over between runs in any case. Each implementation supplies the
catalog's contents and three edits, so the TOML side gains the sharing semantics the settings side
had, and `UpgradeDependencyVersion` selects versions for either kind of catalog through the same
loop under one `VersionCatalog.Matcher().asVisitor(...)`.
The entry-level TOML traits keep their role and lose a redundant prefix, becoming
`VersionCatalogLibrary` and `VersionCatalogPlugin` after the catalog's own `[libraries]` and
`[plugins]` tables. They edit their entries directly through `TomlTableValue` rather than by
running the `ChangeValue` and `DeleteKey` recipes over them, which had left `Changed` markers
behind. `TomlTableValue.withKey` renames a property in place, so detaching a `version.ref` into a
`version` keeps the entry's padding and quote style.
A TOML catalog carries no `GradleProject` marker, so the scanner records the root project's if it
is scanned and otherwise the first it sees, and a catalog resolves against that project's
repositories. `GradleSettings` is no help here: its repositories are the ones plugins resolve from.
An exact version needs neither.
A `[plugins]` entry, or a `plugin(alias, id).versionRef(...)` in the settings DSL, can share a version declaration with libraries. Moving that declaration because every library referring to it is moving upgrades the plugin too, which the recipe was never asked to do. `withVersions` now counts plugin referrers, and a declaration one refers to is never moved; the libraries that are moving detach to versions of their own instead. `VersionCatalog.getPluginVersions()` reports them, read from the `[plugins]` table or from the settings block's `plugin(...)` calls. `LibraryVersion` becomes `EntryVersion`, since a plugin's version is read the same way as a library's.
A `MavenDownloadingException` while selecting a version for a catalog library left that library
alone and said nothing, so a catalog the build can't resolve looked no different from one with
nothing to upgrade. The failure now marks the catalog, as it already does for a dependency declared
in a script, the exception naming the library and the repositories it tried.
The warning is added only to a catalog that carries none, because a repeated download failure
reports itself differently the second time around ("Did not attempt to download because of a
previous failure to retrieve from this repository") and the recipe would never stabilize.
jkschneider
force-pushed
the
gradle-version-catalogs
branch
from
September 12, 2026 18:28
61afa78 to
b8acfd3
Compare
`VersionCatalog.Matcher` is the way to a catalog, so `SettingsVersionCatalog` and `TomlVersionCatalog` are package private along with their matchers. Nothing outside `org.openrewrite.gradle.trait` named either one, and neither is worth supporting as public API. The nested interface a library or plugin implements is `Entry`, not `EntryVersion`. `SettingsVersionCatalogTest` loses the `PublicApi` class it nested its cases in, which named something that is no longer true and had no sibling.
jkschneider
force-pushed
the
gradle-version-catalogs
branch
from
September 12, 2026 22:58
5ad75e9 to
9df452b
Compare
This was referenced Sep 12, 2026
jkschneider
marked this pull request as ready for review
September 12, 2026 23:01
shanman190
reviewed
Sep 12, 2026
Contributor
There was a problem hiding this comment.
Indentation seemed to get off here.
kdvolder
reviewed
Sep 14, 2026
| versionCatalogs { | ||
| libs { | ||
| version('widgetVersion', '2.0') | ||
| library('widgetA', 'com.acme', 'widget-a').version('2.0') |
Contributor
There was a problem hiding this comment.
This is wrong, it should 'revert' back to using 'widgetVersion'.
kdvolder
reviewed
Sep 14, 2026
| versionCatalogs { | ||
| create("libs") { | ||
| version("widgetVersion", "2.0") | ||
| library("widgetA", "com.acme", "widget-a").version("2.0") |
Contributor
There was a problem hiding this comment.
Should use 'widgetVersion'
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's changed?
UpgradeDependencyVersionupgrades libraries declared in a Gradle version catalog, whether thecatalog is a
dependencyResolutionManagement { versionCatalogs { ... } }block insettings.gradle(.kts)or agradle/libs.versions.tomlfile.This brings together #8503 and #8699, which each modeled one of those. The first two commits are
those PRs squashed with their authors' attribution intact; the third reconciles them.
The model
VersionCatalog.Matcherfinds a catalog in either form, the waySpringBean.Matcherinrewrite-spring finds a bean whether it is an XML
<bean>or a@Beanmethod. A recipe starts fromone
new VersionCatalog.Matcher().asVisitor(...)and gets a catalog at alibs { ... }call or ata
*.versions.tomldocument; the visitor it returns only accepts Gradle scripts and versionsfiles, so nothing else is walked. That is what
UpgradeDependencyVersionnow does, in place of aTOML visitor plus a branch inside its script visitor. The two implementations are package private,
since the matcher is the way to one and neither is worth supporting as public API.
VersionCatalogreads asgetLibraryVersions,getPluginVersions,getVersionDeclarationsandgetVersion, and is edited throughwithVersions. Each implementation supplies the contents andthree primitive edits (set a library's own version, detach a library from a shared declaration, set
a declaration's value). The recipe selects a new version for each matching library and hands the
whole map to
withVersions, the same for either kind of catalog.The TOML entry traits edit their entries directly through
TomlTableValueinstead of running theChangeValueandDeleteKeyrecipes over them, which had leftChangedmarkers on the tree.TomlTableValue.withKeyrenames a property in place, soversion.ref = "x"becomesversion = "1.2"with the entry's padding, commas and quote style intact, mirroring what thesettings side does with
versionRef(...)andversion(...).Sharing
Where new versions go is decided once, on
VersionCatalog, from everything a run is moving. Ashared version declaration changes in place when every entry referring to it is a library moving to
the same version; otherwise only the libraries that are moving get a version of their own, and the
declaration stays for the ones that are not. A recipe whose glob matches every sharer bumps the
declaration; a recipe matching one of them detaches that one.
A plugin can share a declaration with libraries, in the
[plugins]table or asplugin(alias, id).versionRef(...)in the settings DSL. A declaration a plugin refers to is nevermoved, since this recipe moves no plugins, and the libraries that are moving detach instead.
#8503 produced the same results within a run and additionally across runs, by remembering the
original sharing in a marker it added during the run. That marker is gone here. Adding a marker at
run time changes the tree without changing the source, so a file reads as changed with nothing to
show for it, and nothing carries over between runs in any case. Two independent recipes each
targeting one sharer therefore do not converge on the shared declaration together: the first
detaches its library, and the last, finding its library is the declaration's only remaining
referrer, moves the declaration itself. Nothing is left stale, and the sequential tests from #8503
now assert that outcome.
Naming
A version catalog is a Gradle concept, so a
Gradleprefix on the types said nothing. The twoimplementations are named for where the catalog is declared, and the entry traits after the
tables they represent.
Repositories for a TOML catalog
A TOML document carries no
GradleProjectmarker, so on its own it has nothing to resolvelatest.releaseor30.xagainst. The scanner keeps the root project's marker if it is scannedand otherwise the first one seen, and the catalog pass resolves with that project's repositories.
GradleSettingsis deliberately not consulted: its repositories are the ones plugins resolve from,so a library lookup through it would go to the wrong place. A settings-script catalog on a
settings file without a project marker resolves the same way. The symbolic-version test in
UpgradeDependencyVersionTomlCatalogTestexercises this through the tooling API, as #8503's didfor the settings form.
A library whose metadata can't be downloaded is left alone and the catalog is marked with the
MavenDownloadingException, as a dependency declared in a script already is. Only a catalogcarrying no warning gets one, because a repeated download failure reports itself differently the
second time around and the recipe would otherwise never stabilize.
Testing
VersionCatalogTestmatches a settings catalog and a TOML catalog in one run through the onematcher and reads a
versionRef'd version from each.UpgradeDependencyVersionTomlCatalogTestcovers inline versions, string notation, detaching one of two sharers, bumping the shared
declaration when a glob matches both, a declaration a plugin shares, two sequential recipes leaving
the declaration to its last referrer, a symbolic version resolved through the build's repositories,
a library whose metadata can't be downloaded, and leaving a non-catalog TOML file alone. #8503's
UpgradeDependencyVersionVersionCatalogTestpasses with its two sequential cases asserting thebehavior above and gains the plugin-sharing case in both DSLs,
SettingsVersionCatalogTestwithout its marker case, and #8699's trait tests unchanged apart from the renames and one case:
withGroupThenWithModuleUsesUpdatedSemanticCoordinatechained edits that netted out to theoriginal text, and it only passed single-cycle because the
Changedmarkers short-circuited thesecond cycle. It now chains to a real change. The full
rewrite-gradleandrewrite-tomlsuitespass.
Supersedes #8503 and #8699.