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
1 change: 1 addition & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions plugin-gradle/IDE_HOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,18 +34,23 @@

final class IdeHook {
static class State extends NoLambda.EqualityBasedOnSerialization {
final @Nullable String path;
final @Nullable List<String> 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;
}
}
}
Expand All @@ -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<File> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@ protected void createFormatTasks(String name, FormatExtension formatExtension) {
TaskProvider<SpotlessApply> 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<SpotlessCheck> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Provider<SpotlessTaskService> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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");
}
}