From 9d64197c1f8160029d1d5f4a095524ad8fbd3b3d Mon Sep 17 00:00:00 2001 From: inorichi Date: Fri, 20 Feb 2026 15:00:07 +0100 Subject: [PATCH 01/12] Make BaseKotlinExtension compatible with isolated projects --- .../com/diffplug/gradle/spotless/BaseKotlinExtension.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java index 975be992c2..179739aa58 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -160,7 +160,7 @@ private KtlintConfig( Map editorConfigOverride, List customRuleSets) throws IOException { Objects.requireNonNull(version); - File defaultEditorConfig = getProject().getRootProject().file(".editorconfig"); + File defaultEditorConfig = new File(getProject().getRootDir(), ".editorconfig"); FileSignature editorConfigPath = defaultEditorConfig.exists() ? FileSignature.signAsList(defaultEditorConfig) : null; this.version = version; this.editorConfigPath = editorConfigPath; From 2bf655bf094b46c238c7427cc44409db10084119 Mon Sep 17 00:00:00 2001 From: inorichi Date: Fri, 20 Feb 2026 15:00:09 +0100 Subject: [PATCH 02/12] Remove GradleCompat and always use project providers for querying properties --- .../gradle/spotless/FormatExtension.java | 3 +- .../gradle/spotless/GradleCompat.java | 41 ------------------- .../com/diffplug/gradle/spotless/IdeHook.java | 6 +-- .../gradle/spotless/SpotlessPlugin.java | 4 +- 4 files changed, 7 insertions(+), 47 deletions(-) delete mode 100644 plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index 913f320d73..698fb4b908 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -635,7 +635,8 @@ public LicenseHeaderConfig updateYearWithLatest(boolean updateYearWithLatest) { FormatterStep createStep() { return builder.withYearModeLazy(() -> { - if (Boolean.parseBoolean(GradleCompat.findOptionalProperty(spotless.project, LicenseHeaderStep.FLAG_SET_LICENSE_HEADER_YEARS_FROM_GIT_HISTORY()))) { + String yearProperty = spotless.project.getProviders().gradleProperty(LicenseHeaderStep.FLAG_SET_LICENSE_HEADER_YEARS_FROM_GIT_HISTORY()).getOrNull(); + if (Boolean.parseBoolean(yearProperty)) { return YearMode.SET_FROM_GIT; } else { boolean updateYear = updateYearWithLatest == null ? getRatchetFrom() != null : updateYearWithLatest; diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java deleted file mode 100644 index 16d02b75b8..0000000000 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2025 DiffPlug - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.diffplug.gradle.spotless; - -import javax.annotation.Nullable; - -import org.gradle.api.Project; - -public final class GradleCompat { - private GradleCompat() {} - - @Nullable public static String findOptionalProperty(Project project, String propertyName) { - @Nullable String value = project.getProviders().gradleProperty(propertyName).getOrNull(); - if (value != null) { - return value; - } - @Nullable Object property = project.findProperty(propertyName); - if (property != null) { - return property.toString(); - } - return null; - } - - public static boolean isPropertyPresent(Project project, String propertyName) { - return project.getProviders().gradleProperty(propertyName).isPresent() || - project.hasProperty(propertyName); - } -} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java index 8a10ffbcf3..6090bd748f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java @@ -39,10 +39,10 @@ static class State extends NoLambda.EqualityBasedOnSerialization { final boolean useStdOut; State(Project project) { - var pathsString = GradleCompat.findOptionalProperty(project, PROPERTY); + var pathsString = project.getProviders().gradleProperty(PROPERTY).getOrNull(); if (pathsString != null) { - useStdIn = GradleCompat.isPropertyPresent(project, USE_STD_IN); - useStdOut = GradleCompat.isPropertyPresent(project, USE_STD_OUT); + useStdIn = project.getProviders().gradleProperty(USE_STD_IN).isPresent(); + useStdOut = project.getProviders().gradleProperty(USE_STD_OUT).isPresent(); paths = Arrays.stream(pathsString.split(",")) .map(String::trim) .filter(s -> !s.isEmpty()) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java index 2bd773f2d3..1583cf3d67 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.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. @@ -42,7 +42,7 @@ public void apply(Project project) { + "https://docs.gradle.org/current/userguide/building_java_projects.html#sec:java_cross_compilation"); } // if -PspotlessModern=true, then use the modern stuff instead of the legacy stuff - if (GradleCompat.isPropertyPresent(project, SPOTLESS_MODERN)) { + if (project.getProviders().gradleProperty(SPOTLESS_MODERN).isPresent()) { project.getLogger().warn("'spotlessModern' has no effect as of Spotless 5.0, recommend removing it."); } // make sure there's a `clean` and a `check` From f7829b55c2a20c3a2ee58faad9370fe0226fb7c3 Mon Sep 17 00:00:00 2001 From: inorichi Date: Fri, 20 Feb 2026 15:00:10 +0100 Subject: [PATCH 03/12] Make non-predeclared spotless compatible with isolated projects --- .../gradle/spotless/FormatExtension.java | 8 ++-- .../spotless/RegisterDependenciesTask.java | 6 +-- .../gradle/spotless/SpotlessExtension.java | 34 +++-------------- .../spotless/SpotlessExtensionImpl.java | 2 +- .../spotless/SpotlessExtensionPredeclare.java | 30 +++++++++++++-- .../gradle/spotless/SpotlessTaskService.java | 17 +++++++++ .../spotless/ErrorShouldRethrowTest.java | 7 ++-- .../spotless/GradleIntegrationHarness.java | 4 +- .../gradle/spotless/MultiProjectTest.java | 38 ++++++++++++++++++- .../RegisterDependenciesTaskTest.java | 4 +- .../gradle/spotless/UpToDateTest.java | 4 +- 11 files changed, 101 insertions(+), 53 deletions(-) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index 698fb4b908..02a3bbf3b1 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -95,11 +95,11 @@ public FormatExtension(SpotlessExtension spotless) { } protected final Provisioner provisioner() { - return spotless.getRegisterDependenciesTask().getTaskService().get().provisionerFor(spotless); + return spotless.getSpotlessTaskService().get().provisionerFor(spotless); } protected final P2Provisioner p2Provisioner() { - return spotless.getRegisterDependenciesTask().getTaskService().get().p2ProvisionerFor(spotless); + return spotless.getSpotlessTaskService().get().p2ProvisionerFor(spotless); } private String formatName() { @@ -1099,7 +1099,7 @@ protected void setupTask(SpotlessTask task) { LineEnding lineEndings = getLineEndings(); task.setLineEndingsPolicy( getProject().provider(() -> lineEndings.createPolicy(projectDir.getAsFile(), () -> totalTarget))); - spotless.getRegisterDependenciesTask().hookSubprojectTask(task); + spotless.getSpotlessTaskService().get().hookSubprojectTask(getProject(), task); task.setupRatchet(getRatchetFrom() != null ? getRatchetFrom() : ""); } @@ -1135,7 +1135,7 @@ public TaskProvider createIndependentApplyTaskLazy(String taskNam "Task name must not end with " + SpotlessExtension.APPLY); TaskProvider spotlessTask = spotless.project.getTasks() .register(taskName + SpotlessTaskService.INDEPENDENT_HELPER, SpotlessTaskImpl.class, task -> { - task.init(spotless.getRegisterDependenciesTask().getTaskService()); + task.init(spotless.getSpotlessTaskService()); setupTask(task); // clean removes the SpotlessCache, so we have to run after clean task.mustRunAfter(BasePlugin.CLEAN_TASK_NAME); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java index 33aded8bdd..3c66f5427f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RegisterDependenciesTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 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. @@ -25,7 +25,6 @@ import org.gradle.api.DefaultTask; import org.gradle.api.provider.Provider; -import org.gradle.api.services.BuildServiceRegistry; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.OutputFile; @@ -64,8 +63,7 @@ void hookSubprojectTask(SpotlessTask task) { void setup() { Preconditions.checkArgument(getProject().getRootProject() == getProject(), "Can only be used on the root project"); String compositeBuildSuffix = getName().substring(TASK_NAME.length()); // see https://github.com/diffplug/spotless/pull/1001 - BuildServiceRegistry buildServices = getProject().getGradle().getSharedServices(); - taskService = buildServices.registerIfAbsent("SpotlessTaskService" + compositeBuildSuffix, SpotlessTaskService.class, spec -> {}); + taskService = SpotlessTaskService.registerIfAbsent(getProject(), compositeBuildSuffix); usesService(taskService); getBuildEventsListenerRegistry().onTaskCompletion(taskService); unitOutput = new File(getProject().getLayout().getBuildDirectory().getAsFile().get(), "tmp/spotless-register-dependencies"); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java index 2ff4896d9e..c41b28c331 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.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. @@ -27,15 +27,14 @@ import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Project; -import org.gradle.api.tasks.TaskContainer; -import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.provider.Provider; import org.gradle.language.base.plugins.LifecycleBasePlugin; import com.diffplug.spotless.LineEnding; public abstract class SpotlessExtension { final Project project; - private final RegisterDependenciesTask registerDependenciesTask; + private final Provider spotlessTaskService; protected static final String TASK_GROUP = LifecycleBasePlugin.VERIFICATION_GROUP; protected static final String BUILD_SETUP_TASK_GROUP = "build setup"; @@ -52,11 +51,11 @@ public abstract class SpotlessExtension { protected SpotlessExtension(Project project) { this.project = requireNonNull(project); - this.registerDependenciesTask = findRegisterDepsTask().get(); + this.spotlessTaskService = SpotlessTaskService.registerIfAbsent(project, ""); } - RegisterDependenciesTask getRegisterDependenciesTask() { - return registerDependenciesTask; + Provider getSpotlessTaskService() { + return spotlessTaskService; } /** Line endings (if any). */ @@ -303,27 +302,6 @@ T instantiateFormatExtension(Class clazz) { protected abstract void createFormatTasks(String name, FormatExtension formatExtension); - TaskProvider findRegisterDepsTask() { - try { - return findRegisterDepsTask(RegisterDependenciesTask.TASK_NAME); - } catch (Exception e) { - // in a composite build there can be multiple Spotless plugins on the classpath, and they will each try to register - // a task on the root project with the same name. That will generate casting errors, which we can catch and try again - // with an identity-specific identifier. - // https://github.com/diffplug/spotless/pull/1001 for details - return findRegisterDepsTask(RegisterDependenciesTask.TASK_NAME + System.identityHashCode(RegisterDependenciesTask.class)); - } - } - - private TaskProvider findRegisterDepsTask(String taskName) { - TaskContainer rootProjectTasks = project.getRootProject().getTasks(); - if (!rootProjectTasks.getNames().contains(taskName)) { - return rootProjectTasks.register(taskName, RegisterDependenciesTask.class, RegisterDependenciesTask::setup); - } else { - return rootProjectTasks.named(taskName, RegisterDependenciesTask.class); - } - } - public void predeclareDepsFromBuildscript() { if (project.getRootProject() != project) { throw new GradleException("predeclareDepsFromBuildscript can only be called from the root project"); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java index 04bfb8d221..173b5a1113 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java @@ -61,7 +61,7 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) { // create the SpotlessTask String taskName = EXTENSION + SpotlessPlugin.capitalize(name); TaskProvider spotlessTask = tasks.register(taskName, SpotlessTaskImpl.class, task -> { - task.init(getRegisterDependenciesTask().getTaskService()); + task.init(getSpotlessTaskService()); task.setGroup(TASK_GROUP); task.getIdeHookState().set(ideHook); // clean removes the SpotlessCache, so we have to run after clean 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 52a40a22b4..3357f19c12 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 @@ -20,23 +20,28 @@ import org.gradle.api.Action; import org.gradle.api.Project; +import org.gradle.api.tasks.TaskProvider; import com.diffplug.spotless.LazyForwardingEquality; public class SpotlessExtensionPredeclare extends SpotlessExtension { private final SortedMap toSetup = new TreeMap<>(); + private final RegisterDependenciesTask registerDependenciesTask; public SpotlessExtensionPredeclare(Project project, GradleProvisioner.Policy policy) { super(project); - getRegisterDependenciesTask().getTaskService().get().predeclaredProvisioner = policy.dedupingProvisioner(project); - getRegisterDependenciesTask().getTaskService().get().predeclaredP2Provisioner = policy.dedupingP2Provisioner(project); + this.registerDependenciesTask = findRegisterDepsTask().get(); + SpotlessTaskService taskService = getSpotlessTaskService().get(); + taskService.isUsingPredeclared = true; + taskService.predeclaredProvisioner = policy.dedupingProvisioner(project); + taskService.predeclaredP2Provisioner = policy.dedupingP2Provisioner(project); project.afterEvaluate(unused -> toSetup.forEach((name, formatExtension) -> { for (Action lazyAction : formatExtension.lazyActions) { lazyAction.execute(formatExtension); } - getRegisterDependenciesTask().steps.addAll(formatExtension.steps); + registerDependenciesTask.steps.addAll(formatExtension.steps); // needed to fix Deemon memory leaks (#1194), but this line came from https://github.com/diffplug/spotless/pull/1206 - LazyForwardingEquality.unlazy(getRegisterDependenciesTask().steps); + LazyForwardingEquality.unlazy(registerDependenciesTask.steps); })); } @@ -49,4 +54,21 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) { protected void predeclare(GradleProvisioner.Policy policy) { throw new UnsupportedOperationException("predeclare can't be called from within `" + EXTENSION_PREDECLARE + "`"); } + + private TaskProvider findRegisterDepsTask() { + try { + return findRegisterDepsTask(RegisterDependenciesTask.TASK_NAME); + } catch (Exception e) { + // in a composite build there can be multiple Spotless plugins on the classpath, and they will each try to register + // a task on the root project with the same name. That will generate casting errors, which we can catch and try again + // with an identity-specific identifier. + // https://github.com/diffplug/spotless/pull/1001 for details + return findRegisterDepsTask(RegisterDependenciesTask.TASK_NAME + System.identityHashCode(RegisterDependenciesTask.class)); + } + } + + private TaskProvider findRegisterDepsTask(String taskName) { + return project.getTasks().register(taskName, RegisterDependenciesTask.class, RegisterDependenciesTask::setup); + } + } 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 3632f74050..5538417ba5 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 @@ -28,6 +28,7 @@ import javax.inject.Inject; import org.gradle.api.DefaultTask; +import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileTree; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileVisitDetails; @@ -55,6 +56,7 @@ * apply already did). */ public abstract class SpotlessTaskService implements BuildService, AutoCloseable, OperationCompletionListener { + protected boolean isUsingPredeclared = false; private final Map apply = Collections.synchronizedMap(new HashMap<>()); private final Map source = Collections.synchronizedMap(new HashMap<>()); private final Map provisioner = Collections.synchronizedMap(new HashMap<>()); @@ -126,6 +128,21 @@ static void usesServiceTolerateTestFailure(DefaultTask task, Provider { + registerTask.hookSubprojectTask(task); + }); + } + + public static Provider registerIfAbsent(Project project, String suffix) { + return project.getGradle().getSharedServices() + .registerIfAbsent("SpotlessTaskService" + suffix, SpotlessTaskService.class, spec -> {}); + } + abstract static class ClientTask extends DefaultTask { @Internal abstract Property getSpotlessCleanDirectory(); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ErrorShouldRethrowTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ErrorShouldRethrowTest.java index ebd4bcbbf2..3e71cfc984 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ErrorShouldRethrowTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ErrorShouldRethrowTest.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. @@ -136,10 +136,9 @@ private void expectSuccess() throws Exception { private StringSelfie expectFailureAndConsoleToBe() throws Exception { BuildResult result = gradleRunner().withArguments("check").buildAndFail(); String output = result.getOutput(); - int register = output.indexOf(":spotlessInternalRegisterDependencies"); - int firstNewlineAfterThat = output.indexOf('\n', register + 1); + int firstTask = output.indexOf("> Task"); int firstTry = output.indexOf("\n* Try:"); - String useThisToMatch = output.substring(firstNewlineAfterThat, firstTry).trim(); + String useThisToMatch = output.substring(firstTask, firstTry).trim(); return Selfie.expectSelfie(useThisToMatch); } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java index 2882e21b4f..0e223a3409 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.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. @@ -197,7 +197,7 @@ private void taskIsUpToDate(String task, boolean upToDate) throws IOException { List expected = outcomes(buildResult, upToDate ? TaskOutcome.UP_TO_DATE : TaskOutcome.SUCCESS); List notExpected = outcomes(buildResult, upToDate ? TaskOutcome.SUCCESS : TaskOutcome.UP_TO_DATE); - boolean everythingAsExpected = !expected.isEmpty() && notExpected.isEmpty() && buildResult.getTasks().size() - 1 == expected.size(); + boolean everythingAsExpected = !expected.isEmpty() && notExpected.isEmpty() && buildResult.getTasks().size() == expected.size(); if (!everythingAsExpected) { fail("Expected all tasks to be " + (upToDate ? TaskOutcome.UP_TO_DATE : TaskOutcome.SUCCESS) + ", but instead was\n" + buildResultToString(buildResult)); } 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..ecbb63183b 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. @@ -151,4 +151,40 @@ public void predeclaredUndeclared() throws IOException { Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) .contains("Could not find method spotlessPredeclare() for arguments"); } + + @Test + void nonPredeclaredSupportsIsolatedProjects() throws IOException { + setFile("gradle.properties").toContent("org.gradle.unsafe.isolated-projects=true"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "", + "spotless {", + " java {", + " target file('test.java')", + " googleJavaFormat('1.17.0')", + " }", + "}"); + createNSubprojects(); + gradleRunner().withArguments("spotlessApply").build(); + } + + @Test + void predeclaredRequiresNonIsolatedProjects() throws IOException { + setFile("gradle.properties").toContent("org.gradle.unsafe.isolated-projects=true"); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless { predeclareDeps() }", + "spotlessPredeclare {", + " java { googleJavaFormat('1.17.0') }", + "}"); + createNSubprojects(); + Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) + .contains("Cannot access project"); + } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java index 76d0449060..92d6ed179d 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RegisterDependenciesTaskTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 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. @@ -43,8 +43,6 @@ void duplicateConfigs() throws IOException { setFile("gradle.properties").toLines(); String newestSupported = gradleRunner().withArguments("spotlessCheck").build().getOutput(); Assertions.assertThat(newestSupported.replace("\r", "")) - .startsWith( - "> Task :spotlessInternalRegisterDependencies\n") .contains( "> Task :sub:spotlessJava\n", "> Task :sub:spotlessJavaCheck\n", diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/UpToDateTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/UpToDateTest.java index 9b7eccdfff..1c06374e9f 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/UpToDateTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/UpToDateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 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. @@ -88,7 +88,7 @@ void testPathologicalCase() throws IOException { // the format task is UP-TO-DATE (same inputs), but the apply tasks will run again pauseForFilesystem(); BuildResult buildResult = gradleRunner().withArguments("spotlessApply").build(); - Assertions.assertThat(buildResult.taskPaths(TaskOutcome.UP_TO_DATE)).containsExactly(":spotlessInternalRegisterDependencies", ":spotlessMisc"); + Assertions.assertThat(buildResult.taskPaths(TaskOutcome.UP_TO_DATE)).containsExactly(":spotlessMisc"); Assertions.assertThat(buildResult.taskPaths(TaskOutcome.SUCCESS)).containsExactly(":spotlessMiscApply", ":spotlessApply"); assertFile("README.md").hasContent("abc"); From bebb7463a0c3ebc64697810bda8987b04c80216c Mon Sep 17 00:00:00 2001 From: inorichi Date: Fri, 20 Feb 2026 20:22:38 +0100 Subject: [PATCH 04/12] Add changelog for isolated projects --- plugin-gradle/CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index b73e607579..7b24b9ae89 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +- Partial support for isolated projects. They work if predeclared dependencies are not used. ([#2854](https://github.com/diffplug/spotless/pull/2854)) ### Fixed - Fix the ability to specify a wildcard version (`*`) for external formatter executables, which did not work. ([#2848](https://github.com/diffplug/spotless/pull/2848)) From 52999982fecdab694130ca3830953a5b42793bf7 Mon Sep 17 00:00:00 2001 From: inorichi Date: Sat, 21 Feb 2026 10:21:46 +0100 Subject: [PATCH 05/12] Add a note about isolated projects incompatibility with predeclared dependencies --- plugin-gradle/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index d07316069b..07bb302af5 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -1918,6 +1918,8 @@ Alternatively, you can also use `predeclareDepsFromBuildscript()` to resolve the If you use this feature, you will get an error if you use a formatter in a subproject which is not declared in the `spotlessPredeclare` block. +Note that this feature is also incompatible with Isolated projects, because every project must reference the root project. + ## How do I preview what `spotlessApply` will do? From 9ac74d3f310f9055f4b5f444ba1938564dc18bb5 Mon Sep 17 00:00:00 2001 From: inorichi Date: Sat, 21 Feb 2026 10:58:19 +0100 Subject: [PATCH 06/12] Fix isolated project tests --- .../java/com/diffplug/gradle/spotless/MultiProjectTest.java | 2 +- 1 file changed, 1 insertion(+), 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 ecbb63183b..b9ffd83a50 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 @@ -185,6 +185,6 @@ void predeclaredRequiresNonIsolatedProjects() throws IOException { "}"); createNSubprojects(); Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) - .contains("Cannot access project"); + .containsAnyOf("Cannot access project", "cannot access 'Project.tasks'"); } } From fd20a1fdd554f517f6c3e0776dca9c3c6b5a576b Mon Sep 17 00:00:00 2001 From: inorichi Date: Mon, 2 Mar 2026 09:24:24 +0100 Subject: [PATCH 07/12] Use isolated project for accessing root project files --- .../com/diffplug/gradle/spotless/BaseKotlinExtension.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java index 179739aa58..b916b56550 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java @@ -25,6 +25,8 @@ import javax.annotation.Nullable; +import org.gradle.api.file.Directory; + import com.diffplug.common.collect.ImmutableList; import com.diffplug.common.collect.ImmutableSortedMap; import com.diffplug.spotless.FileSignature; @@ -160,7 +162,9 @@ private KtlintConfig( Map editorConfigOverride, List customRuleSets) throws IOException { Objects.requireNonNull(version); - File defaultEditorConfig = new File(getProject().getRootDir(), ".editorconfig"); + @SuppressWarnings("UnstableApiUsage") + Directory rootProjectDir = getProject().getIsolated().getRootProject().getProjectDirectory(); + File defaultEditorConfig = rootProjectDir.file(".editorconfig").getAsFile(); FileSignature editorConfigPath = defaultEditorConfig.exists() ? FileSignature.signAsList(defaultEditorConfig) : null; this.version = version; this.editorConfigPath = editorConfigPath; From 13d4045e9acb435279555de1667af02ca3acb16d Mon Sep 17 00:00:00 2001 From: inorichi Date: Mon, 2 Mar 2026 09:26:02 +0100 Subject: [PATCH 08/12] Restore GradleCompat with IP safe approach --- .../gradle/spotless/FormatExtension.java | 3 +- .../gradle/spotless/GradleCompat.java | 45 +++++++++++++++++++ .../com/diffplug/gradle/spotless/IdeHook.java | 6 +-- .../gradle/spotless/SpotlessPlugin.java | 4 +- 4 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index 02a3bbf3b1..e17f37b0fa 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -635,8 +635,7 @@ public LicenseHeaderConfig updateYearWithLatest(boolean updateYearWithLatest) { FormatterStep createStep() { return builder.withYearModeLazy(() -> { - String yearProperty = spotless.project.getProviders().gradleProperty(LicenseHeaderStep.FLAG_SET_LICENSE_HEADER_YEARS_FROM_GIT_HISTORY()).getOrNull(); - if (Boolean.parseBoolean(yearProperty)) { + if (Boolean.parseBoolean(GradleCompat.findOptionalProperty(spotless.project, LicenseHeaderStep.FLAG_SET_LICENSE_HEADER_YEARS_FROM_GIT_HISTORY()))) { return YearMode.SET_FROM_GIT; } else { boolean updateYear = updateYearWithLatest == null ? getRatchetFrom() != null : updateYearWithLatest; diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java new file mode 100644 index 0000000000..5c70413378 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java @@ -0,0 +1,45 @@ +/* + * Copyright 2025-2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.gradle.spotless; + +import javax.annotation.Nullable; + +import org.gradle.api.Project; +import org.gradle.api.plugins.ExtraPropertiesExtension; + +public final class GradleCompat { + private GradleCompat() {} + + @Nullable public static String findOptionalProperty(Project project, String propertyName) { + @Nullable String value = project.getProviders().gradleProperty(propertyName).getOrNull(); + if (value != null) { + return value; + } + ExtraPropertiesExtension extras = project.getExtensions().getByType(ExtraPropertiesExtension.class); + if (extras.has(propertyName)) { + @Nullable Object property = extras.get(propertyName); + if (property != null) { + return property.toString(); + } + } + return null; + } + + public static boolean isPropertyPresent(Project project, String propertyName) { + return project.getProviders().gradleProperty(propertyName).isPresent() || + project.getExtensions().getByType(ExtraPropertiesExtension.class).has(propertyName); + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java index 6090bd748f..8a10ffbcf3 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java @@ -39,10 +39,10 @@ static class State extends NoLambda.EqualityBasedOnSerialization { final boolean useStdOut; State(Project project) { - var pathsString = project.getProviders().gradleProperty(PROPERTY).getOrNull(); + var pathsString = GradleCompat.findOptionalProperty(project, PROPERTY); if (pathsString != null) { - useStdIn = project.getProviders().gradleProperty(USE_STD_IN).isPresent(); - useStdOut = project.getProviders().gradleProperty(USE_STD_OUT).isPresent(); + useStdIn = GradleCompat.isPropertyPresent(project, USE_STD_IN); + useStdOut = GradleCompat.isPropertyPresent(project, USE_STD_OUT); paths = Arrays.stream(pathsString.split(",")) .map(String::trim) .filter(s -> !s.isEmpty()) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java index 1583cf3d67..2bd773f2d3 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2026 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ public void apply(Project project) { + "https://docs.gradle.org/current/userguide/building_java_projects.html#sec:java_cross_compilation"); } // if -PspotlessModern=true, then use the modern stuff instead of the legacy stuff - if (project.getProviders().gradleProperty(SPOTLESS_MODERN).isPresent()) { + if (GradleCompat.isPropertyPresent(project, SPOTLESS_MODERN)) { project.getLogger().warn("'spotlessModern' has no effect as of Spotless 5.0, recommend removing it."); } // make sure there's a `clean` and a `check` From 4e9839f75beb9bd6aed37e59434626310f6550a4 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 2 Mar 2026 17:09:21 +0800 Subject: [PATCH 09/12] Clean up GradleCompat --- .../java/com/diffplug/gradle/spotless/GradleCompat.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java index 5c70413378..496b0286d1 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleCompat.java @@ -24,10 +24,6 @@ public final class GradleCompat { private GradleCompat() {} @Nullable public static String findOptionalProperty(Project project, String propertyName) { - @Nullable String value = project.getProviders().gradleProperty(propertyName).getOrNull(); - if (value != null) { - return value; - } ExtraPropertiesExtension extras = project.getExtensions().getByType(ExtraPropertiesExtension.class); if (extras.has(propertyName)) { @Nullable Object property = extras.get(propertyName); @@ -39,7 +35,6 @@ private GradleCompat() {} } public static boolean isPropertyPresent(Project project, String propertyName) { - return project.getProviders().gradleProperty(propertyName).isPresent() || - project.getExtensions().getByType(ExtraPropertiesExtension.class).has(propertyName); + return project.getExtensions().getByType(ExtraPropertiesExtension.class).has(propertyName); } } From bc5786840cc7c13d94afd96c7ac0e6f7b5c93c18 Mon Sep 17 00:00:00 2001 From: inorichi Date: Mon, 2 Mar 2026 10:25:43 +0100 Subject: [PATCH 10/12] Use gradle arguments for isolated projects tests --- .../java/com/diffplug/gradle/spotless/MultiProjectTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 b9ffd83a50..9366efe517 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 @@ -154,7 +154,6 @@ public void predeclaredUndeclared() throws IOException { @Test void nonPredeclaredSupportsIsolatedProjects() throws IOException { - setFile("gradle.properties").toContent("org.gradle.unsafe.isolated-projects=true"); setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -168,12 +167,11 @@ void nonPredeclaredSupportsIsolatedProjects() throws IOException { " }", "}"); createNSubprojects(); - gradleRunner().withArguments("spotlessApply").build(); + gradleRunner().withArguments("spotlessApply", "-Dorg.gradle.unsafe.isolated-projects=true").build(); } @Test void predeclaredRequiresNonIsolatedProjects() throws IOException { - setFile("gradle.properties").toContent("org.gradle.unsafe.isolated-projects=true"); setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -184,7 +182,7 @@ void predeclaredRequiresNonIsolatedProjects() throws IOException { " java { googleJavaFormat('1.17.0') }", "}"); createNSubprojects(); - Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) + Assertions.assertThat(gradleRunner().withArguments("spotlessApply", "-Dorg.gradle.unsafe.isolated-projects=true").buildAndFail().getOutput()) .containsAnyOf("Cannot access project", "cannot access 'Project.tasks'"); } } From c33648342eb58e25ab7f07e6e20df69378f2019e Mon Sep 17 00:00:00 2001 From: inorichi Date: Mon, 2 Mar 2026 11:17:49 +0100 Subject: [PATCH 11/12] Use root project layout for retrieving editorconfig --- .../com/diffplug/gradle/spotless/BaseKotlinExtension.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java index b916b56550..1772d27376 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java @@ -25,8 +25,6 @@ import javax.annotation.Nullable; -import org.gradle.api.file.Directory; - import com.diffplug.common.collect.ImmutableList; import com.diffplug.common.collect.ImmutableSortedMap; import com.diffplug.spotless.FileSignature; @@ -162,9 +160,7 @@ private KtlintConfig( Map editorConfigOverride, List customRuleSets) throws IOException { Objects.requireNonNull(version); - @SuppressWarnings("UnstableApiUsage") - Directory rootProjectDir = getProject().getIsolated().getRootProject().getProjectDirectory(); - File defaultEditorConfig = rootProjectDir.file(".editorconfig").getAsFile(); + File defaultEditorConfig = getProject().getRootProject().getLayout().getProjectDirectory().file(".editorconfig").getAsFile(); FileSignature editorConfigPath = defaultEditorConfig.exists() ? FileSignature.signAsList(defaultEditorConfig) : null; this.version = version; this.editorConfigPath = editorConfigPath; From 25191904526b094e29b3b38fede1cd4dde097c54 Mon Sep 17 00:00:00 2001 From: inorichi Date: Mon, 2 Mar 2026 16:29:30 +0100 Subject: [PATCH 12/12] Move isolated project tests to separate file and fix ktlint incompatibility --- .../gradle/spotless/BaseKotlinExtension.java | 2 +- .../gradle/spotless/IsolatedProjectTest.java | 102 ++++++++++++++++++ .../gradle/spotless/MultiProjectTest.java | 36 +------ 3 files changed, 104 insertions(+), 36 deletions(-) create mode 100644 plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IsolatedProjectTest.java diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java index 1772d27376..830cec07b1 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java @@ -160,7 +160,7 @@ private KtlintConfig( Map editorConfigOverride, List customRuleSets) throws IOException { Objects.requireNonNull(version); - File defaultEditorConfig = getProject().getRootProject().getLayout().getProjectDirectory().file(".editorconfig").getAsFile(); + File defaultEditorConfig = new File(getProject().getRootProject().getProjectDir(), ".editorconfig"); FileSignature editorConfigPath = defaultEditorConfig.exists() ? FileSignature.signAsList(defaultEditorConfig) : null; this.version = version; this.editorConfigPath = editorConfigPath; 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 new file mode 100644 index 0000000000..e9c1af7197 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IsolatedProjectTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2026 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.gradle.spotless; + +import java.io.IOException; + +import org.assertj.core.api.Assertions; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.Test; + +import com.diffplug.common.base.StringPrinter; + +class IsolatedProjectTest extends GradleIntegrationHarness { + private static final int N = 10; + + @Override + public GradleRunner gradleRunner() throws IOException { + setFile("gradle.properties").toContent("org.gradle.unsafe.isolated-projects=true"); + return super.gradleRunner(); + } + + private void createNSubprojects() throws IOException { + for (int i = 0; i < N; i++) { + createSubproject(Integer.toString(i)); + } + String settings = StringPrinter.buildString(printer -> { + for (int i = 0; i < N; i++) { + printer.println("include '" + i + "'"); + } + }); + setFile("settings.gradle").toContent(settings); + } + + void createSubproject(String name) throws IOException { + setFile(name + "/build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " kotlin {", + " target file('Test.kt')", + " ktlint()", + " }", + "}"); + setFile(name + "/Test.kt").toResource("kotlin/ktlint/basic.dirty"); + } + + @Test + void rootIsSupported() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " kotlin {", + " target file('Test.kt')", + " ktlint()", + " }", + "}"); + setFile("Test.kt").toResource("kotlin/ktlint/basic.dirty"); + createNSubprojects(); + gradleRunner().withArguments("spotlessApply").build(); + } + + @Test + void noRootIsSupported() throws IOException { + setFile("build.gradle").toLines(); + createNSubprojects(); + gradleRunner().withArguments("spotlessApply").build(); + } + + @Test + void predeclaredIsUnsupported() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless { predeclareDeps() }", + "spotlessPredeclare {", + " kotlin { ktlint() }", + "}"); + createNSubprojects(); + Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) + .containsAnyOf("Cannot access project", "cannot access 'Project.tasks'"); + } +} 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 9366efe517..69301327c7 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-2026 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -151,38 +151,4 @@ public void predeclaredUndeclared() throws IOException { Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) .contains("Could not find method spotlessPredeclare() for arguments"); } - - @Test - void nonPredeclaredSupportsIsolatedProjects() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "", - "spotless {", - " java {", - " target file('test.java')", - " googleJavaFormat('1.17.0')", - " }", - "}"); - createNSubprojects(); - gradleRunner().withArguments("spotlessApply", "-Dorg.gradle.unsafe.isolated-projects=true").build(); - } - - @Test - void predeclaredRequiresNonIsolatedProjects() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'com.diffplug.spotless'", - "}", - "repositories { mavenCentral() }", - "spotless { predeclareDeps() }", - "spotlessPredeclare {", - " java { googleJavaFormat('1.17.0') }", - "}"); - createNSubprojects(); - Assertions.assertThat(gradleRunner().withArguments("spotlessApply", "-Dorg.gradle.unsafe.isolated-projects=true").buildAndFail().getOutput()) - .containsAnyOf("Cannot access project", "cannot access 'Project.tasks'"); - } }