Skip to content

Commit e8267b7

Browse files
authored
Extend IdeHook to support passing in multiple files (#2774)
2 parents 17609c7 + 3195ea5 commit e8267b7

6 files changed

Lines changed: 146 additions & 31 deletions

File tree

plugin-gradle/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
66
### Added
77
- Add a `expandWildcardImports` API for java ([#2679](https://github.com/diffplug/spotless/issues/2594))
88
- Add the ability to specify a wildcard version (`*`) for external formatter executables. ([#2757](https://github.com/diffplug/spotless/issues/2757))
9+
- Add support for passing multiple file paths using the -PspotlessIdeHook option. ([#2774](https://github.com/diffplug/spotless/pull/2774))
910
### Fixed
1011
- configuration cache for groovy. ([#2797](https://github.com/diffplug/spotless/pull/2797))
1112
- [fix] `NPE` due to workingTreeIterator being null for git ignored files. #911 ([#2771](https://github.com/diffplug/spotless/issues/2771))

plugin-gradle/IDE_HOOK.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Thanks to `spotlessApply`, it is not necessary for Spotless and your IDE to agre
99
## How to add an IDE
1010

1111
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`.
12+
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}`.
1213

1314
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`).
1415

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2025 DiffPlug
2+
* Copyright 2016-2026 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,9 @@
1818
import java.io.File;
1919
import java.io.IOException;
2020
import java.nio.file.Files;
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import java.util.stream.Collectors;
2124

2225
import javax.annotation.Nullable;
2326

@@ -31,18 +34,23 @@
3134

3235
final class IdeHook {
3336
static class State extends NoLambda.EqualityBasedOnSerialization {
34-
final @Nullable String path;
37+
final @Nullable List<String> paths;
3538
final boolean useStdIn;
3639
final boolean useStdOut;
3740

3841
State(Project project) {
39-
path = GradleCompat.findOptionalProperty(project, PROPERTY);
40-
if (path != null) {
42+
var pathsString = GradleCompat.findOptionalProperty(project, PROPERTY);
43+
if (pathsString != null) {
4144
useStdIn = GradleCompat.isPropertyPresent(project, USE_STD_IN);
4245
useStdOut = GradleCompat.isPropertyPresent(project, USE_STD_OUT);
46+
paths = Arrays.stream(pathsString.split(","))
47+
.map(String::trim)
48+
.filter(s -> !s.isEmpty())
49+
.collect(Collectors.toList());
4350
} else {
4451
useStdIn = false;
4552
useStdOut = false;
53+
paths = null;
4654
}
4755
}
4856
}
@@ -56,18 +64,29 @@ private static void dumpIsClean() {
5664
}
5765

5866
static void performHook(SpotlessTaskImpl spotlessTask, IdeHook.State state) {
59-
File file = new File(state.path);
60-
if (!file.isAbsolute()) {
61-
System.err.println("Argument passed to " + PROPERTY + " must be an absolute path");
67+
if (state.paths == null) {
6268
return;
6369
}
64-
if (spotlessTask.getTarget().contains(file)) {
70+
if (state.paths.size() > 1 && (state.useStdIn || state.useStdOut)) {
71+
System.err.println("Using " + USE_STD_IN + " or " + USE_STD_OUT + " with multiple files is not supported");
72+
return;
73+
}
74+
List<File> files = state.paths.stream().map(File::new).toList();
75+
for (File file : files) {
76+
if (!file.isAbsolute()) {
77+
System.err.println("Argument passed to " + PROPERTY + " must be one or multiple absolute paths");
78+
return;
79+
}
80+
}
81+
82+
var matchedFiles = files.stream().filter(file -> spotlessTask.getTarget().contains(file)).toList();
83+
for (File file : matchedFiles) {
6584
GitRatchetGradle ratchet = spotlessTask.getRatchet();
6685
try (Formatter formatter = spotlessTask.buildFormatter()) {
6786
if (ratchet != null) {
6887
if (ratchet.isClean(spotlessTask.getProjectDir().get().getAsFile(), spotlessTask.getRootTreeSha(), file)) {
6988
dumpIsClean();
70-
return;
89+
continue;
7190
}
7291
}
7392
byte[] bytes;

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionImpl.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2025 DiffPlug
2+
* Copyright 2016-2026 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -80,15 +80,15 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) {
8080
TaskProvider<SpotlessApply> applyTask = tasks.register(taskName + APPLY, SpotlessApply.class, task -> {
8181
task.init(spotlessTask);
8282
task.setGroup(TASK_GROUP);
83-
task.setEnabled(ideHook.path == null);
83+
task.setEnabled(ideHook.paths == null);
8484
task.dependsOn(spotlessTask);
8585
});
86-
rootApplyTask.configure(task -> task.dependsOn(ideHook.path == null ? applyTask : spotlessTask));
86+
rootApplyTask.configure(task -> task.dependsOn(ideHook.paths == null ? applyTask : spotlessTask));
8787

8888
TaskProvider<SpotlessCheck> checkTask = tasks.register(taskName + CHECK, SpotlessCheck.class, task -> {
8989
task.setGroup(TASK_GROUP);
9090
task.init(spotlessTask);
91-
task.setEnabled(ideHook.path == null);
91+
task.setEnabled(ideHook.paths == null);
9292
task.dependsOn(spotlessTask);
9393

9494
// if the user runs both, make sure that apply happens first,

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2025 DiffPlug
2+
* Copyright 2016-2026 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -79,7 +79,7 @@ Provider<SpotlessTaskService> getTaskServiceProvider() {
7979
@TaskAction
8080
public void performAction(InputChanges inputs) throws Exception {
8181
IdeHook.State ideHook = getIdeHookState().getOrNull();
82-
if (ideHook != null && ideHook.path != null) {
82+
if (ideHook != null && ideHook.paths != null) {
8383
IdeHook.performHook(this, ideHook);
8484
return;
8585
}

plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2025 DiffPlug
2+
* Copyright 2016-2026 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,29 +37,21 @@ class IdeHookTest extends GradleIntegrationHarness {
3737
private String error;
3838
private File dirty;
3939
private File clean;
40+
private File clean2;
4041
private File diverge;
4142
private File outofbounds;
4243

4344
@BeforeEach
4445
void before() throws IOException {
45-
setFile("build.gradle").toLines(
46-
"plugins {",
47-
" id 'com.diffplug.spotless'",
48-
"}",
49-
"spotless {",
50-
" format 'misc', {",
51-
" target 'DIRTY.md', 'CLEAN.md'",
52-
" addStep com.diffplug.spotless.TestingOnly.lowercase()",
53-
" }",
54-
" format 'diverge', {",
55-
" target 'DIVERGE.md'",
56-
" addStep com.diffplug.spotless.TestingOnly.diverge()",
57-
" }",
58-
"}");
46+
var miscTargets = "'DIRTY.md', 'CLEAN.md', 'CLEAN2.md'";
47+
var divergeTargets = "'DIVERGE.md'";
48+
initPluginConfig(miscTargets, divergeTargets);
5949
dirty = new File(rootFolder(), "DIRTY.md");
6050
Files.write("ABC".getBytes(StandardCharsets.UTF_8), dirty);
6151
clean = new File(rootFolder(), "CLEAN.md");
6252
Files.write("abc".getBytes(StandardCharsets.UTF_8), clean);
53+
clean2 = new File(rootFolder(), "CLEAN2.md");
54+
Files.write("def".getBytes(StandardCharsets.UTF_8), clean2);
6355
diverge = new File(rootFolder(), "DIVERGE.md");
6456
Files.write("ABC".getBytes(StandardCharsets.UTF_8), diverge);
6557
outofbounds = new File(rootFolder(), "OUTOFBOUNDS.md");
@@ -85,6 +77,23 @@ private void runWith(boolean configurationCache, String... arguments) throws IOE
8577
this.error = error.toString();
8678
}
8779

80+
private void initPluginConfig(String miscTargets, String divergeTargets) throws IOException {
81+
setFile("build.gradle").toLines(
82+
"plugins {",
83+
" id 'com.diffplug.spotless'",
84+
"}",
85+
"spotless {",
86+
" format 'misc', {",
87+
" target " + miscTargets,
88+
" addStep com.diffplug.spotless.TestingOnly.lowercase()",
89+
" }",
90+
" format 'diverge', {",
91+
" target " + divergeTargets,
92+
" addStep com.diffplug.spotless.TestingOnly.diverge()",
93+
" }",
94+
"}");
95+
}
96+
8897
protected GradleRunner gradleRunner(boolean configurationCache) throws IOException {
8998
if (configurationCache) {
9099
setFile("gradle.properties").toContent("org.gradle.unsafe.configuration-cache=true");
@@ -139,6 +148,91 @@ void outofbounds(boolean configurationCache) throws IOException {
139148
void notAbsolute(boolean configurationCache) throws IOException {
140149
runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=build.gradle", "-PspotlessIdeHookUseStdOut");
141150
Assertions.assertThat(output).isEmpty();
142-
Assertions.assertThat(error).contains("Argument passed to spotlessIdeHook must be an absolute path");
151+
Assertions.assertThat(error).contains("Argument passed to spotlessIdeHook must be one or multiple absolute paths");
152+
}
153+
154+
@ParameterizedTest
155+
@MethodSource("configurationCacheProvider")
156+
void multipleFilesBothDirty(boolean configurationCache) throws IOException {
157+
String paths = dirty.getAbsolutePath() + "," + diverge.getAbsolutePath();
158+
runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths);
159+
Assertions.assertThat(output).isEmpty();
160+
Assertions.assertThat(error).contains("IS DIRTY");
161+
Assertions.assertThat(error).contains("DID NOT CONVERGE");
162+
}
163+
164+
@ParameterizedTest
165+
@MethodSource("configurationCacheProvider")
166+
void multipleFilesBothClean(boolean configurationCache) throws IOException {
167+
String paths = clean.getAbsolutePath() + "," + clean2.getAbsolutePath();
168+
runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths);
169+
Assertions.assertThat(output).isEmpty();
170+
Assertions.assertThat(error).contains("IS CLEAN");
171+
}
172+
173+
@ParameterizedTest
174+
@MethodSource("configurationCacheProvider")
175+
void multipleFilesMixed(boolean configurationCache) throws IOException {
176+
String paths = clean.getAbsolutePath() + "," + dirty.getAbsolutePath();
177+
runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths);
178+
Assertions.assertThat(output).isEmpty();
179+
Assertions.assertThat(error).contains("IS CLEAN");
180+
Assertions.assertThat(error).contains("IS DIRTY");
181+
}
182+
183+
@ParameterizedTest
184+
@MethodSource("configurationCacheProvider")
185+
void multipleFilesWithSpaces(boolean configurationCache) throws IOException {
186+
String paths = clean.getAbsolutePath() + " , " + dirty.getAbsolutePath();
187+
runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths);
188+
Assertions.assertThat(output).isEmpty();
189+
Assertions.assertThat(error).contains("IS CLEAN");
190+
Assertions.assertThat(error).contains("IS DIRTY");
191+
}
192+
193+
@ParameterizedTest
194+
@MethodSource("configurationCacheProvider")
195+
void multipleFilesWithOutOfBounds(boolean configurationCache) throws IOException {
196+
String paths = dirty.getAbsolutePath() + "," + outofbounds.getAbsolutePath();
197+
runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths);
198+
Assertions.assertThat(output).isEmpty();
199+
Assertions.assertThat(error).contains("IS DIRTY");
200+
}
201+
202+
@ParameterizedTest
203+
@MethodSource("configurationCacheProvider")
204+
void multipleFilesStdOutThrowsException(boolean configurationCache) throws IOException {
205+
String paths = dirty.getAbsolutePath() + "," + clean.getAbsolutePath();
206+
runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths, "-PspotlessIdeHookUseStdOut");
207+
Assertions.assertThat(output).isEmpty();
208+
Assertions.assertThat(error).contains("Using spotlessIdeHookUseStdIn or spotlessIdeHookUseStdOut with multiple files is not supported");
209+
}
210+
211+
@ParameterizedTest
212+
@MethodSource("configurationCacheProvider")
213+
void multipleFilesStdInThrowsException(boolean configurationCache) throws IOException {
214+
String paths = dirty.getAbsolutePath() + "," + clean.getAbsolutePath();
215+
runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths, "-PspotlessIdeHookUseStdIn");
216+
Assertions.assertThat(output).isEmpty();
217+
Assertions.assertThat(error).contains("Using spotlessIdeHookUseStdIn or spotlessIdeHookUseStdOut with multiple files is not supported");
218+
}
219+
220+
@ParameterizedTest
221+
@MethodSource("configurationCacheProvider")
222+
void multipleFilesLargeScale(boolean configurationCache) throws IOException {
223+
int fileCount = 500;
224+
StringBuilder paths = new StringBuilder();
225+
for (int i = 0; i < fileCount; i++) {
226+
File f = new File(rootFolder(), "file_" + i + ".md");
227+
Files.write(("Some content " + i).getBytes(StandardCharsets.UTF_8), f);
228+
if (i > 0) {
229+
paths.append(",");
230+
}
231+
paths.append(f.getAbsolutePath());
232+
}
233+
initPluginConfig("'file_*.md'", "'DIVERGE.md'");
234+
runWith(configurationCache, "spotlessApply", "--quiet", "-PspotlessIdeHook=" + paths);
235+
Assertions.assertThat(output).isEmpty();
236+
Assertions.assertThat(error).contains("IS DIRTY");
143237
}
144238
}

0 commit comments

Comments
 (0)