diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 368856e278..56d6c177cb 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -6,6 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Fixed - `prettier()` and other npm-based steps no longer fail to start on npm 12 (`EUNKNOWNCONFIG` from `--scripts-prepend-node-path`). ([#3024](https://github.com/diffplug/spotless/issues/3024)) - `spotlessInternalRegisterDependencies` now writes its output under a build directory that is configured after the plugin is applied, instead of always under the default `build/`. ([#2114](https://github.com/diffplug/spotless/issues/2114)) +- `targetExclude` now accepts a Gradle `Directory`, `DirectoryProperty`, or `Provider` and excludes the files under it. Previously the directory was treated as a single file, so excluding one silently did nothing. ([#2667](https://github.com/diffplug/spotless/issues/2667)) ## [8.10.0] - 2026-08-17 ### Added 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 48f3e50eb9..e1cc198067 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 @@ -47,7 +47,9 @@ import org.gradle.api.file.ConfigurableFileTree; import org.gradle.api.file.Directory; import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileSystemLocation; import org.gradle.api.plugins.BasePlugin; +import org.gradle.api.provider.Provider; import org.gradle.api.tasks.TaskProvider; import org.gradle.util.GradleVersion; import org.slf4j.Logger; @@ -344,6 +346,9 @@ private final FileCollection parseTargetIsExclude(Object target, boolean isExclu matchedFiles.exclude(excludes); } return matchedFiles; + } else if (isExclude && (target instanceof FileSystemLocation || target instanceof Provider)) { + // an excluded directory means the files under it, and the tree resolves them lazily + return getProject().files(target).getAsFileTree(); } else { return getProject().files(target); } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeDirectoryTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeDirectoryTest.java new file mode 100644 index 0000000000..8b2010d1bc --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeDirectoryTest.java @@ -0,0 +1,113 @@ +/* + * 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.junit.jupiter.api.Test; + +class TargetExcludeDirectoryTest extends GradleIntegrationHarness { + @Test + void targetExcludeWithDirectory() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExclude layout.projectDirectory.dir('generated')", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + setFile("generated/excluded.md").toLines("A B C"); + setFile("generated/nested/excluded.md").toLines("A B C"); + setFile("manual.md").toLines("A B C"); + + gradleRunner().withArguments("spotlessApply").build(); + + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("generated/nested/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("a b c"); + } + + @Test + void targetExcludeWithDirectoryProperty() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "def excluded = objects.directoryProperty()", + "excluded.set(layout.projectDirectory.dir('generated'))", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExclude excluded", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + setFile("generated/excluded.md").toLines("A B C"); + setFile("manual.md").toLines("A B C"); + + gradleRunner().withArguments("spotlessApply").build(); + + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("a b c"); + } + + @Test + void targetExcludeWithDirectoryProvider() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExclude providers.provider { layout.projectDirectory.dir('generated') }", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + setFile("generated/excluded.md").toLines("A B C"); + setFile("manual.md").toLines("A B C"); + + gradleRunner().withArguments("spotlessApply").build(); + + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("a b c"); + } + + @Test + void targetExcludeWithDirectoryAndConfigurationCache() throws IOException { + setFile("gradle.properties").toLines("org.gradle.configuration-cache=true"); + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExclude layout.projectDirectory.dir('generated')", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + setFile("generated/excluded.md").toLines("A B C"); + setFile("manual.md").toLines("A B C"); + + gradleRunner().withArguments("spotlessApply").build(); + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("a b c"); + + // the exclusion still applies on a second run with the cache enabled + setFile("manual.md").toLines("D E F"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("generated/excluded.md").hasLines("A B C"); + assertFile("manual.md").hasLines("d e f"); + } + +}