From 4010e8b42c32b8dfac31a988c6820b6b0ef499bd Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 17 Mar 2026 17:18:33 +0100 Subject: [PATCH 1/5] test: Introduce a test harnessing predeclared deps --- .../gradle/spotless/MultiProjectTest.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java index 69301327c7..35bf2708b4 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -137,6 +137,36 @@ public void predeclaredOrdering() throws IOException { .contains("Could not find method spotlessPredeclare() for arguments"); } + @Test + public void predeclaredDepsRegression() throws IOException { + setFile("settings.gradle").toContent("include 'sub'"); + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "repositories { mavenCentral() }", + "spotless {", + " predeclareDeps()", + " java {", + " target file('test.java')", + " googleJavaFormat('1.17.0')", + " }", + "}", + "spotlessPredeclare {", + " java { googleJavaFormat('1.17.0') }", + "}"); + setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + setFile("sub/build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "repositories { mavenCentral() }", + "spotless {", + " java {", + " target file('test.java')", + " googleJavaFormat('1.17.0')", + " }", + "}"); + setFile("sub/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test"); + gradleRunner().withGradleVersion("8.14").withArguments("spotlessApply").build(); + } + @Test public void predeclaredUndeclared() throws IOException { setFile("build.gradle").toLines( From 47489afd92aa79f56372bd53e82d09a44f606a32 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 17 Mar 2026 18:21:12 +0100 Subject: [PATCH 2/5] fix: avoid IllegalMutationException when root project uses predeclareDeps() 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 #2885 --- .../gradle/spotless/SpotlessExtensionPredeclare.java | 1 + .../com/diffplug/gradle/spotless/SpotlessTaskService.java | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java index 3357f19c12..0a41a22ccf 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java @@ -33,6 +33,7 @@ public SpotlessExtensionPredeclare(Project project, GradleProvisioner.Policy pol this.registerDependenciesTask = findRegisterDepsTask().get(); SpotlessTaskService taskService = getSpotlessTaskService().get(); taskService.isUsingPredeclared = true; + taskService.registerDependenciesTask = registerDependenciesTask; taskService.predeclaredProvisioner = policy.dedupingProvisioner(project); taskService.predeclaredP2Provisioner = policy.dedupingP2Provisioner(project); project.afterEvaluate(unused -> toSetup.forEach((name, formatExtension) -> { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java index f760d6a23f..a5ae4f646f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java @@ -64,6 +64,7 @@ public abstract class SpotlessTaskService implements BuildService registerTask.hookSubprojectTask(task)); + if (registerDependenciesTask != null) { + registerDependenciesTask.hookSubprojectTask(task); + } } public static Provider registerIfAbsent(Project project, String suffix) { From 55c0c5c081b83ad35e92911718e3069a64467c2a Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Tue, 17 Mar 2026 20:17:03 +0100 Subject: [PATCH 3/5] fix: IsolatedProjectTest.predeclaredIsUnsupported() is now actually supported with the fix --- .../com/diffplug/gradle/spotless/IsolatedProjectTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IsolatedProjectTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IsolatedProjectTest.java index e9c1af7197..957c7ce85b 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IsolatedProjectTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IsolatedProjectTest.java @@ -17,7 +17,6 @@ import java.io.IOException; -import org.assertj.core.api.Assertions; import org.gradle.testkit.runner.GradleRunner; import org.junit.jupiter.api.Test; @@ -85,7 +84,7 @@ void noRootIsSupported() throws IOException { } @Test - void predeclaredIsUnsupported() throws IOException { + void predeclaredIsSupported() throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -96,7 +95,6 @@ void predeclaredIsUnsupported() throws IOException { " kotlin { ktlint() }", "}"); createNSubprojects(); - Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) - .containsAnyOf("Cannot access project", "cannot access 'Project.tasks'"); + gradleRunner().withArguments("spotlessApply").build(); } } From 3f7f12e649b1085a7937b8730e6a7689a52ca6da Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Wed, 18 Mar 2026 11:27:06 +0100 Subject: [PATCH 4/5] chore: Removes check for predeclare as it's not needed anymore --- .../gradle/spotless/SpotlessExtensionPredeclare.java | 1 - .../com/diffplug/gradle/spotless/SpotlessTaskService.java | 6 ------ 2 files changed, 7 deletions(-) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java index 0a41a22ccf..ef8589523f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java @@ -32,7 +32,6 @@ public SpotlessExtensionPredeclare(Project project, GradleProvisioner.Policy pol super(project); this.registerDependenciesTask = findRegisterDepsTask().get(); SpotlessTaskService taskService = getSpotlessTaskService().get(); - taskService.isUsingPredeclared = true; taskService.registerDependenciesTask = registerDependenciesTask; taskService.predeclaredProvisioner = policy.dedupingProvisioner(project); taskService.predeclaredP2Provisioner = policy.dedupingP2Provisioner(project); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java index a5ae4f646f..0aa7b596a7 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskService.java @@ -56,7 +56,6 @@ * apply already did). */ public abstract class SpotlessTaskService implements BuildService, AutoCloseable, OperationCompletionListener { - protected boolean isUsingPredeclared; private final Map apply = Collections.synchronizedMap(new HashMap<>()); private final Map source = Collections.synchronizedMap(new HashMap<>()); private final Map provisioner = Collections.synchronizedMap(new HashMap<>()); @@ -130,11 +129,6 @@ static void usesServiceTolerateTestFailure(DefaultTask task, Provider Date: Wed, 18 Mar 2026 11:32:20 +0100 Subject: [PATCH 5/5] chore: Updated gradle plugin change --- plugin-gradle/CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index c8398bcefc..54fe09ea39 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -5,6 +5,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added - Add `tableTest` format type for standalone `.table` files. +### Fixed +- Fix illegal mutation when using predeclared dependencies. ### Changes - Bump default `tabletest-formatter` version `1.0.1` -> `1.1.1`, now works with Java 17+.