diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 7598404841..b07bfb59db 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -12,6 +12,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Changes - Bump default `cleanthat` version `2.24` -> `2.25`. ([#2903](https://github.com/diffplug/spotless/pull/2903)) - Bump default `eclipse-jdt` version from `4.35` to `4.39`. ([#2912](https://github.com/diffplug/spotless/pull/2912)) +- Make `spotlessPredeclare` visible to Gradle Kotlin DSL type-safe accessors. ([#2925](https://github.com/diffplug/spotless/pull/2925)) +- Allow `spotlessPredeclare` to be used directly without enabling it first in spotless extension. ([#2925](https://github.com/diffplug/spotless/pull/2925)) ## [8.4.0] - 2026-03-18 ### Added diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 3efa2f2438..9f4e4d04d2 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -1959,17 +1959,26 @@ spotless { By default, Spotless resolves dependencies on a per-project basis. For very large parallel builds, this can sometimes cause problems. As an alternative, Spotless can be configured to resolve all dependencies in the root project like so: ```gradle -spotless { - ... - predeclareDeps() +spotlessPredeclare { + java { eclipse() } + kotlin { ktfmt('0.28') } +} +``` + +By default, `spotlessPredeclare` resolves dependencies from the root project's repositories. Alternatively, you can resolve dependencies from the buildscript repositories rather than the project repositories: + +```gradle +buildscript { + repositories { mavenCentral() } } spotlessPredeclare { + fromBuildscriptRepositories() java { eclipse() } kotlin { ktfmt('0.28') } } ``` -Alternatively, you can also use `predeclareDepsFromBuildscript()` to resolve the dependencies from the buildscript repositories rather than the project repositories. +The older `spotless { predeclareDeps() }` and `spotless { predeclareDepsFromBuildscript() }` APIs are still supported. 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. 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 26e91eba8c..e9acc00c24 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 @@ -308,21 +308,46 @@ T instantiateFormatExtension(Class clazz) { protected abstract void createFormatTasks(String name, FormatExtension formatExtension); + /** + * Enables predeclared dependency resolution using the root project's {@code buildscript} repositories. + * + * @deprecated Configure the repository policy directly in {@code spotlessPredeclare} instead: + *
{@code
+	 *             spotlessPredeclare {
+	 *                 fromBuildscriptRepositories()
+	 *                 java { googleJavaFormat("1.17.0") }
+	 *             }
+	 *             }
+ */ + @Deprecated public void predeclareDepsFromBuildscript() { if (project.getRootProject() != project) { throw new GradleException("predeclareDepsFromBuildscript can only be called from the root project"); } + project.getLogger().info("predeclareDepsFromBuildscript() is deprecated, use 'spotlessPredeclare { fromBuildscriptRepositories() }' directly instead."); predeclare(GradleProvisioner.Policy.ROOT_BUILDSCRIPT); } + /** + * Enables predeclared dependency resolution using the root project's repositories. + * + * @deprecated Declare formats directly in {@code spotlessPredeclare} instead: + *
{@code
+	 *             spotlessPredeclare {
+	 *                 java { googleJavaFormat("1.17.0") }
+	 *             }
+	 *             }
+ */ + @Deprecated public void predeclareDeps() { if (project.getRootProject() != project) { throw new GradleException("predeclareDeps can only be called from the root project"); } + project.getLogger().info("predeclareDeps() is deprecated, use 'spotlessPredeclare { ... }' directly instead."); predeclare(GradleProvisioner.Policy.ROOT_PROJECT); } - protected void predeclare(GradleProvisioner.Policy policy) { - project.getExtensions().create(SpotlessExtensionPredeclare.class, EXTENSION_PREDECLARE, SpotlessExtensionPredeclare.class, project, policy); + void predeclare(GradleProvisioner.Policy policy) { + project.getExtensions().getByType(SpotlessExtensionPredeclare.class).enablePredeclare(policy); } } 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 ef8589523f..ad3f69e848 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 @@ -18,7 +18,10 @@ import java.util.SortedMap; import java.util.TreeMap; +import javax.annotation.Nullable; + import org.gradle.api.Action; +import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.tasks.TaskProvider; @@ -26,35 +29,98 @@ public class SpotlessExtensionPredeclare extends SpotlessExtension { private final SortedMap toSetup = new TreeMap<>(); - private final RegisterDependenciesTask registerDependenciesTask; + private @Nullable GradleProvisioner.Policy policy; + private boolean policyExplicit; + private @Nullable RegisterDependenciesTask registerDependenciesTask; - public SpotlessExtensionPredeclare(Project project, GradleProvisioner.Policy policy) { + public SpotlessExtensionPredeclare(Project project) { super(project); - this.registerDependenciesTask = findRegisterDepsTask().get(); - SpotlessTaskService taskService = getSpotlessTaskService().get(); - taskService.registerDependenciesTask = registerDependenciesTask; - 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); + project.afterEvaluate(unused -> { + if (policy == null) { + return; + } + RegisterDependenciesTask task = registerDependenciesTask; + if (task == null) { + throw new IllegalStateException("spotlessPredeclare was enabled without a register dependencies task."); } - 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(registerDependenciesTask.steps); - })); + toSetup.forEach((name, formatExtension) -> { + for (Action lazyAction : formatExtension.lazyActions) { + lazyAction.execute(formatExtension); + } + task.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(task.steps); + }); + }); } @Override protected void createFormatTasks(String name, FormatExtension formatExtension) { + enablePredeclareIfAbsent(GradleProvisioner.Policy.ROOT_PROJECT); toSetup.put(name, formatExtension); } @Override - protected void predeclare(GradleProvisioner.Policy policy) { + void predeclare(GradleProvisioner.Policy policy) { throw new UnsupportedOperationException("predeclare can't be called from within `" + EXTENSION_PREDECLARE + "`"); } + /** + * Resolves predeclared dependencies from the root project's repositories. + *

+ * This is the default behavior when any format is declared in {@code spotlessPredeclare}. + * + * @see SpotlessExtension#predeclareDeps() + */ + public void fromProjectRepositories() { + enablePredeclare(GradleProvisioner.Policy.ROOT_PROJECT); + } + + /** + * Resolves predeclared dependencies from the root project's {@code buildscript} repositories. + *

+ * Use this when formatter dependencies should be resolved from {@code buildscript { repositories { ... } }} + * instead of the root project's normal repositories. + * + * @see SpotlessExtension#predeclareDepsFromBuildscript() + */ + public void fromBuildscriptRepositories() { + enablePredeclare(GradleProvisioner.Policy.ROOT_BUILDSCRIPT); + } + + void enablePredeclare(GradleProvisioner.Policy policy) { + enablePredeclare(policy, true); + } + + private void enablePredeclareIfAbsent(GradleProvisioner.Policy policy) { + enablePredeclare(policy, false); + } + + private void enablePredeclare(GradleProvisioner.Policy policy, boolean explicit) { + if (this.policy != null) { + if (!explicit || !policyExplicit) { + if (explicit) { + this.policy = policy; + this.policyExplicit = true; + configureProvisioners(policy); + } + return; + } + throw new GradleException("predeclared dependency resolution can only be configured once."); + } + this.policy = policy; + this.policyExplicit = explicit; + registerDependenciesTask = findRegisterDepsTask().get(); + configureProvisioners(policy); + } + + private void configureProvisioners(GradleProvisioner.Policy policy) { + SpotlessTaskService taskService = getSpotlessTaskService().get(); + taskService.registerDependenciesTask = registerDependenciesTask; + taskService.predeclaredProvisioner = policy.dedupingProvisioner(project); + taskService.predeclaredP2Provisioner = policy.dedupingP2Provisioner(project); + } + private TaskProvider findRegisterDepsTask() { try { return findRegisterDepsTask(RegisterDependenciesTask.TASK_NAME); 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..a5d0e19e7a 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. @@ -50,6 +50,9 @@ public void apply(Project project) { // setup the extension project.getExtensions().create(SpotlessExtension.class, SpotlessExtension.EXTENSION, SpotlessExtensionImpl.class, project); + if (project.getRootProject() == project) { + project.getExtensions().create(SpotlessExtensionPredeclare.class, SpotlessExtension.EXTENSION_PREDECLARE, SpotlessExtensionPredeclare.class, project); + } // clear spotless' cache when the user does a clean // resolution for: https://github.com/diffplug/spotless/issues/243#issuecomment-564323856 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 35bf2708b4..3403bbfaad 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 @@ -92,37 +92,60 @@ public void predeclaredFails() throws IOException { } @Test - public void predeclaredSucceeds() throws IOException { + public void predeclaredSucceeds_deprecatedAPI() throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless { predeclareDeps() }", + "", "spotlessPredeclare {", - " java { googleJavaFormat('1.17.0') }", + " java { googleJavaFormat('1.17.0') }", "}"); createNSubprojects(); gradleRunner().withArguments("spotlessApply").build(); } @Test - public void predeclaredFromBuildscriptSucceeds() throws IOException { + public void predeclaredFromBuildscriptSucceeds_deprecatedAPI() throws IOException { setFile("build.gradle").toLines( + "buildscript {", + " repositories { mavenCentral() }", + "}", "plugins {", " id 'com.diffplug.spotless'", "}", "repositories { mavenCentral() }", "spotless { predeclareDepsFromBuildscript() }", "spotlessPredeclare {", - " java { googleJavaFormat('1.17.0') }", + " java { googleJavaFormat('1.17.0') }", + "}"); + createNSubprojects(); + gradleRunner().withArguments("spotlessApply").build(); + } + + @Test + public void predeclaredFromBuildscriptInPredeclareBlockSucceeds() throws IOException { + setFile("build.gradle").toLines( + "buildscript {", + " repositories { mavenCentral() }", + "}", + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "", + "spotlessPredeclare {", + " fromBuildscriptRepositories()", + " java { googleJavaFormat('1.17.0') }", "}"); createNSubprojects(); gradleRunner().withArguments("spotlessApply").build(); } @Test - public void predeclaredOrdering() throws IOException { + public void predeclaredOrderingIsFlexible() throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -131,10 +154,9 @@ public void predeclaredOrdering() throws IOException { "spotlessPredeclare {", " java { googleJavaFormat('1.17.0') }", "}", - "spotless { predeclareDepsFromBuildscript() }"); + "spotless { predeclareDeps() }"); createNSubprojects(); - Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) - .contains("Could not find method spotlessPredeclare() for arguments"); + gradleRunner().withArguments("spotlessApply").build(); } @Test @@ -168,7 +190,7 @@ public void predeclaredDepsRegression() throws IOException { } @Test - public void predeclaredUndeclared() throws IOException { + public void predeclaredWithoutSpotlessBlockSucceeds() throws IOException { setFile("build.gradle").toLines( "plugins {", " id 'com.diffplug.spotless'", @@ -178,7 +200,6 @@ public void predeclaredUndeclared() throws IOException { " java { googleJavaFormat('1.17.0') }", "}"); createNSubprojects(); - Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput()) - .contains("Could not find method spotlessPredeclare() for arguments"); + gradleRunner().withArguments("spotlessApply").build(); } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java index c1bfcf906e..4970b6bb5b 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/SpotlessPredeclareIntegrationTest.java @@ -511,38 +511,68 @@ target file('test.java') @Nested class EdgeCases { @Test - void predeclareRequiresPredeclareDepsCall() throws IOException { + void predeclareBlockEnablesPredeclareDeps() throws IOException { setFile("build.gradle").toContent(""" plugins { id 'com.diffplug.spotless' } repositories { mavenCentral() } spotlessPredeclare { - java { googleJavaFormat('1.17.0') } + format('misc') { trimTrailingWhitespace() } } """); - BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); - assertThat(result.getOutput()) - .contains("Could not find method spotlessPredeclare() for arguments"); + BuildResult result = gradleRunner().withArguments("help").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); } @Test - void predeclareBlockMustComeAfterPredeclareDeps() throws IOException { + void predeclareBlockCanComeBeforePredeclareDeps() throws IOException { setFile("build.gradle").toContent(""" plugins { id 'com.diffplug.spotless' } repositories { mavenCentral() } spotlessPredeclare { - java { googleJavaFormat('1.17.0') } + format('misc') { trimTrailingWhitespace() } } spotless { predeclareDeps() } """); - BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); - assertThat(result.getOutput()) - .contains("Could not find method spotlessPredeclare() for arguments"); + BuildResult result = gradleRunner().withArguments("help").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + } + + @Test + void predeclareBlockHasKotlinDslAccessor() throws IOException { + setFile("build.gradle.kts").toContent(""" + plugins { + id("com.diffplug.spotless") + } + repositories { mavenCentral() } + spotlessPredeclare { + format("misc") { trimTrailingWhitespace() } + } + """); + + BuildResult result = gradleRunner().withArguments("help").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); + } + + @Test + void predeclareBlockCanSelectBuildscriptRepositories() throws IOException { + setFile("build.gradle.kts").toContent(""" + plugins { + id("com.diffplug.spotless") + } + spotlessPredeclare { + format("misc") { trimTrailingWhitespace() } + fromBuildscriptRepositories() + } + """); + + BuildResult result = gradleRunner().withArguments("help").build(); + assertThat(result.getOutput()).contains("BUILD SUCCESSFUL"); } @Test