diff --git a/.circleci/config.yml b/.circleci/config.yml index 3c6d67bfd0..c9e2f911d3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,8 +1,11 @@ anchors: env_gradle: &env_gradle environment: - _JAVA_OPTIONS: "-Xmx3g" - GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2" + # java doesn't play nice with containers, it tries to hog the entire machine + # https://circleci.com/blog/how-to-handle-java-oom-errors/ + # try the experimental JVM option + _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap" + GRADLE_OPTS: "-Dorg.gradle.workers.max=2" # and we're only allowed to use 2 vCPUs docker: - image: cimg/openjdk:8.0 @@ -67,6 +70,8 @@ jobs: <<: *env_gradle docker: - image: cimg/openjdk:11.0 + environment: # java 11 does play nice with containers, doesn't need special settings + _JAVA_OPTIONS: "" <<: *test_nomaven test_justmaven_8: << : *env_gradle diff --git a/CHANGES.md b/CHANGES.md index 0ddf566b74..f9a9af5394 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* `PaddedCell.calculateDirtyState(Formatter, File, byte[])` to allow IDE integrations to send dirty editor buffers. ## [1.29.0] - 2020-05-05 ### Added diff --git a/lib/src/main/java/com/diffplug/spotless/PaddedCell.java b/lib/src/main/java/com/diffplug/spotless/PaddedCell.java index 0888e1646a..d801db038f 100644 --- a/lib/src/main/java/com/diffplug/spotless/PaddedCell.java +++ b/lib/src/main/java/com/diffplug/spotless/PaddedCell.java @@ -186,6 +186,10 @@ public static DirtyState calculateDirtyState(Formatter formatter, File file) thr Objects.requireNonNull(file, "file"); byte[] rawBytes = Files.readAllBytes(file.toPath()); + return calculateDirtyState(formatter, file, rawBytes); + } + + public static DirtyState calculateDirtyState(Formatter formatter, File file, byte[] rawBytes) throws IOException { String raw = new String(rawBytes, formatter.getEncoding()); String rawUnix = LineEnding.toUnix(raw); diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 74d6a578b5..b4ec663675 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +* `-PspotlessIdeHook` which makes the VS Code extension faster and more reliable. See [`IDE_INTEGRATION.md`](IDE_INTEGRATION.md) for more details. ([#568](https://github.com/diffplug/spotless/pull/568)) ## [3.29.0] - 2020-05-05 ### Added diff --git a/plugin-gradle/IDE_HOOK.md b/plugin-gradle/IDE_HOOK.md new file mode 100644 index 0000000000..8734245cad --- /dev/null +++ b/plugin-gradle/IDE_HOOK.md @@ -0,0 +1,26 @@ +# Spotless Gradle IDE integrations + +Thanks to `spotlessApply`, it is not necessary for Spotless and your IDE to agree on formatting - you can always run spotless at the end to fix things up. But if you want them to agree, there are two approaches: + +- 👎setup your IDE to match Spotless: tricky to get right, hard to keep up-to-date + - [eclipse](https://github.com/diffplug/spotless/blob/master/ECLIPSE_SCREENSHOTS.md) +- 👍setup your IDE to use Spotless as the source of truth: easy to setup, guaranteed to stay up-to-date + - [VS Code](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) + - (add your IDE here!) + +## 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`. + +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`). + +In all of these cases, Spotless will send useful status information on `stderr`: + +- if `stderr` starts with `IS DIRTY`, then the file was dirty, and `stdout` contains its full formatted contents + - in every other case, `stdout` will be empty / the file will be unchanged because there is nothing to change +- if `stderr` starts with `IS CLEAN`, then the file is already clean +- if `stderr` starts with `DID NOT CONVERGE`, then the formatter is misbehaving, and the rest of `stderr` has useful diagnostic info (e.g. `spotlessDiagnose` for [padded cell](../PADDEDCELL.md)) +- if `stderr` is empty, then the file is not being formatted by spotless (not included in any target) +- if `stderr` is anything else, then it is the stacktrace of whatever went wrong + +See the VS Code extension above for a working example, or [the PR](https://github.com/diffplug/spotless/pull/568) where this feature was added for more context. diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 61fffb9f2f..f6ca7af118 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -1,34 +1,39 @@ -# Spotless: Keep your code spotless with Gradle +# Spotless plugin for Gradle +*Keep your code Spotless with Gradle* [![Gradle plugin](https://img.shields.io/badge/plugins.gradle.org-com.diffplug.gradle.spotless-blue.svg)](https://plugins.gradle.org/plugin/com.diffplug.gradle.spotless) -[![Maven central](https://img.shields.io/badge/mavencentral-com.diffplug.gradle.spotless%3Aspotless-blue.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-plugin-gradle%22) -[![Javadoc](https://img.shields.io/badge/javadoc-3.29.0-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/3.29.0/index.html) +[![Maven central](https://img.shields.io/badge/mavencentral-yes-blue.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.diffplug.spotless%22%20AND%20a%3A%22spotless-plugin-gradle%22) +[![Javadoc](https://img.shields.io/badge/javadoc-yes-blue.svg)](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/3.29.0/index.html) +[![License Apache](https://img.shields.io/badge/license-apache-blue.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) +[![Changelog](https://img.shields.io/badge/changelog-3.29.0-blue.svg)](CHANGES.md) -[![Changelog](https://img.shields.io/badge/changelog-3.29.0-brightgreen.svg)](CHANGES.md) -[![Travis CI](https://travis-ci.org/diffplug/spotless.svg?branch=master)](https://travis-ci.org/diffplug/spotless) +[![Circle CI](https://circleci.com/gh/diffplug/spotless/tree/master.svg?style=shield)](https://circleci.com/gh/diffplug/spotless/tree/master) [![Live chat](https://img.shields.io/badge/gitter-chat-brightgreen.svg)](https://gitter.im/diffplug/spotless) -[![License Apache](https://img.shields.io/badge/license-apache-brightgreen.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) +[![VS Code plugin Apache](https://img.shields.io/badge/IDE-VS_Code-blueviolet.svg)](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) +[![VS Code plugin Apache](https://img.shields.io/badge/IDE-add_yours-blueviolet.svg)](IDE_HOOK.md) -Spotless is a general-purpose formatting plugin used by [2,700 projects on GitHub (Jan 2020)](https://github.com/search?l=gradle&q=spotless&type=Code). It is completely à la carte, but also includes powerful "batteries-included" if you opt-in. +Spotless is a general-purpose formatting plugin used by [3,500 projects on GitHub (May 2020)](https://github.com/search?l=gradle&q=spotless&type=Code). It is completely à la carte, but also includes powerful "batteries-included" if you opt-in. -To people who use your build, it looks like this: +To people who use your build, it looks like this ([IDE support also available]()): ``` cmd> gradlew build 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 new file mode 100644 index 0000000000..eb21fb5547 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/IdeHook.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 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.File; +import java.io.IOException; +import java.nio.file.Files; + +import com.diffplug.common.base.Errors; +import com.diffplug.common.io.ByteStreams; +import com.diffplug.spotless.Formatter; +import com.diffplug.spotless.PaddedCell; + +class IdeHook { + final static String PROPERTY = "spotlessIdeHook"; + final static String USE_STD_IN = "spotlessIdeHookUseStdIn"; + final static String USE_STD_OUT = "spotlessIdeHookUseStdOut"; + + static void performHook(SpotlessTask spotlessTask) { + String path = (String) spotlessTask.getProject().property(PROPERTY); + File file = new File(path); + if (!file.isAbsolute()) { + System.err.println("Argument passed to " + PROPERTY + " must be an absolute path"); + return; + } + if (spotlessTask.getTarget().contains(file)) { + try (Formatter formatter = spotlessTask.buildFormatter()) { + byte[] bytes; + if (spotlessTask.getProject().hasProperty(USE_STD_IN)) { + bytes = ByteStreams.toByteArray(System.in); + } else { + bytes = Files.readAllBytes(file.toPath()); + } + PaddedCell.DirtyState dirty = PaddedCell.calculateDirtyState(formatter, file, bytes); + if (dirty.isClean()) { + System.err.println("IS CLEAN"); + } else if (dirty.didNotConverge()) { + System.err.println("DID NOT CONVERGE"); + System.err.println("Run 'spotlessDiagnose' for details https://github.com/diffplug/spotless/blob/master/PADDEDCELL.md"); + } else { + System.err.println("IS DIRTY"); + if (spotlessTask.getProject().hasProperty(USE_STD_OUT)) { + dirty.writeCanonicalTo(System.out); + } else { + dirty.writeCanonicalTo(file); + } + } + System.err.close(); + System.out.close(); + } catch (IOException e) { + e.printStackTrace(System.err); + throw Errors.asRuntime(e); + } + } + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java index 7ebb62bab4..ffb62ad039 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java @@ -302,5 +302,14 @@ public Object doCall(TaskExecutionGraph graph) { diagnoseTask.source = spotlessTask; rootDiagnoseTask.dependsOn(diagnoseTask); diagnoseTask.mustRunAfter(clean); + + if (project.hasProperty(IdeHook.PROPERTY)) { + // disable the normal tasks, to disable their up-to-date checking + spotlessTask.setEnabled(false); + checkTask.setEnabled(false); + applyTask.setEnabled(false); + // the rootApplyTask is no longer just a marker task, now it does a bit of work itself + rootApplyTask.doLast(unused -> IdeHook.performHook(spotlessTask)); + } } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java index 0a40d8b3ac..8cd04ed980 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTask.java @@ -30,6 +30,7 @@ import org.gradle.api.DefaultTask; import org.gradle.api.GradleException; +import org.gradle.api.file.FileCollection; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Internal; @@ -104,15 +105,19 @@ public FormatExceptionPolicy getExceptionPolicy() { return exceptionPolicy; } - protected Iterable target; + protected FileCollection target; @Internal - public Iterable getTarget() { + public FileCollection getTarget() { return target; } public void setTarget(Iterable target) { - this.target = Objects.requireNonNull(target); + if (target instanceof FileCollection) { + this.target = (FileCollection) target; + } else { + this.target = getProject().files(target); + } } /** Internal use only. */ 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 new file mode 100644 index 0000000000..c13d3d98bb --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/IdeHookTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2016 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.File; +import java.io.IOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.Assertions; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.Before; +import org.junit.Test; + +import com.diffplug.common.base.StringPrinter; +import com.diffplug.common.io.Files; + +public class IdeHookTest extends GradleIntegrationTest { + private String output, error; + private File dirty, clean, diverge, outofbounds; + + @Before + public void before() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "spotless {", + " format 'misc', {", + " target 'DIRTY.md', 'CLEAN.md'", + " customLazyGroovy('lowercase') {", + " return { str -> str.toLowerCase(Locale.ROOT) }", + " }", + " }", + " format 'diverge', {", + " target 'DIVERGE.md'", + " customLazyGroovy('diverge') {", + " return { str -> str + ' ' }", + " }", + " }", + "}"); + 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); + diverge = new File(rootFolder(), "DIVERGE.md"); + Files.write("ABC".getBytes(StandardCharsets.UTF_8), diverge); + outofbounds = new File(rootFolder(), "OUTOFBOUNDS.md"); + Files.write("ABC".getBytes(StandardCharsets.UTF_8), outofbounds); + } + + private void runWith(String... arguments) throws IOException { + StringBuilder output = new StringBuilder(); + StringBuilder error = new StringBuilder(); + try (Writer outputWriter = new StringPrinter(output::append).toWriter(); + Writer errorWriter = new StringPrinter(error::append).toWriter();) { + // gradle 2.14 -> 4.6 confirmed to work + // gradle 4.7 -> 5.1 don't work in tooling API because of https://github.com/gradle/gradle/issues/7617 + // gradle 5.1 -> current confirmed to work + GradleRunner.create() + .withProjectDir(rootFolder()) + .withPluginClasspath() + .withArguments(arguments) + .forwardStdOutput(outputWriter) + .forwardStdError(errorWriter) + .build(); + } + this.output = output.toString(); + this.error = error.toString(); + } + + @Test + public void dirty() throws IOException { + runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + dirty.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); + Assertions.assertThat(output).isEqualTo("abc"); + Assertions.assertThat(error).startsWith("IS DIRTY"); + } + + @Test + public void clean() throws IOException { + runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + clean.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).startsWith("IS CLEAN"); + } + + @Test + public void diverge() throws IOException { + runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + diverge.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).startsWith("DID NOT CONVERGE"); + } + + @Test + public void outofbounds() throws IOException { + runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=" + outofbounds.getAbsolutePath(), "-PspotlessIdeHookUseStdOut"); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).isEmpty(); + } + + @Test + public void notAbsolute() throws IOException { + runWith("spotlessApply", "--quiet", "-PspotlessIdeHook=build.gradle", "-PspotlessIdeHookUseStdOut"); + Assertions.assertThat(output).isEmpty(); + Assertions.assertThat(error).contains("Argument passed to spotlessIdeHook must be an absolute path"); + } +}