Skip to content

Gradle version catalogs: one model for settings and TOML - #8864

Merged
jkschneider merged 6 commits into
mainfrom
gradle-version-catalogs
Sep 12, 2026
Merged

jkschneider merged 6 commits into
mainfrom
gradle-version-catalogs

Conversation

@jkschneider

@jkschneider jkschneider commented Sep 12, 2026 •

Copy link
Copy Markdown
Member

What's changed?

UpgradeDependencyVersion upgrades libraries declared in a Gradle version catalog, whether the
catalog is a dependencyResolutionManagement { versionCatalogs { ... } } block in
settings.gradle(.kts) or a gradle/libs.versions.toml file.

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              the concept; how new versions are placed lives here
├─ SettingsVersionCatalog   declared in settings.gradle(.kts)      (package private)
└─ TomlVersionCatalog       declared in gradle/libs.versions.toml  (package private)
VersionCatalogLibrary       one [libraries] entry in a TOML catalog
VersionCatalogPlugin        one [plugins] entry in a TOML catalog

VersionCatalog.Matcher finds a catalog in either form, the way SpringBean.Matcher in
rewrite-spring finds a bean whether it is an XML <bean> or a @Bean method. A recipe starts from
one new VersionCatalog.Matcher().asVisitor(...) and gets a catalog at a libs { ... } call or at
a *.versions.toml document; the visitor it returns only accepts Gradle scripts and versions
files, so nothing else is walked. That is what UpgradeDependencyVersion now does, in place of a
TOML 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.

VersionCatalog reads as getLibraryVersions, getPluginVersions, getVersionDeclarations and
getVersion, and is edited through withVersions. Each implementation supplies the contents and
three 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 TomlTableValue instead of running the
ChangeValue and DeleteKey recipes over them, which had left Changed markers on the tree.
TomlTableValue.withKey renames a property in place, so version.ref = "x" becomes
version = "1.2" with the entry's padding, commas and quote style intact, mirroring what the
settings side does with versionRef(...) and version(...).

Sharing

Where new versions go is decided once, on VersionCatalog, from everything a run is moving. A
shared 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 as
plugin(alias, id).versionRef(...) in the settings DSL. A declaration a plugin refers to is never
moved, 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 Gradle prefix on the types said nothing. The two
implementations 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 GradleProject marker, so on its own it has nothing to resolve
latest.release or 30.x against. The scanner keeps the root project's marker if it is scanned
and otherwise the first one seen, and the catalog pass resolves with that project's repositories.
GradleSettings is 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
UpgradeDependencyVersionTomlCatalogTest exercises this through the tooling API, as #8503's did
for 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 catalog
carrying no warning gets one, because a repeated download failure reports itself differently the
second time around and the recipe would otherwise never stabilize.

Testing

VersionCatalogTest matches a settings catalog and a TOML catalog in one run through the one
matcher and reads a versionRef'd version from each. UpgradeDependencyVersionTomlCatalogTest
covers 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
UpgradeDependencyVersionVersionCatalogTest passes with its two sequential cases asserting the
behavior above and gains the plugin-sharing case in both DSLs, SettingsVersionCatalogTest
without its marker case, and #8699's trait tests unchanged apart from the renames and one case:
withGroupThenWithModuleUsesUpdatedSemanticCoordinate chained edits that netted out to the
original text, and it only passed single-cycle because the Changed markers short-circuited the
second cycle. It now chains to a real change. The full rewrite-gradle and rewrite-toml suites
pass.

Supersedes #8503 and #8699.

BoykoAlex and others added 2 commits September 11, 2026 23:00
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.
Comment thread rewrite-gradle/src/main/java/org/openrewrite/gradle/UpgradeDependencyVersion.java Outdated
@jkschneider
jkschneider force-pushed the gradle-version-catalogs branch from 97441fc to 61afa78 Compare September 12, 2026 17:41
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
jkschneider force-pushed the gradle-version-catalogs branch from 61afa78 to b8acfd3 Compare September 12, 2026 18:28
`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
jkschneider force-pushed the gradle-version-catalogs branch from 5ad75e9 to 9df452b Compare September 12, 2026 22:58
@jkschneider
jkschneider marked this pull request as ready for review September 12, 2026 23:01
@jkschneider
jkschneider merged commit c6fd89e into main Sep 12, 2026
1 check passed
@jkschneider
jkschneider deleted the gradle-version-catalogs branch September 12, 2026 23:21
@github-project-automation github-project-automation Bot moved this from In Progress to Done in OpenRewrite Sep 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation seemed to get off here.

versionCatalogs {
libs {
version('widgetVersion', '2.0')
library('widgetA', 'com.acme', 'widget-a').version('2.0')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong, it should 'revert' back to using 'widgetVersion'.

versionCatalogs {
create("libs") {
version("widgetVersion", "2.0")
library("widgetA", "com.acme", "widget-a").version("2.0")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use 'widgetVersion'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

5 participants