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]
### Fixed
- `spotlessCheck` violation message now suggests the correct composite/included-build task path (e.g. `./gradlew :my-utils:spotlessApply`) instead of a bare `spotlessApply` / `:spotlessApply` that does not select included-build tasks. ([#2421](https://github.com/diffplug/spotless/issues/2421))

## [8.9.0] - 2026-07-27
### Added
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 All @@ -25,9 +25,12 @@
import java.util.List;

import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileTree;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.FileVisitor;
import org.gradle.api.initialization.IncludedBuild;
import org.gradle.api.invocation.Gradle;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
Expand Down Expand Up @@ -139,10 +142,68 @@ void init(TaskProvider<SpotlessTaskImpl> impl) {
getProjectPath().set(getProject().getPath());
getEncoding().set(impl.map(SpotlessTask::getEncoding));
getRunToFixMessage().convention(
"Run '" + calculateGradleCommand() + " spotlessApply' to fix all violations.");
"Run '" + calculateGradleCommand() + " " + spotlessApplySelector(getProject()) + "' to fix all violations.");
}

private static String calculateGradleCommand() {
return FileSignature.machineIsWin() ? "gradlew.bat" : "./gradlew";
}

/**
* Task selector for the fix-it hint.
* <ul>
* <li>In the main/root build, bare {@code spotlessApply} fixes every project (see #2592).</li>
* <li>In an included/composite build, bare task names do not select included-build tasks,
* so we must use the build-tree path (e.g. {@code :my-utils:spotlessApply}) — see #2421.</li>
* </ul>
*/
static String spotlessApplySelector(Project project) {
if (project.getGradle().getParent() == null) {
return "spotlessApply";
}
return buildTreePath(project) + ":spotlessApply";
}

/**
* Path of this project in the full build tree (includes included-build names).
* Uses {@link Project#getBuildTreePath()} when available (Gradle 8.10+), otherwise
* reconstructs it from {@link Gradle#getIncludedBuilds()}.
*/
static String buildTreePath(Project project) {
try {
Object treePath = Project.class.getMethod("getBuildTreePath").invoke(project);
if (treePath instanceof String s && !s.isEmpty()) {
return s;
}
} catch (ReflectiveOperationException ignored) {
// Gradle < 8.10
}
return reconstructBuildTreePath(project);
}

private static String reconstructBuildTreePath(Project project) {
StringBuilder buildPrefix = new StringBuilder();
Gradle gradle = project.getGradle();
while (gradle.getParent() != null) {
Gradle parent = gradle.getParent();
File rootDir = gradle.getRootProject().getRootDir().getAbsoluteFile();
String buildName = null;
for (IncludedBuild included : parent.getIncludedBuilds()) {
if (included.getProjectDir().getAbsoluteFile().equals(rootDir)) {
buildName = included.getName();
break;
}
}
if (buildName == null) {
buildName = gradle.getRootProject().getName();
}
buildPrefix.insert(0, ":" + buildName);
gradle = parent;
}
String projectPath = project.getPath();
if (":".equals(projectPath)) {
return buildPrefix.length() == 0 ? ":" : buildPrefix.toString();
}
return buildPrefix + projectPath;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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.junit.jupiter.api.Test;

import com.diffplug.spotless.FileSignature;

/**
* Regression for <a href="https://github.com/diffplug/spotless/issues/2421">#2421</a>:
* when Spotless runs inside an included build of a composite, the run-to-fix hint must
* use the build-tree task path (e.g. {@code :my-utils:spotlessApply}), not a bare
* {@code spotlessApply} which only selects tasks in the root/main build.
*/
class CompositeBuildRunToFixMessageTest extends GradleIntegrationHarness {

private static String expectedGradleCommand() {
return FileSignature.machineIsWin() ? "gradlew.bat" : "./gradlew";
}

@Test
void includedBuildRootSuggestsBuildTreePath() throws IOException {
createCompositeWithIncluded("my-utils", null);

String output = gradleRunner()
.withArguments(":my-utils:spotlessCheck")
.buildAndFail()
.getOutput();

Assertions.assertThat(output)
.contains("Run '" + expectedGradleCommand() + " :my-utils:spotlessApply' to fix all violations.");
}

@Test
void includedBuildSubprojectSuggestsBuildTreePath() throws IOException {
createCompositeWithIncluded("my-utils", "lib");

String output = gradleRunner()
.withArguments(":my-utils:lib:spotlessCheck")
.buildAndFail()
.getOutput();

Assertions.assertThat(output)
.contains("Run '" + expectedGradleCommand() + " :my-utils:lib:spotlessApply' to fix all violations.");
}

@Test
void standaloneBuildStillSuggestsBareSpotlessApply() throws IOException {
// Outside a composite, bare spotlessApply is correct and preferred (#2592).
setFile("settings.gradle").toContent("rootProject.name = 'standalone'");
setFile("build.gradle").toLines(
"plugins {",
" id 'com.diffplug.spotless'",
"}",
"spotless {",
" format 'misc', {",
" target 'test.txt'",
" leadingTabsToSpaces(2)",
" }",
"}");
setFile("test.txt").toContent("\thello\n");

String output = gradleRunner()
.withArguments("spotlessCheck")
.buildAndFail()
.getOutput();

Assertions.assertThat(output)
.contains("Run '" + expectedGradleCommand() + " spotlessApply' to fix all violations.")
.doesNotContain(":spotlessApply");
}

private void createCompositeWithIncluded(String includedName, String subproject) throws IOException {
setFile("settings.gradle").toLines(
"rootProject.name = 'my-composite'",
"includeBuild('" + includedName + "')");
setFile("build.gradle").toContent("");

setFile(includedName + "/settings.gradle").toContent(
subproject == null
? "rootProject.name = '" + includedName + "'"
: "rootProject.name = '" + includedName + "'\ninclude '" + subproject + "'");

String spotlessBlock = String.join("\n",
"plugins {",
" id 'com.diffplug.spotless'",
"}",
"spotless {",
" format 'misc', {",
" target 'test.txt'",
" leadingTabsToSpaces(2)",
" }",
"}");

if (subproject == null) {
setFile(includedName + "/build.gradle").toContent(spotlessBlock);
setFile(includedName + "/test.txt").toContent("\thello\n");
} else {
setFile(includedName + "/build.gradle").toContent("");
setFile(includedName + "/" + subproject + "/build.gradle").toContent(spotlessBlock);
setFile(includedName + "/" + subproject + "/test.txt").toContent("\thello\n");
}
}
}