Skip to content

Fix for illegal mutation issue with predeclareDeps#2892

Merged
nedtwigg merged 5 commits intodiffplug:mainfrom
bric3:fix-illegal-mutation
Mar 18, 2026
Merged

Fix for illegal mutation issue with predeclareDeps#2892
nedtwigg merged 5 commits intodiffplug:mainfrom
bric3:fix-illegal-mutation

Conversation

@bric3
Copy link
Contributor

@bric3 bric3 commented Mar 17, 2026

Fix IllegalMutationException when root project uses predeclareDeps() with format tasks

Fixes

Introduced by

Problem

PR #2854 ("Partially support isolated projects") refactored SpotlessTaskService.hookSubprojectTask to look up the RegisterDependenciesTask dynamically via the task container rather than holding a direct reference. The call used withType(Class, Action):

project.getRootProject().getTasks()
  .withType(RegisterDependenciesTask.class, task -> task.hookSubprojectTask(task));

On Gradle 8.14.x (unsure about the exact version this behavior was introduced), this throws IllegalMutationException when all the following are true:

  1. The root project calls predeclareDeps()
  2. The root project also configures its own spotless format tasks (e.g. java, groovyGradle)
  3. A spotless task on the root project is being realized

During root project task realization, Gradle activates its mutation guard on the root task container.
The configure callback calls FormatExtension.setupTask() then hookSubprojectTask() then project.getRootProject().getTasks().withType(Class, Action) on that same guarded container which Gradle forbids.

Switching to withType(Class).configureEach(Action) does not help either as Gradle also considers configureEach a mutating registration in this context.

Fix

Since RegisterDependenciesTask is the task that must run before all projects, this fix assumes it can be safely referenced by the main spotless service. So the fix stores the RegisterDependenciesTask reference directly in SpotlessTaskService at configuration time (when SpotlessExtensionPredeclare is constructed and isUsingPredeclared() is set). Then hookSubprojectTask uses that reference directly, with no task container lookup during task realization.

// SpotlessExtensionPredeclare constructor
taskService.registerDependenciesTask = this.registerDependenciesTask;

// hookSubprojectTask — no task container query
if (registerDependenciesTask != null) {
    registerDependenciesTask.hookSubprojectTask(task);
}

The RegisterDependenciesTask reference is already computed eagerly in SpotlessExtensionPredeclare (via findRegisterDepsTask().get()), so no new eagerness is introduced.

Note that IsolatedProjectTest.predeclaredIsUnsupported() (introduced in #2854) expected a build failure that no longer occurs because this fix`

Verification

Added MultiProjectTest.predeclaredDepsRegression() that reproduces the exact trigger conditions and runs against Gradle 8.14.

bric3 added 3 commits March 17, 2026 17:18
…Deps() with format tasks

Store the `RegisterDependenciesTask` reference in `SpotlessTaskService`
at configuration time instead of querying the task container via
`withType()` during task realization, which triggers Gradle's mutation
guard on 8.14+.

Note that `withType(Class, Action)` and `withType(Class).configureEach(Action)`
both trigger Gradle's `DefaultMutationGuard` when called during task
realization (in Gradle 8.14+, maybe earlier).

Fixes diffplug#2885
@bric3 bric3 marked this pull request as ready for review March 17, 2026 20:15
@nedtwigg
Copy link
Member

This needs an update to the changelog in plugin-gradle/CHANGES.md. Otherwise this lgtm. @inorichi do you see any issues with this?

@inorichi
Copy link
Contributor

LGTM, and if predeclaredDeps is now supported by isolated projects, even better.

I left a comment for a minor change.

@bric3
Copy link
Contributor Author

bric3 commented Mar 18, 2026

Done I updated the changes, if that's alright with you.

@nedtwigg nedtwigg merged commit db8dc1c into diffplug:main Mar 18, 2026
20 checks passed
@bric3 bric3 deleted the fix-illegal-mutation branch March 18, 2026 17:16
@nedtwigg
Copy link
Member

Published in plugin-gradle 8.4.0.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants