Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
2 changes: 2 additions & 0 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a name="preview"></a>

## How do I preview what `spotlessApply` will do?
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -160,7 +160,7 @@ private KtlintConfig(
Map<String, Object> editorConfigOverride,
List<String> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1098,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() : "");
}

Expand Down Expand Up @@ -1134,7 +1135,7 @@ public TaskProvider<SpotlessApply> createIndependentApplyTaskLazy(String taskNam
"Task name must not end with " + SpotlessExtension.APPLY);
TaskProvider<SpotlessTaskImpl> 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);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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> spotlessTaskService;

protected static final String TASK_GROUP = LifecycleBasePlugin.VERIFICATION_GROUP;
protected static final String BUILD_SETUP_TASK_GROUP = "build setup";
Expand All @@ -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<SpotlessTaskService> getSpotlessTaskService() {
return spotlessTaskService;
}

/** Line endings (if any). */
Expand Down Expand Up @@ -303,27 +302,6 @@ <T extends FormatExtension> T instantiateFormatExtension(Class<T> clazz) {

protected abstract void createFormatTasks(String name, FormatExtension formatExtension);

TaskProvider<RegisterDependenciesTask> 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<RegisterDependenciesTask> 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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) {
// create the SpotlessTask
String taskName = EXTENSION + SpotlessPlugin.capitalize(name);
TaskProvider<SpotlessTaskImpl> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, FormatExtension> 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<FormatExtension> 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);
}));
}

Expand All @@ -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<RegisterDependenciesTask> 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<RegisterDependenciesTask> findRegisterDepsTask(String taskName) {
return project.getTasks().register(taskName, RegisterDependenciesTask.class, RegisterDependenciesTask::setup);
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,6 +56,7 @@
* apply already did).
*/
public abstract class SpotlessTaskService implements BuildService<BuildServiceParameters.None>, AutoCloseable, OperationCompletionListener {
protected boolean isUsingPredeclared = false;
private final Map<String, SpotlessApply> apply = Collections.synchronizedMap(new HashMap<>());
private final Map<String, SpotlessTask> source = Collections.synchronizedMap(new HashMap<>());
private final Map<String, Provisioner> provisioner = Collections.synchronizedMap(new HashMap<>());
Expand Down Expand Up @@ -126,6 +128,21 @@ static void usesServiceTolerateTestFailure(DefaultTask task, Provider<SpotlessTa
}
}

public void hookSubprojectTask(Project project, SpotlessTask task) {
// This check allows isolated projects support by not accessing the root project tasks unless really needed
if (!isUsingPredeclared)
return;

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

public static Provider<SpotlessTaskService> registerIfAbsent(Project project, String suffix) {
return project.getGradle().getSharedServices()
.registerIfAbsent("SpotlessTaskService" + suffix, SpotlessTaskService.class, spec -> {});
}

abstract static class ClientTask extends DefaultTask {
@Internal
abstract Property<File> getSpotlessCleanDirectory();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -197,7 +197,7 @@ private void taskIsUpToDate(String task, boolean upToDate) throws IOException {

List<String> expected = outcomes(buildResult, upToDate ? TaskOutcome.UP_TO_DATE : TaskOutcome.SUCCESS);
List<String> 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));
}
Expand Down
Loading