diff --git a/.circleci/config.yml b/.circleci/config.yml
index 2fe588a448..48ba552c3e 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -108,14 +108,14 @@ jobs:
- *restore_cache_wrapper
- *restore_cache_deps
- run:
- name: gradlew npmTest
- command: export SPOTLESS_EXCLUDE_MAVEN=true && ./gradlew npmTest --build-cache
+ name: gradlew testNpm
+ command: export SPOTLESS_EXCLUDE_MAVEN=true && ./gradlew testNpm --build-cache
- store_test_results:
- path: testlib/build/test-results/NpmTest
+ path: testlib/build/test-results/testNpm
- store_test_results:
- path: plugin-maven/build/test-results/NpmTest
+ path: plugin-maven/build/test-results/testNpm
- store_test_results:
- path: plugin-gradle/build/test-results/NpmTest
+ path: plugin-gradle/build/test-results/testNpm
- run:
name: gradlew test
command: export SPOTLESS_EXCLUDE_MAVEN=true && ./gradlew test --build-cache
@@ -141,12 +141,12 @@ jobs:
- store_test_results:
path: plugin-gradle/build/test-results/test
- run:
- name: gradlew npmTest
- command: gradlew npmTest --build-cache -PSPOTLESS_EXCLUDE_MAVEN=true
+ name: gradlew testNpm
+ command: gradlew testNpm --build-cache -PSPOTLESS_EXCLUDE_MAVEN=true
- store_test_results:
- path: testlib/build/test-results/NpmTest
+ path: testlib/build/test-results/testNpm
- store_test_results:
- path: plugin-gradle/build/test-results/NpmTest
+ path: plugin-gradle/build/test-results/testNpm
changelog_print:
<< : *env_gradle
steps:
diff --git a/gradle/special-tests.gradle b/gradle/special-tests.gradle
index 5ba7b3b2c6..c435bc7e81 100644
--- a/gradle/special-tests.gradle
+++ b/gradle/special-tests.gradle
@@ -1,5 +1,5 @@
apply plugin: 'org.gradle.test-retry'
-
+apply plugin: 'com.adarshr.test-logger'
def special = [
'Npm',
'Black',
@@ -21,7 +21,7 @@ tasks.named('test') {
}
special.forEach { tag ->
- tasks.register("${tag}Test", Test) {
+ tasks.register("test${tag}", Test) {
useJUnitPlatform { includeTags tag }
}
}
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index 89aab234cf..ad93e65328 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -3,6 +3,23 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
## [Unreleased]
+### Added
+* You can now predeclare formatter dependencies in the root project.
+ * specify one of:
+ * `spotless { predeclareDeps() }` to resolve all deps from the root project, which will show up in dependency reports.
+ * `spotless { predeclareDepsFromBuildscript() }` to resolve all deps from `buildscript { repositories {`, which will not show up in dependency reports ([see #1027](https://github.com/diffplug/spotless/issues/1027)).
+ * and then below that you have a block where you simply declare each formatter which you are using, e.g.
+ * ```
+ spotless {
+ ...
+ predeclareDepsFromBuildscript()
+ }
+ spotlessPredeclare {
+ java { eclipse() }
+ kotlin { ktfmt('0.28') }
+ }
+ ```
+ * By default, Spotless resolves all dependencies per-project, and the predeclaration above is unnecessary in the vast majority of cases.
## [6.0.5] - 2021-12-16
### Fixed
@@ -42,7 +59,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
* To make this daemon-restriction obsolete, please see and upvote [#987](https://github.com/diffplug/spotless/issues/987).
### Changed
* **BREAKING** Previously, many projects required `buildscript { repositories { mavenCentral() }}` at the top of their root project, because Spotless resolved its dependencies using the buildscript repositories. Spotless now resolves its dependencies from the normal project repositories of each project with a `spotless {...}` block. This means that you can remove the `buildscript {}` block, but you still need a `repositories { mavenCentral() }` (or similar) in each project which is using Spotless. ([#980](https://github.com/diffplug/spotless/pull/980), [#983](https://github.com/diffplug/spotless/pull/983))
- * If you prefer the old behavior, we are open to adding that back as a new feature, see [#984 predeclared dependencies](https://github.com/diffplug/spotless/issues/984).
+ * If you prefer the old behavior, it is available via [`predeclareDepsFromBuildscript()` starting in `6.1.0`](../README.md#dependency-resolution-modes).
* **BREAKING** `createIndepentApplyTask(String taskName)` now requires that `taskName` does not end with `Apply`
* Bump minimum required Gradle from `6.1` to `6.1.1`.
* Bump default formatter versions ([#989](https://github.com/diffplug/spotless/pull/989))
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index 821ca6c053..6edb03d322 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -86,6 +86,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
- [Multiple (or custom) language-specific blocks](#multiple-or-custom-language-specific-blocks)
- [Inception (languages within languages within...)](#inception-languages-within-languages-within)
- [Disabling warnings and error messages](#disabling-warnings-and-error-messages)
+ - [Dependency resolution modes](#dependency-resolution-modes)
- [How do I preview what `spotlessApply` will do?](#how-do-i-preview-what-spotlessapply-will-do)
- [Example configurations (from real-world projects)](#example-configurations-from-real-world-projects)
@@ -910,6 +911,26 @@ spotless {
ignoreErrorForPath('path/to/file.java') // ignore errors by all steps on this specific file
```
+
+## Dependency resolution modes
+
+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') }
+}
+```
+
+Alternatively, you can also use `predeclareDepsFromBuildscript()` to resolve the dependencies from the buildscript repositories rather than the project repositories.
+
+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.
+
## How do I preview what `spotlessApply` will do?
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 b09a4af3d0..9da66b7929 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
@@ -73,7 +73,7 @@ public FormatExtension(SpotlessExtension spotless) {
}
protected final Provisioner provisioner() {
- return spotless.getRegisterDependenciesTask().getTaskService().get().provisionerFor(spotless.project);
+ return spotless.getRegisterDependenciesTask().getTaskService().get().provisionerFor(spotless);
}
private String formatName() {
@@ -757,16 +757,6 @@ protected void setupTask(SpotlessTask task) {
} else {
steps = this.steps;
}
- //
- // By calling .hashCode, we are triggering all steps to evaluate their state,
- // which triggers dependency resolution. It's important to do that here, because
- // otherwise it won't happen until Gradle starts checking for task up-to-date-ness.
- // For a large parallel build, the task up-to-dateness might get called on a different
- // thread than the thread where task configuration happens, which will trigger a
- // java.util.ConcurrentModificationException
- // See https://github.com/diffplug/spotless/issues/1015 for details.
- steps.hashCode();
- //
task.setSteps(steps);
task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> totalTarget));
spotless.getRegisterDependenciesTask().hookSubprojectTask(task);
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java
index 0587d070f6..240ffcdcdf 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/GradleProvisioner.java
@@ -19,15 +19,19 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
-import java.util.Objects;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
+import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.ConfigurationContainer;
+import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.attributes.Bundling;
+import org.gradle.api.initialization.dsl.ScriptHandler;
+import com.diffplug.common.base.Unhandled;
import com.diffplug.common.collect.ImmutableList;
import com.diffplug.spotless.Provisioner;
@@ -35,45 +39,82 @@
class GradleProvisioner {
private GradleProvisioner() {}
- static Provisioner newDedupingProvisioner(Project project) {
- return new DedupingProvisioner(project);
+ enum Policy {
+ INDEPENDENT, ROOT_PROJECT, ROOT_BUILDSCRIPT;
+
+ public DedupingProvisioner dedupingProvisioner(Project project) {
+ switch (this) {
+ case ROOT_PROJECT:
+ return new DedupingProvisioner(forProject(project));
+ case ROOT_BUILDSCRIPT:
+ return new DedupingProvisioner(forRootProjectBuildscript(project));
+ case INDEPENDENT:
+ default:
+ throw Unhandled.enumException(this);
+ }
+ }
}
static class DedupingProvisioner implements Provisioner {
- private final Project project;
+ private final Provisioner provisioner;
private final Map> cache = new HashMap<>();
- DedupingProvisioner(Project project) {
- this.project = project;
+ DedupingProvisioner(Provisioner provisioner) {
+ this.provisioner = provisioner;
}
@Override
public Set provisionWithTransitives(boolean withTransitives, Collection mavenCoordinates) {
Request req = new Request(withTransitives, mavenCoordinates);
- Set result = cache.get(req);
+ Set result;
+ synchronized (cache) {
+ result = cache.get(req);
+ }
if (result != null) {
return result;
} else {
- result = cache.get(req);
- if (result != null) {
- return result;
- } else {
- result = forProject(project).provisionWithTransitives(req.withTransitives, req.mavenCoords);
- cache.put(req, result);
+ synchronized (cache) {
+ result = cache.get(req);
+ if (result == null) {
+ result = provisioner.provisionWithTransitives(req.withTransitives, req.mavenCoords);
+ cache.put(req, result);
+ }
return result;
}
}
}
+
+ /** A child Provisioner which retries cached elements only. */
+ final Provisioner cachedOnly = (withTransitives, mavenCoordinates) -> {
+ Request req = new Request(withTransitives, mavenCoordinates);
+ Set result;
+ synchronized (cache) {
+ result = cache.get(req);
+ }
+ if (result != null) {
+ return result;
+ }
+ throw new GradleException("Add a step with " + req.mavenCoords + " into the `spotlessPredeclare` block in the root project.");
+ };
+ }
+
+ static Provisioner forProject(Project project) {
+ return forConfigurationContainer(project, project.getConfigurations(), project.getDependencies());
+ }
+
+ static Provisioner forRootProjectBuildscript(Project project) {
+ Project rootProject = project.getRootProject();
+ ScriptHandler buildscript = rootProject.getBuildscript();
+ return forConfigurationContainer(rootProject, buildscript.getConfigurations(), buildscript.getDependencies());
}
- private static Provisioner forProject(Project project) {
- Objects.requireNonNull(project);
+ private static Provisioner forConfigurationContainer(Project project, ConfigurationContainer configurations, DependencyHandler dependencies) {
return (withTransitives, mavenCoords) -> {
try {
- Configuration config = project.getConfigurations().create("spotless"
+ Configuration config = configurations.create("spotless"
+ new Request(withTransitives, mavenCoords).hashCode());
mavenCoords.stream()
- .map(project.getDependencies()::create)
+ .map(dependencies::create)
.forEach(config.getDependencies()::add);
config.setDescription(mavenCoords.toString());
config.setTransitive(withTransitives);
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 e2a5145b02..a3fbdf4a81 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
@@ -15,16 +15,26 @@
*/
package com.diffplug.gradle.spotless;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.provider.Property;
import org.gradle.api.services.BuildServiceRegistry;
+import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
+import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.build.event.BuildEventsListenerRegistry;
import com.diffplug.common.base.Preconditions;
+import com.diffplug.common.io.Files;
+import com.diffplug.spotless.FormatterStep;
/**
* NOT AN END-USER TASK, DO NOT USE FOR ANYTHING!
@@ -40,11 +50,10 @@ public abstract class RegisterDependenciesTask extends DefaultTask {
static final String TASK_NAME = "spotlessInternalRegisterDependencies";
void hookSubprojectTask(SpotlessTask task) {
- // TODO: in the future, we might use this hook to implement #984
- // spotlessSetup {
- // java { googleJavaFormat('1.2') }
- // ...etc
- // }
+ // this ensures that if a user is using predeclared dependencies,
+ // those predeclared deps will be resolved before they are needed
+ // by the child tasks
+ //
// it's also needed to make sure that jvmLocalCache gets set
// in the SpotlessTaskService before any spotless tasks run
task.dependsOn(this);
@@ -56,11 +65,27 @@ void setup() {
BuildServiceRegistry buildServices = getProject().getGradle().getSharedServices();
getTaskService().set(buildServices.registerIfAbsent("SpotlessTaskService" + compositeBuildSuffix, SpotlessTaskService.class, spec -> {}));
getBuildEventsListenerRegistry().onTaskCompletion(getTaskService());
+ unitOutput = new File(getProject().getBuildDir(), "tmp/spotless-register-dependencies");
+ }
+
+ List steps = new ArrayList<>();
+
+ @Input
+ public List getSteps() {
+ return steps;
+ }
+
+ File unitOutput;
+
+ @OutputFile
+ public File getUnitOutput() {
+ return unitOutput;
}
@TaskAction
- public void trivialFunction() {
- // nothing to do :)
+ public void trivialFunction() throws IOException {
+ Files.createParentDirs(unitOutput);
+ Files.write(Integer.toString(1), unitOutput, StandardCharsets.UTF_8);
}
@Internal
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 52edf06e66..4b7c84343f 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
@@ -27,26 +27,33 @@
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 com.diffplug.spotless.LineEnding;
public abstract class SpotlessExtension {
final Project project;
+ private final RegisterDependenciesTask registerDependenciesTask;
protected static final String TASK_GROUP = "Verification";
protected static final String CHECK_DESCRIPTION = "Checks that sourcecode satisfies formatting steps.";
protected static final String APPLY_DESCRIPTION = "Applies code formatting steps to sourcecode in-place.";
static final String EXTENSION = "spotless";
+ static final String EXTENSION_PREDECLARE = "spotlessPredeclare";
static final String CHECK = "Check";
static final String APPLY = "Apply";
static final String DIAGNOSE = "Diagnose";
protected SpotlessExtension(Project project) {
this.project = requireNonNull(project);
+ this.registerDependenciesTask = findRegisterDepsTask().get();
}
- abstract RegisterDependenciesTask getRegisterDependenciesTask();
+ RegisterDependenciesTask getRegisterDependenciesTask() {
+ return registerDependenciesTask;
+ }
/** Line endings (if any). */
LineEnding lineEndings = LineEnding.GIT_ATTRIBUTES;
@@ -231,4 +238,43 @@ 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");
+ }
+ predeclare(GradleProvisioner.Policy.ROOT_BUILDSCRIPT);
+ }
+
+ public void predeclareDeps() {
+ if (project.getRootProject() != project) {
+ throw new GradleException("predeclareDeps can only be called from the root project");
+ }
+ predeclare(GradleProvisioner.Policy.ROOT_PROJECT);
+ }
+
+ protected void predeclare(GradleProvisioner.Policy policy) {
+ project.getExtensions().create(SpotlessExtension.class, EXTENSION_PREDECLARE, SpotlessExtensionPredeclare.class, project, policy);
+ }
}
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 9473707d53..24fa7b54b0 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
@@ -23,7 +23,6 @@
import org.gradle.api.tasks.TaskProvider;
public class SpotlessExtensionImpl extends SpotlessExtension {
- private final TaskProvider registerDependenciesTask;
final TaskProvider> rootCheckTask, rootApplyTask, rootDiagnoseTask;
public SpotlessExtensionImpl(Project project) {
@@ -39,7 +38,6 @@ public SpotlessExtensionImpl(Project project) {
rootDiagnoseTask = project.getTasks().register(EXTENSION + DIAGNOSE, task -> {
task.setGroup(TASK_GROUP); // no description on purpose
});
- registerDependenciesTask = findRegisterDepsTask();
project.afterEvaluate(unused -> {
if (enforceCheck) {
@@ -53,31 +51,6 @@ public SpotlessExtensionImpl(Project project) {
});
}
- 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) {
- 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);
- }
- }
-
- RegisterDependenciesTask getRegisterDependenciesTask() {
- return registerDependenciesTask.get();
- }
-
@Override
protected void createFormatTasks(String name, FormatExtension formatExtension) {
boolean isIdeHook = project.hasProperty(IdeHook.PROPERTY);
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
new file mode 100644
index 0000000000..a086f1ca52
--- /dev/null
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionPredeclare.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 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.util.SortedMap;
+import java.util.TreeMap;
+
+import org.gradle.api.Action;
+import org.gradle.api.Project;
+
+public class SpotlessExtensionPredeclare extends SpotlessExtension {
+ private final SortedMap toSetup = new TreeMap<>();
+
+ public SpotlessExtensionPredeclare(Project project, GradleProvisioner.Policy policy) {
+ super(project);
+ getRegisterDependenciesTask().getTaskService().get().predeclaredProvisioner = policy.dedupingProvisioner(project);
+ project.afterEvaluate(unused -> {
+ toSetup.forEach((name, formatExtension) -> {
+ for (Action lazyAction : formatExtension.lazyActions) {
+ lazyAction.execute(formatExtension);
+ }
+ getRegisterDependenciesTask().steps.addAll(formatExtension.steps);
+ });
+ });
+ }
+
+ @Override
+ protected void createFormatTasks(String name, FormatExtension formatExtension) {
+ toSetup.put(name, formatExtension);
+ }
+
+ @Override
+ protected void predeclare(GradleProvisioner.Policy policy) {
+ throw new UnsupportedOperationException("predeclare can't be called from within `" + EXTENSION_PREDECLARE + "`");
+ }
+}
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 de016ae6d2..7fc0ea3775 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
@@ -20,10 +20,10 @@
import java.util.HashMap;
import java.util.Map;
+import javax.annotation.Nullable;
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
-import org.gradle.api.Project;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Property;
@@ -48,10 +48,19 @@ public abstract class SpotlessTaskService implements BuildService source = Collections.synchronizedMap(new HashMap<>());
private final Map provisioner = Collections.synchronizedMap(new HashMap<>());
- Provisioner provisionerFor(Project project) {
- return provisioner.computeIfAbsent(project.getPath(), unused -> {
- return GradleProvisioner.newDedupingProvisioner(project);
- });
+ @Nullable
+ GradleProvisioner.DedupingProvisioner predeclaredProvisioner;
+
+ Provisioner provisionerFor(SpotlessExtension spotless) {
+ if (spotless instanceof SpotlessExtensionPredeclare) {
+ return predeclaredProvisioner;
+ } else {
+ if (predeclaredProvisioner != null) {
+ return predeclaredProvisioner.cachedOnly;
+ } else {
+ return provisioner.computeIfAbsent(spotless.project.getPath(), unused -> new GradleProvisioner.DedupingProvisioner(GradleProvisioner.forProject(spotless.project)));
+ }
+ }
}
void registerSourceAlreadyRan(SpotlessTask task) {
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
new file mode 100644
index 0000000000..086a2ddb1d
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/MultiProjectTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2016-2021 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.junit.jupiter.api.Test;
+
+import com.diffplug.common.base.StringPrinter;
+
+class MultiProjectTest extends GradleIntegrationHarness {
+ private static int N = 100;
+
+ 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 {",
+ " java {",
+ " target file('test.java')",
+ " googleJavaFormat('1.2')",
+ " }",
+ "}");
+ setFile(name + "/test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test");
+ }
+
+ @Test
+ public void noRootSpotless() throws IOException {
+ createNSubprojects();
+ setFile("build.gradle").toLines();
+ applyIsUpToDate(false);
+ }
+
+ @Test
+ public void hasRootSpotless() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "",
+ "spotless {",
+ " java {",
+ " target file('test.java')",
+ " googleJavaFormat('1.2')",
+ " }",
+ "}");
+ setFile("test.java").toResource("java/googlejavaformat/JavaCodeUnformatted.test");
+ createNSubprojects();
+ applyIsUpToDate(false);
+ }
+
+ @Test
+ public void predeclaredFails() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "spotless { predeclareDeps() }");
+ createNSubprojects();
+ Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput())
+ .contains("Add a step with [com.google.googlejavaformat:google-java-format:1.2] into the `spotlessPredeclare` block in the root project.");
+ }
+
+ @Test
+ public void predeclaredSucceeds() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless { predeclareDeps() }",
+ "spotlessPredeclare {",
+ " java { googleJavaFormat('1.2') }",
+ "}");
+ createNSubprojects();
+ gradleRunner().withArguments("spotlessApply").build();
+ }
+
+ @Test
+ public void predeclaredFromBuildscriptSucceeds() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless { predeclareDepsFromBuildscript() }",
+ "spotlessPredeclare {",
+ " java { googleJavaFormat('1.2') }",
+ "}");
+ createNSubprojects();
+ gradleRunner().withArguments("spotlessApply").build();
+ }
+
+ @Test
+ public void predeclaredOrdering() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotlessPredeclare {",
+ " java { googleJavaFormat('1.2') }",
+ "}",
+ "spotless { predeclareDepsFromBuildscript() }");
+ createNSubprojects();
+ Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput())
+ .contains("Could not find method spotlessPredeclare() for arguments");
+ }
+
+ @Test
+ public void predeclaredUndeclared() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotlessPredeclare {",
+ " java { googleJavaFormat('1.2') }",
+ "}");
+ createNSubprojects();
+ Assertions.assertThat(gradleRunner().withArguments("spotlessApply").buildAndFail().getOutput())
+ .contains("Could not find method spotlessPredeclare() for arguments");
+ }
+}
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 a0026d2625..9b7eccdfff 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
@@ -88,8 +88,8 @@ 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(":spotlessMisc");
- Assertions.assertThat(buildResult.taskPaths(TaskOutcome.SUCCESS)).containsExactly(":spotlessInternalRegisterDependencies", ":spotlessMiscApply", ":spotlessApply");
+ Assertions.assertThat(buildResult.taskPaths(TaskOutcome.UP_TO_DATE)).containsExactly(":spotlessInternalRegisterDependencies", ":spotlessMisc");
+ Assertions.assertThat(buildResult.taskPaths(TaskOutcome.SUCCESS)).containsExactly(":spotlessMiscApply", ":spotlessApply");
assertFile("README.md").hasContent("abc");
// and it'll take two more runs to get to fully UP-TO-DATE
diff --git a/settings.gradle b/settings.gradle
index 02f4e7db8f..ea16d5e1bc 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -13,6 +13,8 @@ pluginManagement {
id 'com.diffplug.p2.asmaven' version '3.27.0' // DO NOT UPDATE, see https://github.com/diffplug/spotless/pull/874
// https://github.com/gradle/test-retry-gradle-plugin/releases
id 'org.gradle.test-retry' version '1.3.1'
+ // https://github.com/radarsh/gradle-test-logger-plugin/blob/develop/CHANGELOG.md
+ id 'com.adarshr.test-logger' version '3.1.0'
}
}
plugins {
@@ -23,6 +25,7 @@ plugins {
id 'com.diffplug.spotless-changelog' apply false
id 'com.diffplug.p2.asmaven' apply false
id 'org.gradle.test-retry' apply false
+ id 'com.adarshr.test-logger' apply false
}
if (System.env['CI'] != null) {
// use the remote buildcache on all CI builds