diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index b9de11b3a1..8e01d74bc6 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added - Add the ability to specify a wildcard version (`*`) for external formatter executables. ([#2757](https://github.com/diffplug/spotless/issues/2757)) +- Add support for passing multiple file paths using the -PspotlessIdeHook option. ([#2774](https://github.com/diffplug/spotless/pull/2774)) ### Fixed - [fix] `NPE` due to workingTreeIterator being null for git ignored files. #911 ([#2771](https://github.com/diffplug/spotless/issues/2771)) ### Changes diff --git a/plugin-gradle/IDE_HOOK.md b/plugin-gradle/IDE_HOOK.md index c09f5a51a3..3eaa706344 100644 --- a/plugin-gradle/IDE_HOOK.md +++ b/plugin-gradle/IDE_HOOK.md @@ -9,6 +9,7 @@ Thanks to `spotlessApply`, it is not necessary for Spotless and your IDE to agre ## How to add an IDE The Spotless plugin for Gradle accepts a command-line argument `-PspotlessIdeHook=${ABSOLUTE_PATH_TO_FILE}`. In this mode, `spotlessCheck` is disabled, and `spotlessApply` will apply only to that one file. Because it already knows the absolute path of the only file you are asking about, it is able to run much faster than a normal invocation of `spotlessApply`. +By passing in a comma separated list of absolute file paths, you can format multiple files in one invocation `-PspotlessIdeHook=${ABSOLUTE_PATH_TO_FILE_A},${ABSOLUTE_PATH_TO_FILE_B}`. For extra flexibility, you can add `-PspotlessIdeHookUseStdIn`, and Spotless will read the file content from `stdin`. This allows you to send the content of a dirty editor buffer without writing to a file. You can also add `-PspotlessIdeHookUseStdOut`, and Spotless will return the formatted content on `stdout` rather than writing it to a file (you should also add `--quiet` to make sure Gradle doesn't dump logging info into `stdout`). diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java index 98bb35069c..b8db151c1a 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java @@ -18,6 +18,9 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -31,18 +34,23 @@ final class IdeHook { static class State extends NoLambda.EqualityBasedOnSerialization { - final @Nullable String path; + final @Nullable List paths; final boolean useStdIn; final boolean useStdOut; State(Project project) { - path = GradleCompat.findOptionalProperty(project, PROPERTY); - if (path != null) { + var pathsString = GradleCompat.findOptionalProperty(project, PROPERTY); + if (pathsString != null) { useStdIn = GradleCompat.isPropertyPresent(project, USE_STD_IN); useStdOut = GradleCompat.isPropertyPresent(project, USE_STD_OUT); + paths = Arrays.stream(pathsString.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); } else { useStdIn = false; useStdOut = false; + paths = null; } } } @@ -56,18 +64,29 @@ private static void dumpIsClean() { } static void performHook(SpotlessTaskImpl spotlessTask, IdeHook.State state) { - File file = new File(state.path); - if (!file.isAbsolute()) { - System.err.println("Argument passed to " + PROPERTY + " must be an absolute path"); + if (state.paths == null) { return; } - if (spotlessTask.getTarget().contains(file)) { + if (state.paths.size() > 1 && (state.useStdIn || state.useStdOut)) { + System.err.println("Using " + USE_STD_IN + " or " + USE_STD_OUT + " with multiple files is not supported"); + return; + } + List files = state.paths.stream().map(File::new).toList(); + for (File file : files) { + if (!file.isAbsolute()) { + System.err.println("Argument passed to " + PROPERTY + " must be one or multiple absolute paths"); + return; + } + } + + var matchedFiles = files.stream().filter(file -> spotlessTask.getTarget().contains(file)).toList(); + for (File file : matchedFiles) { GitRatchetGradle ratchet = spotlessTask.getRatchet(); try (Formatter formatter = spotlessTask.buildFormatter()) { if (ratchet != null) { if (ratchet.isClean(spotlessTask.getProjectDir().get().getAsFile(), spotlessTask.getRootTreeSha(), file)) { dumpIsClean(); - return; + continue; } } byte[] bytes; 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 42a5414c3f..7f8d3a1504 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 @@ -80,15 +80,15 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) { TaskProvider applyTask = tasks.register(taskName + APPLY, SpotlessApply.class, task -> { task.init(spotlessTask); task.setGroup(TASK_GROUP); - task.setEnabled(ideHook.path == null); + task.setEnabled(ideHook.paths == null); task.dependsOn(spotlessTask); }); - rootApplyTask.configure(task -> task.dependsOn(ideHook.path == null ? applyTask : spotlessTask)); + rootApplyTask.configure(task -> task.dependsOn(ideHook.paths == null ? applyTask : spotlessTask)); TaskProvider checkTask = tasks.register(taskName + CHECK, SpotlessCheck.class, task -> { task.setGroup(TASK_GROUP); task.init(spotlessTask); - task.setEnabled(ideHook.path == null); + task.setEnabled(ideHook.paths == null); task.dependsOn(spotlessTask); // if the user runs both, make sure that apply happens first, diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java index 252e418a0b..b78d5b5108 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java @@ -79,7 +79,7 @@ Provider getTaskServiceProvider() { @TaskAction public void performAction(InputChanges inputs) throws Exception { IdeHook.State ideHook = getIdeHookState().getOrNull(); - if (ideHook != null && ideHook.path != null) { + if (ideHook != null && ideHook.paths != null) { IdeHook.performHook(this, ideHook); return; } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java index 98f94433f5..2a9d3d4e9b 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java @@ -37,29 +37,21 @@ class IdeHookTest extends GradleIntegrationHarness { private String error; private File dirty; private File clean; + private File clean2; private File diverge; private File outofbounds; @BeforeEach void before() throws IOException { - setFile("build.gradle").toLines( - "plugins {", - " id 'com.diffplug.spotless'", - "}", - "spotless {", - " format 'misc', {", - " target 'DIRTY.md', 'CLEAN.md'", - " addStep com.diffplug.spotless.TestingOnly.lowercase()", - " }", - " format 'diverge', {", - " target 'DIVERGE.md'", - " addStep com.diffplug.spotless.TestingOnly.diverge()", - " }", - "}"); + var miscTargets = "'DIRTY.md', 'CLEAN.md', 'CLEAN2.md'"; + var divergeTargets = "'DIVERGE.md'"; + initPluginConfig(miscTargets, divergeTargets); dirty = new File(rootFolder(), "DIRTY.md"); Files.write("ABC".getBytes(StandardCharsets.UTF_8), dirty); clean = new File(rootFolder(), "CLEAN.md"); Files.write("abc".getBytes(StandardCharsets.UTF_8), clean); + clean2 = new File(rootFolder(), "CLEAN2.md"); + Files.write("def".getBytes(StandardCharsets.UTF_8), clean2); diverge = new File(rootFolder(), "DIVERGE.md"); Files.write("ABC".getBytes(StandardCharsets.UTF_8), diverge); outofbounds = new File(rootFolder(), "OUTOFBOUNDS.md"); @@ -85,6 +77,23 @@ private void runWith(boolean configurationCache, String... arguments) throws IOE this.error = error.toString(); } + private void initPluginConfig(String miscTargets, String divergeTargets) throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " format 'misc', {", + " target " + miscTargets, + " addStep com.diffplug.spotless.TestingOnly.lowercase()", + " }", + " format 'diverge', {", + " target " + divergeTargets, + " addStep com.diffplug.spotless.TestingOnly.diverge()", + " }", + "}"); + } + protected GradleRunner gradleRunner(boolean configurationCache) throws IOException { if (configurationCache) { setFile("gradle.properties").toContent("org.gradle.unsafe.configuration-cache=true"); @@ -139,6 +148,90 @@ void outofbounds(boolean configurationCache) throws IOException { void notAbsolute(boolean configurationCache) throws IOException { runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=build.gradle", "-PspotlessIdeHookUseStdOut"); Assertions.assertThat(output).isEmpty(); - Assertions.assertThat(error).contains("Argument passed to spotlessIdeHook must be an absolute path"); + Assertions.assertThat(error).contains("Argument passed to spotlessIdeHook must be one or multiple absolute paths"); + } + + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void multipleFilesBothDirty(boolean configurationCache) throws IOException { + String paths = dirty.getAbsolutePath() + "," + diverge.getAbsolutePath(); + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("IS DIRTY"); + Assertions.assertThat(error).contains("DID NOT CONVERGE"); + } + + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void multipleFilesBothClean(boolean configurationCache) throws IOException { + String paths = clean.getAbsolutePath() + "," + clean2.getAbsolutePath(); + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("IS CLEAN"); + } + + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void multipleFilesMixed(boolean configurationCache) throws IOException { + String paths = clean.getAbsolutePath() + "," + dirty.getAbsolutePath(); + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("IS CLEAN"); + Assertions.assertThat(error).contains("IS DIRTY"); + } + + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void multipleFilesWithSpaces(boolean configurationCache) throws IOException { + String paths = clean.getAbsolutePath() + " , " + dirty.getAbsolutePath(); + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("IS CLEAN"); + Assertions.assertThat(error).contains("IS DIRTY"); + } + + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void multipleFilesWithOutOfBounds(boolean configurationCache) throws IOException { + String paths = dirty.getAbsolutePath() + "," + outofbounds.getAbsolutePath(); + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("IS DIRTY"); + } + + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void multipleFilesStdOutThrowsException(boolean configurationCache) throws IOException { + String paths = dirty.getAbsolutePath() + "," + clean.getAbsolutePath(); + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths, "-PspotlessIdeHookUseStdOut"); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("Using spotlessIdeHookUseStdIn or spotlessIdeHookUseStdOut with multiple files is not supported"); + } + + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void multipleFilesStdInThrowsException(boolean configurationCache) throws IOException { + String paths = dirty.getAbsolutePath() + "," + clean.getAbsolutePath(); + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths, "-PspotlessIdeHookUseStdIn"); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("Using spotlessIdeHookUseStdIn or spotlessIdeHookUseStdOut with multiple files is not supported"); + } + + @ParameterizedTest + @MethodSource("configurationCacheProvider") + void multipleFilesLargeScale(boolean configurationCache) throws IOException { + int fileCount = 500; + StringBuilder paths = new StringBuilder(); + for (int i = 0; i < fileCount; i++) { + File f = new File(rootFolder(), "file_" + i + ".md"); + Files.write(("Some content " + i).getBytes(StandardCharsets.UTF_8), f); + if (i > 0) + paths.append(","); + paths.append(f.getAbsolutePath()); + } + initPluginConfig("'file_*.md'", "'DIVERGE.md'"); + runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("IS DIRTY"); } }