From 4ed50e646c79f53168f2df88d09453767928e2eb Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 23 Aug 2020 15:40:15 -0700 Subject: [PATCH 01/27] Bump gradle from 6.3 to 6.6 --- gradle/wrapper/gradle-wrapper.jar | Bin 58695 -> 58694 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f3d88b1c2faf2fc91d853cd5d4242b5547257070..490fda8577df6c95960ba7077c43220e5bb2c0d9 100755 GIT binary patch delta 6577 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 MViXjGx&OBR0?kH%b^rhX delta 6547 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 Date: Sun, 23 Aug 2020 17:31:20 -0700 Subject: [PATCH 02/27] Infrastructure for running third-party processes. --- .../com/diffplug/spotless/ProcessRunner.java | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 lib/src/main/java/com/diffplug/spotless/ProcessRunner.java diff --git a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java new file mode 100644 index 0000000000..eedb4d6cef --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java @@ -0,0 +1,152 @@ +/* + * Copyright 2020 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.spotless; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * Shelling out to a process is harder than it ought to be in Java. + * If you don't read stdout and stderr on their own threads, you risk + * deadlock on a clogged buffer. + * + * ProcessRunner allocates two threads specifically for the purpose of + * flushing stdout and stderr to buffers. These threads will remain alive until + * the ProcessRunner is closed, so it is especially useful for repeated + * calls to an external process. + */ +public class ProcessRunner implements AutoCloseable { + private ExecutorService executor; + + public ProcessRunner() { + this.executor = Executors.newFixedThreadPool(2); + } + + /** Executes the given shell command (using `cmd` on windows and `sh` on unix). */ + public Result shell(String cmd) throws IOException, InterruptedException { + return shellWinUnix(cmd, cmd); + } + + /** Executes the given shell command (using `cmd` on windows and `sh` on unix). */ + public Result shellWinUnix(String cmdWin, String cmdUnix) throws IOException, InterruptedException { + List args; + if (FileSignature.machineIsWin()) { + args = Arrays.asList("cmd", "/c", cmdWin); + } else { + args = Arrays.asList("sh", "-c", cmdUnix); + } + return exec(args); + } + + /** Creates a process with the given arguments. */ + public Result exec(String... args) throws IOException, InterruptedException { + return exec(Arrays.asList(args)); + } + + /** Creates a process with the given arguments, the given byte array is written to stdin immediately. */ + public Result exec(byte[] stdin, String... args) throws IOException, InterruptedException { + return exec(stdin, Arrays.asList(args)); + } + + /** Creates a process with the given arguments. */ + public Result exec(List args) throws IOException, InterruptedException { + return exec(new byte[0], args); + } + + /** Creates a process with the given arguments, the given byte array is written to stdin immediately. */ + public Result exec(byte[] stdin, List args) throws IOException, InterruptedException { + ProcessBuilder builder = new ProcessBuilder(args); + Process process = builder.start(); + Future outputFut = executor.submit(() -> drainToBytes(process.getInputStream())); + Future errorFut = executor.submit(() -> drainToBytes(process.getErrorStream())); + // write stdin + process.getOutputStream().write(stdin); + process.getOutputStream().close(); + // wait for the process to finish + int exitCode = process.waitFor(); + try { + // collect the output + return new Result(args, exitCode, outputFut.get(), errorFut.get()); + } catch (ExecutionException e) { + throw ThrowingEx.asRuntime(e); + } + } + + private static void drain(InputStream input, OutputStream output) throws IOException { + byte[] buf = new byte[1024]; + int numRead; + while ((numRead = input.read(buf)) != -1) { + output.write(buf, 0, numRead); + } + } + + private static byte[] drainToBytes(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + drain(input, output); + return output.toByteArray(); + } + + @Override + public void close() { + executor.shutdown(); + } + + public static class Result { + private final List args; + private final int exitCode; + private final byte[] output, error; + + public Result(List args, int exitCode, byte[] output, byte[] error) { + this.args = args; + this.exitCode = exitCode; + this.output = output; + this.error = error; + } + + public List args() { + return args; + } + + public int exitCode() { + return exitCode; + } + + public byte[] output() { + return output; + } + + public byte[] error() { + return error; + } + + public String assertNoError(Charset charset) { + if (exitCode == 0) { + return new String(output, charset); + } else { + throw new RuntimeException("exit code: " + exitCode + "\nstderr: " + new String(error, charset)); + } + } + } +} From 7176ce4f72b1fe56a92e2d5f73625db9716df762 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 23 Aug 2020 17:35:49 -0700 Subject: [PATCH 03/27] Add BlackStep. --- README.md | 4 +- .../diffplug/spotless/python/BlackStep.java | 138 ++++++++++++++++++ .../main/resources/python/black/black.clean | 58 ++++++++ .../main/resources/python/black/black.dirty | 40 +++++ .../spotless/python/BlackStepTest.java | 29 ++++ 5 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 lib/src/main/java/com/diffplug/spotless/python/BlackStep.java create mode 100644 testlib/src/main/resources/python/black/black.clean create mode 100644 testlib/src/main/resources/python/black/black.dirty create mode 100644 testlib/src/test/java/com/diffplug/spotless/python/BlackStepTest.java diff --git a/README.md b/README.md index c4c4966e1e..fb268a362f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ output = [ [![License Apache](https://img.shields.io/badge/license-apache-brightgreen.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) -Spotless can format <java | kotlin | scala | sql | groovy | javascript | flow | typeScript | css | scss | less | jsx | vue | graphql | json | yaml | markdown | license headers | anything> using <gradle | maven | anything>. +Spotless can format <java | kotlin | scala | sql | groovy | python | javascript | flow | typeScript | css | scss | less | jsx | vue | graphql | json | yaml | markdown | license headers | anything> using <gradle | maven | anything>. - [Spotless for Gradle](plugin-gradle) - [VS Code extension](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) @@ -57,6 +57,7 @@ lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}} lib('markdown.FreshMarkStep') +'{{yes}} | {{no}} | {{no}} |', lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} |', lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} |', +lib('python.BlackStep') +'{{yes}} | {{no}} | {{no}} |', lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{no}} |', lib('sql.DBeaverSQLFormatterStep') +'{{yes}} | {{no}} | {{no}} |', extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | {{no}} |', @@ -89,6 +90,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | | [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | +| [`python.BlackStep`](lib/src/main/java/com/diffplug/spotless/python/BlackStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :white_large_square: | | [`sql.DBeaverSQLFormatterStep`](lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`wtp.EclipseWtpFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStep.java) | :+1: | :+1: | :white_large_square: | diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java new file mode 100644 index 0000000000..d0fe76aeb6 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -0,0 +1,138 @@ +/* + * Copyright 2020 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.spotless.python; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ProcessRunner; + +public class BlackStep { + public static String name() { + return "black"; + } + + public static String defaultVersion() { + return "19.10b0"; + } + + private final String version; + private final @Nullable String pathToBlack; + + private BlackStep(String version, @Nullable String pathToBlack) { + this.version = version; + this.pathToBlack = pathToBlack; + } + + public static BlackStep withVersion(String version) { + return new BlackStep(version, null); + } + + public BlackStep withPathToBlack(String pathToBlack) { + return new BlackStep(version, pathToBlack); + } + + public FormatterStep create() { + return FormatterStep.createLazy(name(), this::createState, State::toFunc); + } + + private State createState() throws BlackException, IOException, InterruptedException { + try (ProcessRunner runner = new ProcessRunner()) { + String blackExe; + if (pathToBlack != null) { + blackExe = pathToBlack; + } else { + ProcessRunner.Result which = runner.shellWinUnix("where black", "which black"); + if (which.exitCode() != 0) { + throw new BlackException(ErrorKind.CANT_FIND_BLACK, which); + } else { + blackExe = which.assertNoError(Charset.defaultCharset()).trim(); + } + } + ProcessRunner.Result blackVersion = runner.exec(blackExe, "--version"); + if (blackVersion.exitCode() != 0) { + throw new BlackException(ErrorKind.CANT_FIND_BLACK, blackVersion); + } + String versionString = blackVersion.assertNoError(Charset.defaultCharset()); + Matcher versionMatcher = Pattern.compile("black, version (.*)").matcher(versionString); + if (!versionMatcher.find()) { + throw new BlackException(ErrorKind.CANT_FIND_BLACK, blackVersion); + } + String versionParsed = versionMatcher.group(1); + if (!versionParsed.equals(version)) { + throw new BlackException(ErrorKind.BLACK_WRONG_VERSION, blackVersion); + } + return new State(version, blackExe); + } + } + + public enum ErrorKind { + CANT_FIND_BLACK, BLACK_WRONG_VERSION + } + + public static class BlackException extends Exception { + private static final long serialVersionUID = -1310199343691600283L; + + private ErrorKind kind; + private ProcessRunner.Result result; + + BlackException(ErrorKind kind, ProcessRunner.Result result) { + this.kind = kind; + this.result = result; + } + } + + static class State implements Serializable { + private static final long serialVersionUID = -1825662356883926318L; + // used for up-to-date checks and caching + final String version; + // used for executing + final transient String exeAbsPath; + final transient ProcessRunner runner = new ProcessRunner(); + + State(String version, String exeAbsPath) { + this.version = version; + this.exeAbsPath = exeAbsPath; + } + + String format(String input) throws IOException, InterruptedException { + return runner.exec(input.getBytes(StandardCharsets.UTF_8), exeAbsPath, "-").assertNoError(StandardCharsets.UTF_8); + } + + FormatterFunc.Closeable toFunc() { + return FormatterFunc.Closeable.of(runner, this::format); + } + } + + /** Either returns the "black" on the current path, or throws an exception. */ + public static File findBlackOnPath() throws IOException, InterruptedException { + try (ProcessRunner runner = new ProcessRunner()) { + ProcessRunner.Result result = runner.shellWinUnix("where black", "which black"); + return new File(result.assertNoError(Charset.defaultCharset())); + } catch (Exception e) { + throw new RuntimeException("Could not find 'black' on the path, try `pip install black` or `pip3 install black`", e); + } + } +} diff --git a/testlib/src/main/resources/python/black/black.clean b/testlib/src/main/resources/python/black/black.clean new file mode 100644 index 0000000000..75e5ce55ff --- /dev/null +++ b/testlib/src/main/resources/python/black/black.clean @@ -0,0 +1,58 @@ +from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc + +x = {"a": 37, "b": 42, "c": 927} + +x = 123456789.123456789e123456789 + +if ( + very_long_variable_name is not None + and very_long_variable_name.field > 0 + or very_long_variable_name.is_debug +): + z = "hello " + "world" +else: + world = "world" + a = "hello {}".format(world) + f = rf"hello {world}" +if this and that: + y = "hello " "world" # FIXME: https://github.com/python/black/issues/26 + + +class Foo(object): + def f(self): + return 37 * -2 + + def g(self, x, y=42): + return y + + +def f(a: List[int]): + return 37 - a[42 - u : y ** 3] + + +def very_important_function( + template: str, *variables, file: os.PathLike, debug: bool = False, +): + """Applies `variables` to the `template` and writes to `file`.""" + with open(file, "w") as f: + ... + + +# fmt: off +custom_formatting = [ + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, +] +# fmt: on +regular_formatting = [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, +] diff --git a/testlib/src/main/resources/python/black/black.dirty b/testlib/src/main/resources/python/black/black.dirty new file mode 100644 index 0000000000..2c51c6d4e9 --- /dev/null +++ b/testlib/src/main/resources/python/black/black.dirty @@ -0,0 +1,40 @@ +from seven_dwwarfs import Grumpy, Happy, Sleepy, Bashful, Sneezy, Dopey, Doc +x = { 'a':37,'b':42, + +'c':927} + +x = 123456789.123456789E123456789 + +if very_long_variable_name is not None and \ + very_long_variable_name.field > 0 or \ + very_long_variable_name.is_debug: + z = 'hello '+'world' +else: + world = 'world' + a = 'hello {}'.format(world) + f = rf'hello {world}' +if (this +and that): y = 'hello ''world'#FIXME: https://github.com/python/black/issues/26 +class Foo ( object ): + def f (self ): + return 37*-2 + def g(self, x,y=42): + return y +def f ( a: List[ int ]) : + return 37-a[42-u : y**3] +def very_important_function(template: str,*variables,file: os.PathLike,debug:bool=False,): + """Applies `variables` to the `template` and writes to `file`.""" + with open(file, "w") as f: + ... +# fmt: off +custom_formatting = [ + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, +] +# fmt: on +regular_formatting = [ + 0, 1, 2, + 3, 4, 5, + 6, 7, 8, +] \ No newline at end of file diff --git a/testlib/src/test/java/com/diffplug/spotless/python/BlackStepTest.java b/testlib/src/test/java/com/diffplug/spotless/python/BlackStepTest.java new file mode 100644 index 0000000000..7f086c96c0 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/python/BlackStepTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 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.spotless.python; + +import org.junit.Test; + +import com.diffplug.spotless.StepHarness; + +public class BlackStepTest { + @Test + public void test() throws Exception { + StepHarness.forStep(BlackStep.withVersion(BlackStep.defaultVersion()).create()) + .testResource("python/black/black.dirty", "python/black/black.clean") + .close(); + } +} From 9208477903f65873fc14e52f83e9cc910d4048b1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 23 Aug 2020 17:36:48 -0700 Subject: [PATCH 04/27] Add gradle-integration to BlackStep. --- .../gradle/spotless/PythonExtension.java | 63 +++++++++++++++++++ .../gradle/spotless/SpotlessExtension.java | 5 ++ 2 files changed, 68 insertions(+) create mode 100644 plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java new file mode 100644 index 0000000000..fbb2938cf3 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020 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 com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.python.BlackStep; + +public class PythonExtension extends FormatExtension { + static final String NAME = "python"; + + public PythonExtension(SpotlessExtension spotless) { + super(spotless); + } + + public BlackConfig black() { + return black(BlackStep.defaultVersion()); + } + + public BlackConfig black(String version) { + return new BlackConfig(version); + } + + public class BlackConfig { + BlackStep stepCfg; + + BlackConfig(String version) { + this.stepCfg = BlackStep.withVersion(version); + addStep(createStep()); + } + + public BlackConfig pathToBlack(String pathToBlack) { + stepCfg = stepCfg.withPathToBlack(pathToBlack); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return stepCfg.create(); + } + } + + @Override + protected void setupTask(SpotlessTask task) { + // defaults to all markdown files + if (target == null) { + throw noDefaultTargetException(); + } + super.setupTask(task); + } +} 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 b7f1e6dc53..13ec0b4be4 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 @@ -166,6 +166,11 @@ public void antlr4(Action closure) { format(Antlr4Extension.NAME, Antlr4Extension.class, closure); } + /** Configures the special python-specific extension for python files. */ + public void python(Action closure) { + format(PythonExtension.NAME, PythonExtension.class, closure); + } + /** Configures a custom extension. */ public void format(String name, Action closure) { requireNonNull(name, "name"); From 38f6ef46ee532018d55df3483022423a098f089e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 23 Aug 2020 20:52:49 -0700 Subject: [PATCH 05/27] Refactor part of BlackStep into ForeignExe --- .../com/diffplug/spotless/ForeignExe.java | 109 ++++++++++++++++++ .../diffplug/spotless/python/BlackStep.java | 84 +++----------- .../gradle/spotless/PythonExtension.java | 4 +- 3 files changed, 127 insertions(+), 70 deletions(-) create mode 100644 lib/src/main/java/com/diffplug/spotless/ForeignExe.java diff --git a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java new file mode 100644 index 0000000000..e04a2dcc81 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java @@ -0,0 +1,109 @@ +/* + * Copyright 2020 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.spotless; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +/** + * Finds a foreign executable and checks its version. + * If either part of that fails, it shows you why + * and helps you fix it. + */ +public abstract class ForeignExe { + public static ForeignExe named(String exeName) { + return new ForeignExe() { + @Override + protected String name() { + return exeName; + } + }; + } + + protected abstract String name(); + + protected String versionFlag() { + return "--version"; + } + + protected Pattern versionRegex() { + return Pattern.compile("version (\\S*)"); + } + + public String confirmVersionAndGetPath(String versionExpected, @Nullable String pathToExe) throws IOException, InterruptedException, SetupException { + try (ProcessRunner runner = new ProcessRunner()) { + String exeAbsPath; + if (pathToExe != null) { + exeAbsPath = pathToExe; + } else { + ProcessRunner.Result which = runner.shellWinUnix("where " + name(), "which " + name()); + if (which.exitCode() != 0) { + throw new SetupException(ErrorKind.CANT_FIND, "Unable to find " + name() + " on path", which); + } else { + exeAbsPath = which.assertNoError(Charset.defaultCharset()).trim(); + } + } + ProcessRunner.Result version = runner.exec(exeAbsPath, versionFlag()); + if (version.exitCode() != 0) { + throw new SetupException(ErrorKind.CANT_FIND, "Unable to run " + exeAbsPath, version); + } + Matcher versionMatcher = versionRegex().matcher(version.assertNoError(Charset.defaultCharset())); + if (!versionMatcher.find()) { + throw new SetupException(ErrorKind.CANT_FIND, "Unable to parse version with /" + versionRegex() + "/", version); + } + String versionParsed = versionMatcher.group(1); + if (!versionParsed.equals(versionExpected)) { + throw new SetupException(ErrorKind.WRONG_VERSION, "You specified version " + versionExpected + ", but your system has " + versionParsed, version); + } + return exeAbsPath; + } + } + + public enum ErrorKind { + CANT_FIND, WRONG_VERSION + } + + public class SetupException extends Exception { + private static final long serialVersionUID = -3515370807495069599L; + + private final ErrorKind kind; + private final String msg; + private final ProcessRunner.Result result; + + SetupException(ErrorKind kind, String msg, ProcessRunner.Result result) { + this.kind = kind; + this.msg = msg; + this.result = result; + } + + public ErrorKind getKind() { + return kind; + } + + public ProcessRunner.Result getProcessResult() { + return result; + } + + @Override + public String toString() { + return msg; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index d0fe76aeb6..39c7101012 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -15,16 +15,15 @@ */ package com.diffplug.spotless.python; -import java.io.File; import java.io.IOException; import java.io.Serializable; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.Arrays; +import java.util.List; import javax.annotation.Nullable; +import com.diffplug.spotless.ForeignExe; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.ProcessRunner; @@ -39,69 +38,28 @@ public static String defaultVersion() { } private final String version; - private final @Nullable String pathToBlack; + private final @Nullable String pathToExe; - private BlackStep(String version, @Nullable String pathToBlack) { + private BlackStep(String version, @Nullable String pathToExe) { this.version = version; - this.pathToBlack = pathToBlack; + this.pathToExe = pathToExe; } public static BlackStep withVersion(String version) { return new BlackStep(version, null); } - public BlackStep withPathToBlack(String pathToBlack) { - return new BlackStep(version, pathToBlack); + public BlackStep withPathToExe(String pathToExe) { + return new BlackStep(version, pathToExe); } public FormatterStep create() { return FormatterStep.createLazy(name(), this::createState, State::toFunc); } - private State createState() throws BlackException, IOException, InterruptedException { - try (ProcessRunner runner = new ProcessRunner()) { - String blackExe; - if (pathToBlack != null) { - blackExe = pathToBlack; - } else { - ProcessRunner.Result which = runner.shellWinUnix("where black", "which black"); - if (which.exitCode() != 0) { - throw new BlackException(ErrorKind.CANT_FIND_BLACK, which); - } else { - blackExe = which.assertNoError(Charset.defaultCharset()).trim(); - } - } - ProcessRunner.Result blackVersion = runner.exec(blackExe, "--version"); - if (blackVersion.exitCode() != 0) { - throw new BlackException(ErrorKind.CANT_FIND_BLACK, blackVersion); - } - String versionString = blackVersion.assertNoError(Charset.defaultCharset()); - Matcher versionMatcher = Pattern.compile("black, version (.*)").matcher(versionString); - if (!versionMatcher.find()) { - throw new BlackException(ErrorKind.CANT_FIND_BLACK, blackVersion); - } - String versionParsed = versionMatcher.group(1); - if (!versionParsed.equals(version)) { - throw new BlackException(ErrorKind.BLACK_WRONG_VERSION, blackVersion); - } - return new State(version, blackExe); - } - } - - public enum ErrorKind { - CANT_FIND_BLACK, BLACK_WRONG_VERSION - } - - public static class BlackException extends Exception { - private static final long serialVersionUID = -1310199343691600283L; - - private ErrorKind kind; - private ProcessRunner.Result result; - - BlackException(ErrorKind kind, ProcessRunner.Result result) { - this.kind = kind; - this.result = result; - } + private State createState() throws ForeignExe.SetupException, IOException, InterruptedException { + String exeAbsPath = ForeignExe.named("black").confirmVersionAndGetPath(version, pathToExe); + return new State(this, exeAbsPath); } static class State implements Serializable { @@ -109,30 +67,20 @@ static class State implements Serializable { // used for up-to-date checks and caching final String version; // used for executing - final transient String exeAbsPath; + final transient List args; final transient ProcessRunner runner = new ProcessRunner(); - State(String version, String exeAbsPath) { - this.version = version; - this.exeAbsPath = exeAbsPath; + State(BlackStep step, String exeAbsPath) { + this.version = step.version; + this.args = Arrays.asList(exeAbsPath, "-"); } String format(String input) throws IOException, InterruptedException { - return runner.exec(input.getBytes(StandardCharsets.UTF_8), exeAbsPath, "-").assertNoError(StandardCharsets.UTF_8); + return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertNoError(StandardCharsets.UTF_8); } FormatterFunc.Closeable toFunc() { return FormatterFunc.Closeable.of(runner, this::format); } } - - /** Either returns the "black" on the current path, or throws an exception. */ - public static File findBlackOnPath() throws IOException, InterruptedException { - try (ProcessRunner runner = new ProcessRunner()) { - ProcessRunner.Result result = runner.shellWinUnix("where black", "which black"); - return new File(result.assertNoError(Charset.defaultCharset())); - } catch (Exception e) { - throw new RuntimeException("Could not find 'black' on the path, try `pip install black` or `pip3 install black`", e); - } - } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java index fbb2938cf3..ca12acfcae 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java @@ -41,8 +41,8 @@ public class BlackConfig { addStep(createStep()); } - public BlackConfig pathToBlack(String pathToBlack) { - stepCfg = stepCfg.withPathToBlack(pathToBlack); + public BlackConfig pathToExe(String pathToBlack) { + stepCfg = stepCfg.withPathToExe(pathToBlack); replaceStep(createStep()); return this; } From 153cd0e9548f2578e88c351e91facf8b25b9181e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 23 Aug 2020 22:55:10 -0700 Subject: [PATCH 06/27] Add ClangFormatStep. --- README.md | 4 +- .../spotless/cpp/ClangFormatStep.java | 101 ++++++++++++++++++ testlib/src/main/resources/clang/example.c | 2 + .../src/main/resources/clang/example.c.clean | 5 + testlib/src/main/resources/clang/example.cs | 17 +++ .../src/main/resources/clang/example.cs.clean | 14 +++ .../main/resources/clang/example.java.clean | 3 + .../main/resources/clang/example.java.dirty | 5 + testlib/src/main/resources/clang/example.js | 21 ++++ .../src/main/resources/clang/example.js.clean | 23 ++++ testlib/src/main/resources/clang/example.m | 3 + .../src/main/resources/clang/example.m.clean | 3 + .../src/main/resources/clang/example.proto | 5 + .../main/resources/clang/example.proto.clean | 5 + .../spotless/generic/ClangFormatStepTest.java | 40 +++++++ 15 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java create mode 100644 testlib/src/main/resources/clang/example.c create mode 100644 testlib/src/main/resources/clang/example.c.clean create mode 100644 testlib/src/main/resources/clang/example.cs create mode 100644 testlib/src/main/resources/clang/example.cs.clean create mode 100644 testlib/src/main/resources/clang/example.java.clean create mode 100644 testlib/src/main/resources/clang/example.java.dirty create mode 100644 testlib/src/main/resources/clang/example.js create mode 100644 testlib/src/main/resources/clang/example.js.clean create mode 100644 testlib/src/main/resources/clang/example.m create mode 100644 testlib/src/main/resources/clang/example.m.clean create mode 100644 testlib/src/main/resources/clang/example.proto create mode 100644 testlib/src/main/resources/clang/example.proto.clean create mode 100644 testlib/src/test/java/com/diffplug/spotless/generic/ClangFormatStepTest.java diff --git a/README.md b/README.md index fb268a362f..3f4340c370 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ output = [ [![License Apache](https://img.shields.io/badge/license-apache-brightgreen.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) -Spotless can format <java | kotlin | scala | sql | groovy | python | javascript | flow | typeScript | css | scss | less | jsx | vue | graphql | json | yaml | markdown | license headers | anything> using <gradle | maven | anything>. +Spotless can format <antlr | c | c# | c++ | css | flow | graphql | groovy | html | java | javascript | json | jsx | kotlin | less | license headers | markdown | objective-c | protobuf | python | scala | scss | sql | typeScript | vue | yaml | anything> using <gradle | maven | anything>. - [Spotless for Gradle](plugin-gradle) - [VS Code extension](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) @@ -46,6 +46,7 @@ lib('generic.ReplaceRegexStep') +'{{yes}} | {{yes}} lib('generic.ReplaceStep') +'{{yes}} | {{yes}} | {{no}} |', lib('generic.TrimTrailingWhitespaceStep') +'{{yes}} | {{yes}} | {{no}} |', lib('antlr4.Antlr4FormatterStep') +'{{yes}} | {{yes}} | {{no}} |', +lib('cpp.ClangFormatStep') +'{{yes}} | {{no}} | {{no}} |', extra('cpp.EclipseFormatterStep') +'{{yes}} | {{yes}} | {{no}} |', extra('groovy.GrEclipseFormatterStep') +'{{yes}} | {{no}} | {{no}} |', lib('java.GoogleJavaFormatStep') +'{{yes}} | {{yes}} | {{no}} |', @@ -79,6 +80,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | [`generic.ReplaceStep`](lib/src/main/java/com/diffplug/spotless/generic/ReplaceStep.java) | :+1: | :+1: | :white_large_square: | | [`generic.TrimTrailingWhitespaceStep`](lib/src/main/java/com/diffplug/spotless/generic/TrimTrailingWhitespaceStep.java) | :+1: | :+1: | :white_large_square: | | [`antlr4.Antlr4FormatterStep`](lib/src/main/java/com/diffplug/spotless/antlr4/Antlr4FormatterStep.java) | :+1: | :+1: | :white_large_square: | +| [`cpp.ClangFormatStep`](lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`cpp.EclipseFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/cpp/EclipseFormatterStep.java) | :+1: | :+1: | :white_large_square: | | [`groovy.GrEclipseFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`java.GoogleJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java) | :+1: | :+1: | :white_large_square: | diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java new file mode 100644 index 0000000000..5afec5f237 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020 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.spotless.cpp; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.ForeignExe; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ProcessRunner; + +public class ClangFormatStep { + public static String name() { + return "clang"; + } + + public static String defaultVersion() { + return "10.0.1"; + } + + private final String version; + private final @Nullable String pathToExe; + private final @Nullable String style; + + private ClangFormatStep(String version, @Nullable String pathToExe, @Nullable String style) { + this.version = version; + this.pathToExe = pathToExe; + this.style = style; + } + + public static ClangFormatStep withVersion(String version) { + return new ClangFormatStep(version, null, null); + } + + public ClangFormatStep withStyle(String style) { + return new ClangFormatStep(version, pathToExe, style); + } + + public ClangFormatStep withPathToExe(String pathToExe) { + return new ClangFormatStep(version, pathToExe, style); + } + + public FormatterStep create() { + return FormatterStep.createLazy(name(), this::createState, State::toFunc); + } + + private State createState() throws ForeignExe.SetupException, IOException, InterruptedException { + String exeAbsPath = ForeignExe.named("clang-format").confirmVersionAndGetPath(version, pathToExe); + return new State(this, exeAbsPath); + } + + static class State implements Serializable { + private static final long serialVersionUID = -1825662356883926318L; + // used for up-to-date checks and caching + final String version; + final @Nullable String style; + // used for executing + final transient List args; + final transient ProcessRunner runner = new ProcessRunner(); + + State(ClangFormatStep step, String exeAbsPath) { + this.version = step.version; + this.style = step.style; + args = new ArrayList<>(3); + args.add(exeAbsPath); + if (style != null) { + args.add("--style=" + style); + } + args.add("--assume-filename=MUTABLE"); + } + + String format(String input, File file) throws IOException, InterruptedException { + args.set(args.size() - 1, "--assume-filename=" + file.getName()); + return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertNoError(StandardCharsets.UTF_8); + } + + FormatterFunc.Closeable toFunc() { + return FormatterFunc.Closeable.of(runner, FormatterFunc.needsFile(this::format)); + } + } +} diff --git a/testlib/src/main/resources/clang/example.c b/testlib/src/main/resources/clang/example.c new file mode 100644 index 0000000000..205efe447d --- /dev/null +++ b/testlib/src/main/resources/clang/example.c @@ -0,0 +1,2 @@ +#include +int main() {printf("Testing 123");return 0;} \ No newline at end of file diff --git a/testlib/src/main/resources/clang/example.c.clean b/testlib/src/main/resources/clang/example.c.clean new file mode 100644 index 0000000000..e71ede5b0f --- /dev/null +++ b/testlib/src/main/resources/clang/example.c.clean @@ -0,0 +1,5 @@ +#include +int main() { + printf("Testing 123"); + return 0; +} \ No newline at end of file diff --git a/testlib/src/main/resources/clang/example.cs b/testlib/src/main/resources/clang/example.cs new file mode 100644 index 0000000000..0f90333b32 --- /dev/null +++ b/testlib/src/main/resources/clang/example.cs @@ -0,0 +1,17 @@ +using System; +using System.Text; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; + +namespace Testing +{ + class Program + { + static void Main(string[] args) + { + string message = "Testing 1, 2, 3"; + Console.WriteLine(message); + } + } +} \ No newline at end of file diff --git a/testlib/src/main/resources/clang/example.cs.clean b/testlib/src/main/resources/clang/example.cs.clean new file mode 100644 index 0000000000..f39d6ce749 --- /dev/null +++ b/testlib/src/main/resources/clang/example.cs.clean @@ -0,0 +1,14 @@ +using System; +using System.Text; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Linq; + +namespace Testing { +class Program { + static void Main(string[] args) { + string message = "Testing 1, 2, 3"; + Console.WriteLine(message); + } +} +} \ No newline at end of file diff --git a/testlib/src/main/resources/clang/example.java.clean b/testlib/src/main/resources/clang/example.java.clean new file mode 100644 index 0000000000..d67bb653a7 --- /dev/null +++ b/testlib/src/main/resources/clang/example.java.clean @@ -0,0 +1,3 @@ +public class Java { + public static void main(String[] args) { System.out.println("hello"); } +} \ No newline at end of file diff --git a/testlib/src/main/resources/clang/example.java.dirty b/testlib/src/main/resources/clang/example.java.dirty new file mode 100644 index 0000000000..5589c792d8 --- /dev/null +++ b/testlib/src/main/resources/clang/example.java.dirty @@ -0,0 +1,5 @@ +public class Java { +public static void main(String[] args) { +System.out.println("hello"); +} +} \ No newline at end of file diff --git a/testlib/src/main/resources/clang/example.js b/testlib/src/main/resources/clang/example.js new file mode 100644 index 0000000000..08ebafc1a2 --- /dev/null +++ b/testlib/src/main/resources/clang/example.js @@ -0,0 +1,21 @@ +var numbers=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20, +]; + +const p = { + first: "Peter", + last : "Pan", + get fullName() { return this.first + " " + this.last; } +}; + +const str = "Hello, world!" +; + +var str2=str.charAt(3)+str[0]; + +var multilinestr = "Hello \ +World" +; + +function test (a, b = "world") { let combined =a+ b; return combined}; + +test ("Hello"); diff --git a/testlib/src/main/resources/clang/example.js.clean b/testlib/src/main/resources/clang/example.js.clean new file mode 100644 index 0000000000..fb67c3fea1 --- /dev/null +++ b/testlib/src/main/resources/clang/example.js.clean @@ -0,0 +1,23 @@ +var numbers = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, +]; + +const p = { + first : "Peter", + last : "Pan", + get fullName() { return this.first + " " + this.last; } +}; + +const str = "Hello, world!"; + +var str2 = str.charAt(3) + str[0]; + +var multilinestr = "Hello \ +World"; + +function test(a, b = "world") { + let combined = a + b; + return combined +}; + +test("Hello"); diff --git a/testlib/src/main/resources/clang/example.m b/testlib/src/main/resources/clang/example.m new file mode 100644 index 0000000000..b925417b90 --- /dev/null +++ b/testlib/src/main/resources/clang/example.m @@ -0,0 +1,3 @@ +- (int)method:(int)i { + return [self testing_123:i]; +} \ No newline at end of file diff --git a/testlib/src/main/resources/clang/example.m.clean b/testlib/src/main/resources/clang/example.m.clean new file mode 100644 index 0000000000..4e102e756b --- /dev/null +++ b/testlib/src/main/resources/clang/example.m.clean @@ -0,0 +1,3 @@ +- (int)method:(int)i { + return [self testing_123:i]; +} \ No newline at end of file diff --git a/testlib/src/main/resources/clang/example.proto b/testlib/src/main/resources/clang/example.proto new file mode 100644 index 0000000000..a90d58bbd0 --- /dev/null +++ b/testlib/src/main/resources/clang/example.proto @@ -0,0 +1,5 @@ +message Testing { + required string field1 = 1; + required int32 field2 = 2; + optional string field3 = 3; +} \ No newline at end of file diff --git a/testlib/src/main/resources/clang/example.proto.clean b/testlib/src/main/resources/clang/example.proto.clean new file mode 100644 index 0000000000..fa7a103c68 --- /dev/null +++ b/testlib/src/main/resources/clang/example.proto.clean @@ -0,0 +1,5 @@ +message Testing { + required string field1 = 1; + required int32 field2 = 2; + optional string field3 = 3; +} \ No newline at end of file diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/ClangFormatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/ClangFormatStepTest.java new file mode 100644 index 0000000000..74cb5f4f9e --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/generic/ClangFormatStepTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 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.spotless.generic; + +import java.io.File; +import java.util.Arrays; + +import org.junit.Test; + +import com.diffplug.spotless.StepHarnessWithFile; +import com.diffplug.spotless.cpp.ClangFormatStep; + +public class ClangFormatStepTest { + @Test + public void test() throws Exception { + try (StepHarnessWithFile harness = StepHarnessWithFile.forStep(ClangFormatStep.withVersion(ClangFormatStep.defaultVersion()).create())) { + // can't be named java or it gets compiled into .class file + harness.testResource(new File("example.java"), "clang/example.java.dirty", "clang/example.java.clean"); + // test every other language clang supports + for (String ext : Arrays.asList("c", "cs", "js", "m", "proto")) { + String filename = "example." + ext; + String root = "clang/" + filename; + harness.testResource(new File(filename), root, root + ".clean"); + } + } + } +} From f9a023a41d233c4e29a89c76ece9aff2104d07e2 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 23 Aug 2020 23:02:48 -0700 Subject: [PATCH 07/27] Add gradle-integration to ClangFormatStep. --- .../gradle/spotless/FormatExtension.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) 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 31ac2ae584..be55495888 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 @@ -46,6 +46,7 @@ import com.diffplug.spotless.LazyForwardingEquality; import com.diffplug.spotless.LineEnding; import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.cpp.ClangFormatStep; import com.diffplug.spotless.extra.EclipseBasedStepBuilder; import com.diffplug.spotless.extra.wtp.EclipseWtpFormatterStep; import com.diffplug.spotless.generic.EndWithNewlineStep; @@ -561,6 +562,42 @@ public PrettierConfig prettier(Map devDependencies) { return prettierConfig; } + /** Uses the default version of clang-format. */ + public ClangFormatConfig clangFormat() { + return clangFormat(ClangFormatStep.defaultVersion()); + } + + /** Uses the specified version of clang-format. */ + public ClangFormatConfig clangFormat(String version) { + return new ClangFormatConfig(version); + } + + public class ClangFormatConfig { + ClangFormatStep stepCfg; + + ClangFormatConfig(String version) { + this.stepCfg = ClangFormatStep.withVersion(version); + addStep(createStep()); + } + + /** Any of: LLVM, Google, Chromium, Mozilla, WebKit. */ + public ClangFormatConfig style(String style) { + stepCfg = stepCfg.withStyle(style); + replaceStep(createStep()); + return this; + } + + public ClangFormatConfig pathToExe(String pathToBlack) { + stepCfg = stepCfg.withPathToExe(pathToBlack); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return stepCfg.create(); + } + } + public class EclipseWtpConfig { private final EclipseBasedStepBuilder builder; From cf740ea6533950734faf127c113448a998f44744 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 23 Aug 2020 23:31:41 -0700 Subject: [PATCH 08/27] Add docs for clang-format to plugin-gradle. --- plugin-gradle/README.md | 46 +++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 330e79c584..e8ff170cad 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -31,7 +31,7 @@ output = [ output = prefixDelimiterReplace(input, 'https://javadoc.io/static/com.diffplug.spotless/spotless-plugin-gradle/', '/', versionLast) --> -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. +Spotless is a general-purpose formatting plugin used by [4,000 projects on GitHub (August 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 ([IDE support also available]()): @@ -50,23 +50,30 @@ user@machine repo % ./gradlew build BUILD SUCCESSFUL ``` +Spotless supports all of Gradle's built-in performance features (incremental build, remote and local buildcache, lazy configuration, etc), and also automatically fixes [idempotence issues](https://github.com/diffplug/spotless/blob/main/PADDEDCELL.md), infers [line-endings from git](#line-endings-and-encodings-invisible-stuff), is cautious about [misconfigured encoding](https://github.com/diffplug/spotless/blob/08340a11566cdf56ecf50dbd4d557ed84a70a502/testlib/src/test/java/com/diffplug/spotless/EncodingErrorMsgTest.java#L34-L38) bugs, and can use git to [ratchet formatting](#ratchet) without "format-everything" commits. + + ### Table of Contents - [**Quickstart**](#quickstart) - [Requirements](#requirements) - **Languages** - - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier)) + - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - [Kotlin](#kotlin) ([ktlint](#ktlint), [ktfmt](#ktfmt), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) - - [C/C++](#cc) ([eclipse cdt](#eclipse-cdt)) + - [C/C++](#cc) ([clang-format](#clang-format), [eclipse cdt](#eclipse-cdt)) - [FreshMark](#freshmark) aka markdown - [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter)) - [SQL](#sql) ([dbeaver](#dbeaver), [prettier](#prettier)) - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier)) - Multiple languages - [Prettier](#prettier) ([plugins](#prettier-plugins), [npm detection](#npm-detection)) + - javascript, jsx, angular, vue, flow, typescript, css, less, scss, html, json, graphql, markdown, ymaml + - [clang-format](#clang-format) + - c, c++, c#, objective-c, protobuf, javascript, java - [eclipse web tools platform](#eclipse-web-tools-platform) + - css, html, js, json, xml - **Language independent** - [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history)) - [How can I enforce formatting gradually? (aka "ratchet")](#ratchet) @@ -116,8 +123,6 @@ Spotless consists of a list of formats (in the example above, `misc` and `java`) All the generic steps live in [`FormatExtension`](https://javadoc.io/static/com.diffplug.spotless/spotless-plugin-gradle/5.1.2/com/diffplug/gradle/spotless/FormatExtension.html), and there are many language-specific steps which live in its language-specific subclasses, which are described below. -Spotless supports all of Gradle's built-in performance features (incremental build, buildcache, lazy configuration, etc), and also automatically fixes [idempotence issues](https://github.com/diffplug/spotless/blob/main/PADDEDCELL.md), infers [line-endings from git](#line-endings-and-encodings-invisible-stuff), is cautious about [misconfigured encoding](https://github.com/diffplug/spotless/blob/08340a11566cdf56ecf50dbd4d557ed84a70a502/testlib/src/test/java/com/diffplug/spotless/EncodingErrorMsgTest.java#L34-L38) bugs, and can use git to [ratchet formatting](#ratchet) without "format-everything" commits. - ### Requirements Spotless requires JRE 8+, and Gradle 5.4+. Some steps require JRE 11+, `Unsupported major.minor version` means you're using a step that needs a newer JRE. @@ -143,6 +148,7 @@ spotless { googleJavaFormat() // has its own section below eclipse() // has its own section below prettier() // has its own section below + clangFormat() // has its own section below licenseHeader '/* (C) $YEAR */' // or licenseHeaderFile } @@ -322,7 +328,8 @@ spotless { cpp { target 'src/native/**' // you have to set the target manually - eclipseCdt() // has its own section below + clangFormat() // has its own section below + eclipseCdt() // has its own section below licenseHeader '/* (C) $YEAR */' // or licenseHeaderFile } @@ -549,6 +556,33 @@ spotless { prettier().npmExecutable('/usr/bin/npm').config(...) ``` +## clang-format + +[homepage](https://clang.llvm.org/docs/ClangFormat.html). [changelog](https://releases.llvm.org/download.html). `clang-format` is a formatter for c, c++, c#, objective-c, protobuf, javascript, and java. You can use clang-format in any language-specific format, but usually you will be creating a generic format. + +```gradle +spotless { + format 'csharp', { + // you have to set the target manually + target 'src/**/*.cs' + + clangFormat('10.0.1') // version is optional + + // can also specify a code style + clangFormat().style('LLVM') // or Google, Chromium, Mozilla, WebKit + // TODO: support arbitrary .clang-format + + // if clang-format is not on your path, you must specify its location manually + clangFormat().pathToExe('/usr/local/Cellar/clang-format/10.0.1/bin/clang-format') + // Spotless always checks the version of the clang-format it is using + // and will fail with an error if it does not match the expected version + // (whether manually specified or default). If there is a problem, Spotless + // will suggest commands to help install the correct version. + // TODO: handle installation & packaging automatically + } +} +``` + ## Eclipse web tools platform From 9901ab8e159d37c82de09bfb7e1ab0fcee27d859 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Sun, 23 Aug 2020 23:46:58 -0700 Subject: [PATCH 09/27] Add docs for python-black to plugin-gradle. --- plugin-gradle/README.md | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index e8ff170cad..b04ee0deac 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -63,6 +63,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [Kotlin](#kotlin) ([ktlint](#ktlint), [ktfmt](#ktfmt), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) - [C/C++](#cc) ([clang-format](#clang-format), [eclipse cdt](#eclipse-cdt)) + - [Python](#python) ([black](#black)) - [FreshMark](#freshmark) aka markdown - [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter)) - [SQL](#sql) ([dbeaver](#dbeaver), [prettier](#prettier)) @@ -328,8 +329,8 @@ spotless { cpp { target 'src/native/**' // you have to set the target manually - clangFormat() // has its own section below - eclipseCdt() // has its own section below + clangFormat() // has its own section below + eclipseCdt() // has its own section below licenseHeader '/* (C) $YEAR */' // or licenseHeaderFile } @@ -351,6 +352,38 @@ spotles { } ``` +## Python + +`com.diffplug.gradle.spotless.PythonExtension` [javadoc](https://javadoc.io/static/com.diffplug.spotless/spotless-plugin-gradle/5.1.2/com/diffplug/gradle/spotless/PythonExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/PythonExtension.java) + +```gradle +spotless { + python { + target 'src/main/**/*.py' // have to set manually + + black() // has its own section below + + licenseHeader '/* (C) $YEAR */', 'REGEX_TO_DEFINE_TOP_OF_FILE' // or licenseHeaderFile + } +} +``` + +### black + +[homepage](https://github.com/psf/black). [changelog](https://github.com/psf/black/blob/master/CHANGES.md). + +```gradle +black('19.10b0') // version is optional + +// if black is not on your path, you must specify its location manually +clangFormat().pathToExe('C:/myuser/.pyenv/versions/3.8.0/scripts/black.exe') +// Spotless always checks the version of the black it is using +// and will fail with an error if it does not match the expected version +// (whether manually specified or default). If there is a problem, Spotless +// will suggest commands to help install the correct version. +// TODO: handle installation & packaging automatically +``` + ## FreshMark From eceb244bd507b386fabaac496d437d25ae096519 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 00:10:15 -0700 Subject: [PATCH 10/27] Add tags for black and clang-format --- plugin-gradle/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle index db66119522..216d6fa01a 100644 --- a/plugin-gradle/build.gradle +++ b/plugin-gradle/build.gradle @@ -82,7 +82,9 @@ if (version.endsWith('-SNAPSHOT')) { 'tsfmt', 'prettier', 'scalafmt', - 'scalafix' + 'scalafix', + 'black', + 'clang-format' ] plugins { spotlessPlugin { From 1b1abed4726b2e442a0e48826ab12fb6c6d8d9bf Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 00:32:32 -0700 Subject: [PATCH 11/27] Add categories to keep clang and black out of the standard tests. --- gradle/special-tests.gradle | 16 ++++++++++++++++ plugin-gradle/build.gradle | 12 +----------- plugin-maven/build.gradle | 16 +++------------- testlib/build.gradle | 4 +--- .../diffplug/spotless/category/BlackTest.java | 18 ++++++++++++++++++ .../diffplug/spotless/category/ClangTest.java | 18 ++++++++++++++++++ .../{generic => cpp}/ClangFormatStepTest.java | 6 ++++-- .../spotless/python/BlackStepTest.java | 3 +++ 8 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 gradle/special-tests.gradle create mode 100644 testlib/src/main/java/com/diffplug/spotless/category/BlackTest.java create mode 100644 testlib/src/main/java/com/diffplug/spotless/category/ClangTest.java rename testlib/src/test/java/com/diffplug/spotless/{generic => cpp}/ClangFormatStepTest.java (89%) diff --git a/gradle/special-tests.gradle b/gradle/special-tests.gradle new file mode 100644 index 0000000000..91aab76df5 --- /dev/null +++ b/gradle/special-tests.gradle @@ -0,0 +1,16 @@ +def special = [ + 'Npm', + 'Black', + 'Clang' +] + +tasks.named('test') { + useJUnit { excludeCategories special.collect { "com.diffplug.spotless.category.${it}Test" } as String[] } +} + +special.forEach { + def category = "com.diffplug.spotless.category.${it}Test" + tasks.register("${it}Test", Test) { + useJUnit { includeCategories category } + } +} diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle index 216d6fa01a..f24f2cd233 100644 --- a/plugin-gradle/build.gradle +++ b/plugin-gradle/build.gradle @@ -29,17 +29,7 @@ dependencies { test { testLogging.showStandardStreams = true } -test { - useJUnit { - excludeCategories 'com.diffplug.spotless.category.NpmTest' - } -} - -task npmTest(type: Test) { - useJUnit { - includeCategories 'com.diffplug.spotless.category.NpmTest' - } -} +apply from: rootProject.file('gradle/special-tests.gradle') // make it easy for eclipse to run against latest build tasks.eclipse.dependsOn(pluginUnderTestMetadata) diff --git a/plugin-maven/build.gradle b/plugin-maven/build.gradle index 62a945bd71..84f7ce366b 100644 --- a/plugin-maven/build.gradle +++ b/plugin-maven/build.gradle @@ -190,20 +190,10 @@ jar.doLast { Files.copy(jarIn.toPath(), jarOut.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING) } -test { - useJUnit { - excludeCategories 'com.diffplug.spotless.category.NpmTest' - } -} -task npmTest(type: Test) { - useJUnit { - includeCategories 'com.diffplug.spotless.category.NpmTest' - } -} +apply from: rootProject.file('gradle/special-tests.gradle') + tasks.withType(Test) { systemProperty "localMavenRepositoryDir", LOCAL_MAVEN_REPO_DIR systemProperty "spotlessMavenPluginVersion", project.version + dependsOn(jar) } -// usually test only depends on testClasses, which doesn't run the maven build that we need -test.dependsOn(jar) -npmTest.dependsOn(jar) diff --git a/testlib/build.gradle b/testlib/build.gradle index d53285623c..9a5c1b0e5a 100644 --- a/testlib/build.gradle +++ b/testlib/build.gradle @@ -20,9 +20,7 @@ dependencies { // we'll hold the testlib to a low standard (prize brevity) spotbugs { reportLevel = 'high' } // low|medium|high (low = sensitive to even minor mistakes) -test { useJUnit { excludeCategories 'com.diffplug.spotless.category.NpmTest' } } - -task npmTest(type: Test) { useJUnit { includeCategories 'com.diffplug.spotless.category.NpmTest' } } +apply from: rootProject.file('gradle/special-tests.gradle') javadoc { options.addStringOption('Xdoclint:none', '-quiet') diff --git a/testlib/src/main/java/com/diffplug/spotless/category/BlackTest.java b/testlib/src/main/java/com/diffplug/spotless/category/BlackTest.java new file mode 100644 index 0000000000..4b77e4f4fa --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/category/BlackTest.java @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2020 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.spotless.category; + +public interface BlackTest {} diff --git a/testlib/src/main/java/com/diffplug/spotless/category/ClangTest.java b/testlib/src/main/java/com/diffplug/spotless/category/ClangTest.java new file mode 100644 index 0000000000..f63cff4a3f --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/category/ClangTest.java @@ -0,0 +1,18 @@ +/* + * Copyright 2016-2020 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.spotless.category; + +public interface ClangTest {} diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/ClangFormatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/cpp/ClangFormatStepTest.java similarity index 89% rename from testlib/src/test/java/com/diffplug/spotless/generic/ClangFormatStepTest.java rename to testlib/src/test/java/com/diffplug/spotless/cpp/ClangFormatStepTest.java index 74cb5f4f9e..f8500c4eb5 100644 --- a/testlib/src/test/java/com/diffplug/spotless/generic/ClangFormatStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/cpp/ClangFormatStepTest.java @@ -13,16 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.diffplug.spotless.generic; +package com.diffplug.spotless.cpp; import java.io.File; import java.util.Arrays; import org.junit.Test; +import org.junit.experimental.categories.Category; import com.diffplug.spotless.StepHarnessWithFile; -import com.diffplug.spotless.cpp.ClangFormatStep; +import com.diffplug.spotless.category.ClangTest; +@Category(ClangTest.class) public class ClangFormatStepTest { @Test public void test() throws Exception { diff --git a/testlib/src/test/java/com/diffplug/spotless/python/BlackStepTest.java b/testlib/src/test/java/com/diffplug/spotless/python/BlackStepTest.java index 7f086c96c0..91f2f9e8f3 100644 --- a/testlib/src/test/java/com/diffplug/spotless/python/BlackStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/python/BlackStepTest.java @@ -16,9 +16,12 @@ package com.diffplug.spotless.python; import org.junit.Test; +import org.junit.experimental.categories.Category; import com.diffplug.spotless.StepHarness; +import com.diffplug.spotless.category.BlackTest; +@Category(BlackTest.class) public class BlackStepTest { @Test public void test() throws Exception { From aa89c1f010bbca006d1f715e49e7bcd4e2996eb2 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 00:40:50 -0700 Subject: [PATCH 12/27] FIx spotbugs warnings. --- lib/src/main/java/com/diffplug/spotless/ForeignExe.java | 4 ++-- lib/src/main/java/com/diffplug/spotless/ProcessRunner.java | 3 +++ .../main/java/com/diffplug/spotless/cpp/ClangFormatStep.java | 3 +++ lib/src/main/java/com/diffplug/spotless/python/BlackStep.java | 3 +++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java index e04a2dcc81..19a095aab1 100644 --- a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java +++ b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java @@ -80,12 +80,12 @@ public enum ErrorKind { CANT_FIND, WRONG_VERSION } - public class SetupException extends Exception { + public static class SetupException extends Exception { private static final long serialVersionUID = -3515370807495069599L; private final ErrorKind kind; private final String msg; - private final ProcessRunner.Result result; + private final transient ProcessRunner.Result result; SetupException(ErrorKind kind, String msg, ProcessRunner.Result result) { this.kind = kind; diff --git a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java index eedb4d6cef..f6c73b3e25 100644 --- a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java +++ b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java @@ -27,6 +27,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + /** * Shelling out to a process is harder than it ought to be in Java. * If you don't read stdout and stderr on their own threads, you risk @@ -113,6 +115,7 @@ public void close() { executor.shutdown(); } + @SuppressFBWarnings({"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}) public static class Result { private final List args; private final int exitCode; diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index 5afec5f237..f57ec7bf39 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -29,6 +29,8 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.ProcessRunner; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + public class ClangFormatStep { public static String name() { return "clang"; @@ -69,6 +71,7 @@ private State createState() throws ForeignExe.SetupException, IOException, Inter return new State(this, exeAbsPath); } + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") static class State implements Serializable { private static final long serialVersionUID = -1825662356883926318L; // used for up-to-date checks and caching diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index 39c7101012..70f2acd8a0 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -28,6 +28,8 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.ProcessRunner; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + public class BlackStep { public static String name() { return "black"; @@ -62,6 +64,7 @@ private State createState() throws ForeignExe.SetupException, IOException, Inter return new State(this, exeAbsPath); } + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") static class State implements Serializable { private static final long serialVersionUID = -1825662356883926318L; // used for up-to-date checks and caching From 59953e770835237da5282a0e11cf286525f9c8be Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 19:39:03 -0700 Subject: [PATCH 13/27] Improve the error messages. --- .../com/diffplug/spotless/ForeignExe.java | 120 ++++++++++-------- .../com/diffplug/spotless/ProcessRunner.java | 18 +++ .../spotless/cpp/ClangFormatStep.java | 16 ++- .../diffplug/spotless/python/BlackStep.java | 7 +- 4 files changed, 106 insertions(+), 55 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java index 19a095aab1..f6ecaa6832 100644 --- a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java +++ b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java @@ -27,83 +27,101 @@ * If either part of that fails, it shows you why * and helps you fix it. */ -public abstract class ForeignExe { +public class ForeignExe { + private String name; + private String versionFlag = "--version"; + private Pattern versionRegex = Pattern.compile("version (\\S*)"); + private @Nullable String fixCantFind, fixWrongVersion; + + /** The name of the executable, used by "where" (win) and "which" (unix). */ public static ForeignExe named(String exeName) { - return new ForeignExe() { - @Override - protected String name() { - return exeName; - } - }; + ForeignExe foreign = new ForeignExe(); + foreign.name = exeName; + return foreign; + } + + /** The flag which causes the exe to print its version (defaults to --version). */ + public ForeignExe versionFlag(String versionFlag) { + this.versionFlag = versionFlag; + return this; } - protected abstract String name(); + /** A regex which can parse the version out of the output of the {@link #versionFlag(String)} command (defaults to `version (\\S*)`) */ + public ForeignExe versionRegex(Pattern versionRegex) { + this.versionRegex = versionRegex; + return this; + } - protected String versionFlag() { - return "--version"; + /** Use {version} anywhere you would like to inject the actual version string. */ + public ForeignExe fixCantFind(String msg) { + this.fixCantFind = msg; + return this; } - protected Pattern versionRegex() { - return Pattern.compile("version (\\S*)"); + /** Use {version} or {versionActual} anywhere you would like to inject the actual version strings. */ + public ForeignExe fixWrongVersion(String msg) { + this.fixWrongVersion = msg; + return this; } - public String confirmVersionAndGetPath(String versionExpected, @Nullable String pathToExe) throws IOException, InterruptedException, SetupException { + /** + * Searches for the executable and confirms that it has the expected version. + * If it can't find the executable, or if it doesn't have the correct version, + * throws an exception with a message describing how to fix. + */ + public String confirmVersionAndGetPath(String version, @Nullable String pathToExe) throws IOException, InterruptedException { try (ProcessRunner runner = new ProcessRunner()) { String exeAbsPath; if (pathToExe != null) { exeAbsPath = pathToExe; } else { - ProcessRunner.Result which = runner.shellWinUnix("where " + name(), "which " + name()); - if (which.exitCode() != 0) { - throw new SetupException(ErrorKind.CANT_FIND, "Unable to find " + name() + " on path", which); + ProcessRunner.Result cmdWhich = runner.shellWinUnix("where " + name, "which " + name); + if (cmdWhich.exitCode() != 0) { + throw cantFind("Unable to find " + name + " on path", cmdWhich, version); } else { - exeAbsPath = which.assertNoError(Charset.defaultCharset()).trim(); + exeAbsPath = cmdWhich.assertNoError(Charset.defaultCharset()).trim(); } } - ProcessRunner.Result version = runner.exec(exeAbsPath, versionFlag()); - if (version.exitCode() != 0) { - throw new SetupException(ErrorKind.CANT_FIND, "Unable to run " + exeAbsPath, version); + ProcessRunner.Result cmdVersion = runner.exec(exeAbsPath, versionFlag); + if (cmdVersion.exitCode() != 0) { + throw cantFind("Unable to run " + exeAbsPath, cmdVersion, version); } - Matcher versionMatcher = versionRegex().matcher(version.assertNoError(Charset.defaultCharset())); + Matcher versionMatcher = versionRegex.matcher(cmdVersion.assertNoError(Charset.defaultCharset())); if (!versionMatcher.find()) { - throw new SetupException(ErrorKind.CANT_FIND, "Unable to parse version with /" + versionRegex() + "/", version); + throw cantFind("Unable to parse version with /" + versionRegex + "/", cmdVersion, version); } - String versionParsed = versionMatcher.group(1); - if (!versionParsed.equals(versionExpected)) { - throw new SetupException(ErrorKind.WRONG_VERSION, "You specified version " + versionExpected + ", but your system has " + versionParsed, version); + String versionActual = versionMatcher.group(1); + if (!versionActual.equals(version)) { + throw wrongVersion("You specified version " + version + ", but Spotless found " + versionActual, cmdVersion, version, versionActual); } return exeAbsPath; } } - public enum ErrorKind { - CANT_FIND, WRONG_VERSION - } - - public static class SetupException extends Exception { - private static final long serialVersionUID = -3515370807495069599L; - - private final ErrorKind kind; - private final String msg; - private final transient ProcessRunner.Result result; - - SetupException(ErrorKind kind, String msg, ProcessRunner.Result result) { - this.kind = kind; - this.msg = msg; - this.result = result; - } - - public ErrorKind getKind() { - return kind; - } - - public ProcessRunner.Result getProcessResult() { - return result; + private RuntimeException cantFind(String message, ProcessRunner.Result cmd, String versionExpected) { + StringBuilder errorMsg = new StringBuilder(); + errorMsg.append(message); + errorMsg.append('\n'); + if (fixCantFind != null) { + errorMsg.append(fixCantFind.replace("{version}", versionExpected)); + errorMsg.append('\n'); } + errorMsg.append('\n'); + errorMsg.append(cmd.toString()); + throw new RuntimeException(errorMsg.toString()); + } - @Override - public String toString() { - return msg; + private RuntimeException wrongVersion(String message, ProcessRunner.Result cmd, String versionExpected, String versionActual) { + StringBuilder errorMsg = new StringBuilder(); + errorMsg.append(message); + errorMsg.append('\n'); + if (fixCantFind != null) { + errorMsg.append(fixCantFind.replace("{version}", versionExpected).replace("{versionActual}", versionActual)); + errorMsg.append('\n'); } + errorMsg.append('\n'); + errorMsg.append(cmd.toString()); + throw new RuntimeException(errorMsg.toString()); } + } diff --git a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java index f6c73b3e25..60443d71b5 100644 --- a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java +++ b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java @@ -26,6 +26,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.function.BiConsumer; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -151,5 +152,22 @@ public String assertNoError(Charset charset) { throw new RuntimeException("exit code: " + exitCode + "\nstderr: " + new String(error, charset)); } } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("## executed: " + args + "\n"); + builder.append("## exitCode: " + exitCode + "\n"); + BiConsumer perStream = (name, content) -> { + String string = new String(content, Charset.defaultCharset()).trim(); + builder.append("## " + name + ": (" + (string.isEmpty() ? "empty" : "below") + ")\n"); + if (!string.isEmpty()) { + builder.append(string); + } + }; + perStream.accept("stdout", output); + perStream.accept("stderr", error); + return builder.toString(); + } } } diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index f57ec7bf39..47ed5f91b0 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -66,8 +66,20 @@ public FormatterStep create() { return FormatterStep.createLazy(name(), this::createState, State::toFunc); } - private State createState() throws ForeignExe.SetupException, IOException, InterruptedException { - String exeAbsPath = ForeignExe.named("clang-format").confirmVersionAndGetPath(version, pathToExe); + private State createState() throws IOException, InterruptedException { + String howToInstall = "" + + "You can download clang-format from https://releases.llvm.org and " + + "then point Spotless to it with `pathToExe('/path/to/clang-format')` " + + "or you can use your platform's package manager:" + + "\n win: choco install llvm --version {version}" + + "\n mac: brew install clang-format TODO: how to specify version?" + + "\n linux: apt install clang-format (try clang-format-{version} with dropped minor versions)"; + String exeAbsPath = ForeignExe.named("clang-format") + .fixCantFind(howToInstall) + .fixWrongVersion( + "You can tell Spotless to use the version you already have with `clangFormat('{versionActual}')`" + + "or you can download the currently specified version, {version}.\n\n" + howToInstall) + .confirmVersionAndGetPath(version, pathToExe); return new State(this, exeAbsPath); } diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index 70f2acd8a0..aa569419ce 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -59,8 +59,11 @@ public FormatterStep create() { return FormatterStep.createLazy(name(), this::createState, State::toFunc); } - private State createState() throws ForeignExe.SetupException, IOException, InterruptedException { - String exeAbsPath = ForeignExe.named("black").confirmVersionAndGetPath(version, pathToExe); + private State createState() throws IOException, InterruptedException { + String exeAbsPath = ForeignExe.named("black") + .fixCantFind("Try running `pip install black=={version}`, or else tell Spotless where it is with `black().pathToExe('path/to/executable')`") + .fixWrongVersion("Try running `pip install --force-reinstall black=={version}`, or else specify `black('{versionActual}')` to Spotless") + .confirmVersionAndGetPath(version, pathToExe); return new State(this, exeAbsPath); } From 56f163638d35908ecbc88cf757ea58eddf35f32a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 19:45:13 -0700 Subject: [PATCH 14/27] Refactor ForeignExe to a better, more future-proof API. --- .../com/diffplug/spotless/ForeignExe.java | 69 +++++++++++-------- .../spotless/cpp/ClangFormatStep.java | 5 +- .../diffplug/spotless/python/BlackStep.java | 5 +- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java index f6ecaa6832..85f449b84c 100644 --- a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java +++ b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java @@ -17,6 +17,7 @@ import java.io.IOException; import java.nio.charset.Charset; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,29 +27,37 @@ * Finds a foreign executable and checks its version. * If either part of that fails, it shows you why * and helps you fix it. + * + * Usage: `ForeignExe.nameAndVersion("grep", "2.5.7").confirmVersionAndGetAbsolutePath()` + * will find grep, confirm that it is version 2.5.7, and then return. */ public class ForeignExe { - private String name; + private @Nullable String pathToExe; private String versionFlag = "--version"; private Pattern versionRegex = Pattern.compile("version (\\S*)"); private @Nullable String fixCantFind, fixWrongVersion; + // MANDATORY + private String name; + private String version; + /** The name of the executable, used by "where" (win) and "which" (unix). */ - public static ForeignExe named(String exeName) { + public static ForeignExe nameAndVersion(String exeName, String version) { ForeignExe foreign = new ForeignExe(); - foreign.name = exeName; + foreign.name = Objects.requireNonNull(exeName); + foreign.version = Objects.requireNonNull(version); return foreign; } /** The flag which causes the exe to print its version (defaults to --version). */ public ForeignExe versionFlag(String versionFlag) { - this.versionFlag = versionFlag; + this.versionFlag = Objects.requireNonNull(versionFlag); return this; } /** A regex which can parse the version out of the output of the {@link #versionFlag(String)} command (defaults to `version (\\S*)`) */ public ForeignExe versionRegex(Pattern versionRegex) { - this.versionRegex = versionRegex; + this.versionRegex = Objects.requireNonNull(versionRegex); return this; } @@ -58,18 +67,24 @@ public ForeignExe fixCantFind(String msg) { return this; } - /** Use {version} or {versionActual} anywhere you would like to inject the actual version strings. */ + /** Use {version} or {versionFound} anywhere you would like to inject the actual version strings. */ public ForeignExe fixWrongVersion(String msg) { this.fixWrongVersion = msg; return this; } + /** Path to the executable. If null, will search for the executable on the system path. */ + public ForeignExe pathToExe(@Nullable String pathToExe) { + this.pathToExe = pathToExe; + return this; + } + /** * Searches for the executable and confirms that it has the expected version. * If it can't find the executable, or if it doesn't have the correct version, * throws an exception with a message describing how to fix. */ - public String confirmVersionAndGetPath(String version, @Nullable String pathToExe) throws IOException, InterruptedException { + public String confirmVersionAndGetAbsolutePath() throws IOException, InterruptedException { try (ProcessRunner runner = new ProcessRunner()) { String exeAbsPath; if (pathToExe != null) { @@ -77,51 +92,45 @@ public String confirmVersionAndGetPath(String version, @Nullable String pathToEx } else { ProcessRunner.Result cmdWhich = runner.shellWinUnix("where " + name, "which " + name); if (cmdWhich.exitCode() != 0) { - throw cantFind("Unable to find " + name + " on path", cmdWhich, version); + throw cantFind("Unable to find " + name + " on path", cmdWhich); } else { exeAbsPath = cmdWhich.assertNoError(Charset.defaultCharset()).trim(); } } ProcessRunner.Result cmdVersion = runner.exec(exeAbsPath, versionFlag); if (cmdVersion.exitCode() != 0) { - throw cantFind("Unable to run " + exeAbsPath, cmdVersion, version); + throw cantFind("Unable to run " + exeAbsPath, cmdVersion); } Matcher versionMatcher = versionRegex.matcher(cmdVersion.assertNoError(Charset.defaultCharset())); if (!versionMatcher.find()) { - throw cantFind("Unable to parse version with /" + versionRegex + "/", cmdVersion, version); + throw cantFind("Unable to parse version with /" + versionRegex + "/", cmdVersion); } - String versionActual = versionMatcher.group(1); - if (!versionActual.equals(version)) { - throw wrongVersion("You specified version " + version + ", but Spotless found " + versionActual, cmdVersion, version, versionActual); + String versionFound = versionMatcher.group(1); + if (!versionFound.equals(version)) { + throw wrongVersion("You specified version " + version + ", but Spotless found " + versionFound, cmdVersion, versionFound); } return exeAbsPath; } } - private RuntimeException cantFind(String message, ProcessRunner.Result cmd, String versionExpected) { - StringBuilder errorMsg = new StringBuilder(); - errorMsg.append(message); - errorMsg.append('\n'); - if (fixCantFind != null) { - errorMsg.append(fixCantFind.replace("{version}", versionExpected)); - errorMsg.append('\n'); - } - errorMsg.append('\n'); - errorMsg.append(cmd.toString()); - throw new RuntimeException(errorMsg.toString()); + private RuntimeException cantFind(String message, ProcessRunner.Result cmd) { + return exceptionFmt(message, cmd, fixCantFind == null ? null : fixCantFind.replace("{version}", version)); + } + + private RuntimeException wrongVersion(String message, ProcessRunner.Result cmd, String versionFound) { + return exceptionFmt(message, cmd, fixWrongVersion == null ? null : fixWrongVersion.replace("{version}", version).replace("{versionFound}", versionFound)); } - private RuntimeException wrongVersion(String message, ProcessRunner.Result cmd, String versionExpected, String versionActual) { + private RuntimeException exceptionFmt(String msgPrimary, ProcessRunner.Result cmd, @Nullable String msgFix) { StringBuilder errorMsg = new StringBuilder(); - errorMsg.append(message); + errorMsg.append(msgPrimary); errorMsg.append('\n'); - if (fixCantFind != null) { - errorMsg.append(fixCantFind.replace("{version}", versionExpected).replace("{versionActual}", versionActual)); + if (msgFix != null) { + errorMsg.append(msgFix); errorMsg.append('\n'); } errorMsg.append('\n'); errorMsg.append(cmd.toString()); - throw new RuntimeException(errorMsg.toString()); + return new RuntimeException(errorMsg.toString()); } - } diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index 47ed5f91b0..ad4970ea70 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -74,12 +74,13 @@ private State createState() throws IOException, InterruptedException { "\n win: choco install llvm --version {version}" + "\n mac: brew install clang-format TODO: how to specify version?" + "\n linux: apt install clang-format (try clang-format-{version} with dropped minor versions)"; - String exeAbsPath = ForeignExe.named("clang-format") + String exeAbsPath = ForeignExe.nameAndVersion("clang-format", version) + .pathToExe(pathToExe) .fixCantFind(howToInstall) .fixWrongVersion( "You can tell Spotless to use the version you already have with `clangFormat('{versionActual}')`" + "or you can download the currently specified version, {version}.\n\n" + howToInstall) - .confirmVersionAndGetPath(version, pathToExe); + .confirmVersionAndGetAbsolutePath(); return new State(this, exeAbsPath); } diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index aa569419ce..62438edb2e 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -60,10 +60,11 @@ public FormatterStep create() { } private State createState() throws IOException, InterruptedException { - String exeAbsPath = ForeignExe.named("black") + String exeAbsPath = ForeignExe.nameAndVersion("black", version) + .pathToExe(pathToExe) .fixCantFind("Try running `pip install black=={version}`, or else tell Spotless where it is with `black().pathToExe('path/to/executable')`") .fixWrongVersion("Try running `pip install --force-reinstall black=={version}`, or else specify `black('{versionActual}')` to Spotless") - .confirmVersionAndGetPath(version, pathToExe); + .confirmVersionAndGetAbsolutePath(); return new State(this, exeAbsPath); } From 425a2a6f3cc31ecdcee2af2a373c21053d77adca Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 20:40:28 -0700 Subject: [PATCH 15/27] Improve ProcessRunner performance with reusable buffers. --- .../com/diffplug/spotless/ProcessRunner.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java index 60443d71b5..4491bf4582 100644 --- a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java +++ b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java @@ -41,11 +41,12 @@ * calls to an external process. */ public class ProcessRunner implements AutoCloseable { - private ExecutorService executor; + private final ExecutorService threadStdOut = Executors.newSingleThreadExecutor(); + private final ExecutorService threadStdErr = Executors.newSingleThreadExecutor(); + private final ByteArrayOutputStream bufStdOut = new ByteArrayOutputStream(); + private final ByteArrayOutputStream bufStdErr = new ByteArrayOutputStream(); - public ProcessRunner() { - this.executor = Executors.newFixedThreadPool(2); - } + public ProcessRunner() {} /** Executes the given shell command (using `cmd` on windows and `sh` on unix). */ public Result shell(String cmd) throws IOException, InterruptedException { @@ -82,8 +83,8 @@ public Result exec(List args) throws IOException, InterruptedException { public Result exec(byte[] stdin, List args) throws IOException, InterruptedException { ProcessBuilder builder = new ProcessBuilder(args); Process process = builder.start(); - Future outputFut = executor.submit(() -> drainToBytes(process.getInputStream())); - Future errorFut = executor.submit(() -> drainToBytes(process.getErrorStream())); + Future outputFut = threadStdOut.submit(() -> drainToBytes(process.getInputStream(), bufStdOut)); + Future errorFut = threadStdErr.submit(() -> drainToBytes(process.getErrorStream(), bufStdErr)); // write stdin process.getOutputStream().write(stdin); process.getOutputStream().close(); @@ -105,15 +106,16 @@ private static void drain(InputStream input, OutputStream output) throws IOExcep } } - private static byte[] drainToBytes(InputStream input) throws IOException { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - drain(input, output); - return output.toByteArray(); + private static byte[] drainToBytes(InputStream input, ByteArrayOutputStream buffer) throws IOException { + buffer.reset(); + drain(input, buffer); + return buffer.toByteArray(); } @Override public void close() { - executor.shutdown(); + threadStdOut.shutdown(); + threadStdErr.shutdown(); } @SuppressFBWarnings({"EI_EXPOSE_REP", "EI_EXPOSE_REP2"}) From f673500266a54ed7583febbebc28e0cfdb8760d9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 20:46:36 -0700 Subject: [PATCH 16/27] Improve the ProcessRunner API. --- .../java/com/diffplug/spotless/ForeignExe.java | 8 ++++---- .../com/diffplug/spotless/ProcessRunner.java | 16 ++++++++++++++-- .../diffplug/spotless/cpp/ClangFormatStep.java | 2 +- .../com/diffplug/spotless/python/BlackStep.java | 2 +- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java index 85f449b84c..537c5a050d 100644 --- a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java +++ b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java @@ -91,17 +91,17 @@ public String confirmVersionAndGetAbsolutePath() throws IOException, Interrupted exeAbsPath = pathToExe; } else { ProcessRunner.Result cmdWhich = runner.shellWinUnix("where " + name, "which " + name); - if (cmdWhich.exitCode() != 0) { + if (cmdWhich.exitNotZero()) { throw cantFind("Unable to find " + name + " on path", cmdWhich); } else { - exeAbsPath = cmdWhich.assertNoError(Charset.defaultCharset()).trim(); + exeAbsPath = cmdWhich.assertExitZero(Charset.defaultCharset()).trim(); } } ProcessRunner.Result cmdVersion = runner.exec(exeAbsPath, versionFlag); - if (cmdVersion.exitCode() != 0) { + if (cmdVersion.exitNotZero()) { throw cantFind("Unable to run " + exeAbsPath, cmdVersion); } - Matcher versionMatcher = versionRegex.matcher(cmdVersion.assertNoError(Charset.defaultCharset())); + Matcher versionMatcher = versionRegex.matcher(cmdVersion.assertExitZero(Charset.defaultCharset())); if (!versionMatcher.find()) { throw cantFind("Unable to parse version with /" + versionRegex + "/", cmdVersion); } diff --git a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java index 4491bf4582..bc29e9bda8 100644 --- a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java +++ b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java @@ -147,11 +147,23 @@ public byte[] error() { return error; } - public String assertNoError(Charset charset) { + /** Returns true if the exit code was not zero. */ + public boolean exitNotZero() { + return exitCode != 0; + } + + /** + * Asserts that the exit code was zero, and if so, returns + * the content of stdout encoded with the given charset. + * + * If the exit code was not zero, throws an exception + * with useful debugging information. + */ + public String assertExitZero(Charset charset) { if (exitCode == 0) { return new String(output, charset); } else { - throw new RuntimeException("exit code: " + exitCode + "\nstderr: " + new String(error, charset)); + throw new RuntimeException(toString()); } } diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index ad4970ea70..d8826fbba4 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -107,7 +107,7 @@ static class State implements Serializable { String format(String input, File file) throws IOException, InterruptedException { args.set(args.size() - 1, "--assume-filename=" + file.getName()); - return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertNoError(StandardCharsets.UTF_8); + return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertExitZero(StandardCharsets.UTF_8); } FormatterFunc.Closeable toFunc() { diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index 62438edb2e..e8476b6e50 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -83,7 +83,7 @@ static class State implements Serializable { } String format(String input) throws IOException, InterruptedException { - return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertNoError(StandardCharsets.UTF_8); + return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertExitZero(StandardCharsets.UTF_8); } FormatterFunc.Closeable toFunc() { From 7f16ecca031810b5e6e6f647e1f10a6d2152d9f4 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 21:06:51 -0700 Subject: [PATCH 17/27] Fix resource leak in our new native steps. --- .../diffplug/spotless/cpp/ClangFormatStep.java | 15 ++++++++------- .../com/diffplug/spotless/python/BlackStep.java | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index d8826fbba4..31dbc5cc80 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -92,26 +92,27 @@ static class State implements Serializable { final @Nullable String style; // used for executing final transient List args; - final transient ProcessRunner runner = new ProcessRunner(); State(ClangFormatStep step, String exeAbsPath) { this.version = step.version; this.style = step.style; - args = new ArrayList<>(3); + args = new ArrayList<>(2); args.add(exeAbsPath); if (style != null) { args.add("--style=" + style); } - args.add("--assume-filename=MUTABLE"); } - String format(String input, File file) throws IOException, InterruptedException { - args.set(args.size() - 1, "--assume-filename=" + file.getName()); - return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertExitZero(StandardCharsets.UTF_8); + String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException { + String[] processArgs = args.toArray(new String[args.size() + 1]); + // add an argument to the end + processArgs[args.size()] = "--assume-filename=" + file.getName(); + return runner.exec(input.getBytes(StandardCharsets.UTF_8), processArgs).assertExitZero(StandardCharsets.UTF_8); } FormatterFunc.Closeable toFunc() { - return FormatterFunc.Closeable.of(runner, FormatterFunc.needsFile(this::format)); + ProcessRunner runner = new ProcessRunner(); + return FormatterFunc.Closeable.of(runner, FormatterFunc.needsFile((input, file) -> format(runner, input, file))); } } } diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index e8476b6e50..c566aace49 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -75,19 +75,19 @@ static class State implements Serializable { final String version; // used for executing final transient List args; - final transient ProcessRunner runner = new ProcessRunner(); State(BlackStep step, String exeAbsPath) { this.version = step.version; this.args = Arrays.asList(exeAbsPath, "-"); } - String format(String input) throws IOException, InterruptedException { + String format(ProcessRunner runner, String input) throws IOException, InterruptedException { return runner.exec(input.getBytes(StandardCharsets.UTF_8), args).assertExitZero(StandardCharsets.UTF_8); } FormatterFunc.Closeable toFunc() { - return FormatterFunc.Closeable.of(runner, this::format); + ProcessRunner runner = new ProcessRunner(); + return FormatterFunc.Closeable.of(runner, input -> format(runner, input)); } } } From 7e1f2618e1d822beee09a93029e15821e2f4b25b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 21:08:06 -0700 Subject: [PATCH 18/27] Fix {versionFound} tags in ClangFormatStep and BlackStep. --- .../main/java/com/diffplug/spotless/cpp/ClangFormatStep.java | 2 +- lib/src/main/java/com/diffplug/spotless/python/BlackStep.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index 31dbc5cc80..54510c6636 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -78,7 +78,7 @@ private State createState() throws IOException, InterruptedException { .pathToExe(pathToExe) .fixCantFind(howToInstall) .fixWrongVersion( - "You can tell Spotless to use the version you already have with `clangFormat('{versionActual}')`" + + "You can tell Spotless to use the version you already have with `clangFormat('{versionFound}')`" + "or you can download the currently specified version, {version}.\n\n" + howToInstall) .confirmVersionAndGetAbsolutePath(); return new State(this, exeAbsPath); diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index c566aace49..3b576b7963 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -63,7 +63,7 @@ private State createState() throws IOException, InterruptedException { String exeAbsPath = ForeignExe.nameAndVersion("black", version) .pathToExe(pathToExe) .fixCantFind("Try running `pip install black=={version}`, or else tell Spotless where it is with `black().pathToExe('path/to/executable')`") - .fixWrongVersion("Try running `pip install --force-reinstall black=={version}`, or else specify `black('{versionActual}')` to Spotless") + .fixWrongVersion("Try running `pip install --force-reinstall black=={version}`, or else specify `black('{versionFound}')` to Spotless") .confirmVersionAndGetAbsolutePath(); return new State(this, exeAbsPath); } From 18c10f9c93d6f18f753233d0b5f028d5f0961916 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 21:29:46 -0700 Subject: [PATCH 19/27] Refactored FormatterFunc to encourage a safer resource usage pattern. --- .../com/diffplug/spotless/FormatterFunc.java | 59 ++++++++++++++++++- .../diffplug/spotless/FormatterStepImpl.java | 6 ++ .../spotless/cpp/ClangFormatStep.java | 2 +- .../diffplug/spotless/python/BlackStep.java | 2 +- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java index 6e87346bbb..f5c58aa359 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java @@ -61,6 +61,61 @@ public String apply(String unix) throws Exception { } }; } + + @FunctionalInterface + interface ResourceFunc { + String apply(T resource, String unix) throws Exception; + } + + /** Creates a {@link FormatterFunc.Closeable} which uses the given resource to execute the format function. */ + public static Closeable of(T resource, ResourceFunc function) { + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(function, "function"); + return new Closeable() { + @Override + public void close() { + ThrowingEx.run(resource::close); + } + + @Override + public String apply(String unix, File file) throws Exception { + return function.apply(resource, unix); + } + + @Override + public String apply(String unix) throws Exception { + return function.apply(resource, unix); + } + }; + } + + @FunctionalInterface + interface ResourceFileFunc { + String apply(T resource, String unix, File file) throws Exception; + } + + /** Creates a {@link FormatterFunc.Closeable} which uses the given resource to execute the file-dependent format function. */ + public static Closeable of(T resource, ResourceFileFunc function) { + Objects.requireNonNull(resource, "resource"); + Objects.requireNonNull(function, "function"); + return new Closeable() { + @Override + public void close() { + ThrowingEx.run(resource::close); + } + + @Override + public String apply(String unix, File file) throws Exception { + FormatterStepImpl.checkNotSentinel(file); + return function.apply(resource, unix, file); + } + + @Override + public String apply(String unix) throws Exception { + return apply(unix, FormatterStepImpl.SENTINEL); + } + }; + } } /** @@ -80,9 +135,7 @@ interface NeedsFile extends FormatterFunc { @Override default String apply(String unix, File file) throws Exception { - if (file == FormatterStepImpl.SENTINEL) { - throw new IllegalArgumentException("This step requires the underlying file. If this is a test, use StepHarnessWithFile"); - } + FormatterStepImpl.checkNotSentinel(file); return applyWithFile(unix, file); } diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java b/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java index 71e00faf2f..061aff6af9 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java @@ -114,4 +114,10 @@ protected String format(Integer state, String rawUnix, File file) throws Excepti /** A dummy SENTINEL file. */ static final File SENTINEL = new File(""); + + static void checkNotSentinel(File file) { + if (file == SENTINEL) { + throw new IllegalArgumentException("This step requires the underlying file. If this is a test, use StepHarnessWithFile"); + } + } } diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index 54510c6636..ce7645b902 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -112,7 +112,7 @@ String format(ProcessRunner runner, String input, File file) throws IOException, FormatterFunc.Closeable toFunc() { ProcessRunner runner = new ProcessRunner(); - return FormatterFunc.Closeable.of(runner, FormatterFunc.needsFile((input, file) -> format(runner, input, file))); + return FormatterFunc.Closeable.of(runner, this::format); } } } diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index 3b576b7963..3e3405d182 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -87,7 +87,7 @@ String format(ProcessRunner runner, String input) throws IOException, Interrupte FormatterFunc.Closeable toFunc() { ProcessRunner runner = new ProcessRunner(); - return FormatterFunc.Closeable.of(runner, input -> format(runner, input)); + return FormatterFunc.Closeable.of(runner, this::format); } } } From 550acac92be3a75bde42bf96052e173a809cb7ab Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 21:43:01 -0700 Subject: [PATCH 20/27] Deprecate the old FormatterFunc.Closeable.of(), and rename all its usages to FormatterFunc.Closeable.ofDangerous(). It would be preferable to not use the dangerous method outside of test harnesses, but that doesn't need to happen right now. --- .../com/diffplug/spotless/FormatterFunc.java | 27 ++++++++++++++----- .../spotless/npm/PrettierFormatterStep.java | 2 +- .../spotless/npm/TsFmtFormatterStep.java | 2 +- .../com/diffplug/spotless/StepHarness.java | 4 +-- .../spotless/StepHarnessWithFile.java | 4 +-- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java index f5c58aa359..bd17e7fc8f 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java @@ -40,8 +40,17 @@ interface Closeable extends FormatterFunc, AutoCloseable { @Override void close(); - /** Creates a {@link Closeable} from an AutoCloseable and a function. */ - public static Closeable of(AutoCloseable closeable, FormatterFunc function) { + /** + * Dangerous way to create a {@link Closeable} from an AutoCloseable and a function. + * + * It's important for FormatterStep's to allocate their resources as lazily as possible. + * It's easy to create a resource inside the state, and not realize that it may not be + * released. It's far better to use one of the non-deprecated `of()` methods below. + * + * The bug (and its fix) which is easy to write using this method: https://github.com/diffplug/spotless/commit/7f16ecca031810b5e6e6f647e1f10a6d2152d9f4 + * How the `of()` methods below make the correct thing easier to write and safer: https://github.com/diffplug/spotless/commit/18c10f9c93d6f18f753233d0b5f028d5f0961916 + */ + public static Closeable ofDangerous(AutoCloseable closeable, FormatterFunc function) { Objects.requireNonNull(closeable, "closeable"); Objects.requireNonNull(function, "function"); return new Closeable() { @@ -52,16 +61,22 @@ public void close() { @Override public String apply(String unix, File file) throws Exception { - return function.apply(Objects.requireNonNull(unix), Objects.requireNonNull(file)); + return function.apply(unix, file); } @Override public String apply(String unix) throws Exception { - return function.apply(Objects.requireNonNull(unix)); + return function.apply(unix); } }; } + /** @deprecated synonym for {@link #ofDangerous(AutoCloseable, FormatterFunc)} */ + @Deprecated + public static Closeable of(AutoCloseable closeable, FormatterFunc function) { + return ofDangerous(closeable, function); + } + @FunctionalInterface interface ResourceFunc { String apply(T resource, String unix) throws Exception; @@ -90,12 +105,12 @@ public String apply(String unix) throws Exception { } @FunctionalInterface - interface ResourceFileFunc { + interface ResourceFuncNeedsFile { String apply(T resource, String unix, File file) throws Exception; } /** Creates a {@link FormatterFunc.Closeable} which uses the given resource to execute the file-dependent format function. */ - public static Closeable of(T resource, ResourceFileFunc function) { + public static Closeable of(T resource, ResourceFuncNeedsFile function) { Objects.requireNonNull(resource, "resource"); Objects.requireNonNull(function, "function"); return new Closeable() { diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java index 46487c5ce5..fb7c3ef1f9 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java @@ -84,7 +84,7 @@ public FormatterFunc createFormatterFunc() { ServerProcessInfo prettierRestServer = npmRunServer(); PrettierRestService restService = new PrettierRestService(prettierRestServer.getBaseUrl()); String prettierConfigOptions = restService.resolveConfig(this.prettierConfig.getPrettierConfigPath(), this.prettierConfig.getOptions()); - return Closeable.of(() -> endServer(restService, prettierRestServer), new PrettierFilePathPassingFormatterFunc(prettierConfigOptions, restService)); + return Closeable.ofDangerous(() -> endServer(restService, prettierRestServer), new PrettierFilePathPassingFormatterFunc(prettierConfigOptions, restService)); } catch (Exception e) { throw ThrowingEx.asRuntime(e); } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java index 2245cd0b05..3d2b1fcbc6 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java @@ -90,7 +90,7 @@ public FormatterFunc createFormatterFunc() { Map tsFmtOptions = unifyOptions(); ServerProcessInfo tsfmtRestServer = npmRunServer(); TsFmtRestService restService = new TsFmtRestService(tsfmtRestServer.getBaseUrl()); - return Closeable.of(() -> endServer(restService, tsfmtRestServer), input -> restService.format(input, tsFmtOptions)); + return Closeable.ofDangerous(() -> endServer(restService, tsfmtRestServer), input -> restService.format(input, tsFmtOptions)); } catch (Exception e) { throw ThrowingEx.asRuntime(e); } diff --git a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java index 64522197e5..38cf0fc332 100644 --- a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java +++ b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java @@ -36,7 +36,7 @@ public static StepHarness forStep(FormatterStep step) { // We don't care if an individual FormatterStep is misbehaving on line-endings, because // Formatter fixes that. No reason to care in tests either. It's likely to pop up when // running tests on Windows from time-to-time - return new StepHarness(FormatterFunc.Closeable.of( + return new StepHarness(FormatterFunc.Closeable.ofDangerous( () -> { if (step instanceof FormatterStepImpl.Standard) { ((FormatterStepImpl.Standard) step).cleanupFormatterFunc(); @@ -47,7 +47,7 @@ public static StepHarness forStep(FormatterStep step) { /** Creates a harness for testing a formatter whose steps don't depend on the file. */ public static StepHarness forFormatter(Formatter formatter) { - return new StepHarness(FormatterFunc.Closeable.of( + return new StepHarness(FormatterFunc.Closeable.ofDangerous( formatter::close, input -> formatter.compute(input, new File("")))); } diff --git a/testlib/src/main/java/com/diffplug/spotless/StepHarnessWithFile.java b/testlib/src/main/java/com/diffplug/spotless/StepHarnessWithFile.java index 296ccadc11..dced15cf47 100644 --- a/testlib/src/main/java/com/diffplug/spotless/StepHarnessWithFile.java +++ b/testlib/src/main/java/com/diffplug/spotless/StepHarnessWithFile.java @@ -36,7 +36,7 @@ public static StepHarnessWithFile forStep(FormatterStep step) { // We don't care if an individual FormatterStep is misbehaving on line-endings, because // Formatter fixes that. No reason to care in tests either. It's likely to pop up when // running tests on Windows from time-to-time - return new StepHarnessWithFile(FormatterFunc.Closeable.of( + return new StepHarnessWithFile(FormatterFunc.Closeable.ofDangerous( () -> { if (step instanceof FormatterStepImpl.Standard) { ((FormatterStepImpl.Standard) step).cleanupFormatterFunc(); @@ -57,7 +57,7 @@ public String apply(String unix, File file) throws Exception { /** Creates a harness for testing a formatter whose steps do depend on the file. */ public static StepHarnessWithFile forFormatter(Formatter formatter) { - return new StepHarnessWithFile(FormatterFunc.Closeable.of( + return new StepHarnessWithFile(FormatterFunc.Closeable.ofDangerous( formatter::close, input -> formatter.compute(input, new File("")))); } From ab212c502c4b83a312ed9ab203ef5ba46ab0e9ab Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 22:28:01 -0700 Subject: [PATCH 21/27] Improve the ergonomics of error messages. --- .../com/diffplug/spotless/ForeignExe.java | 1 - .../com/diffplug/spotless/ProcessRunner.java | 43 ++++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java index 537c5a050d..1f707fcbb8 100644 --- a/lib/src/main/java/com/diffplug/spotless/ForeignExe.java +++ b/lib/src/main/java/com/diffplug/spotless/ForeignExe.java @@ -129,7 +129,6 @@ private RuntimeException exceptionFmt(String msgPrimary, ProcessRunner.Result cm errorMsg.append(msgFix); errorMsg.append('\n'); } - errorMsg.append('\n'); errorMsg.append(cmd.toString()); return new RuntimeException(errorMsg.toString()); } diff --git a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java index bc29e9bda8..7361a583a0 100644 --- a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java +++ b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java @@ -122,13 +122,13 @@ public void close() { public static class Result { private final List args; private final int exitCode; - private final byte[] output, error; + private final byte[] stdOut, stdErr; - public Result(List args, int exitCode, byte[] output, byte[] error) { + public Result(List args, int exitCode, byte[] stdOut, byte[] stdErr) { this.args = args; this.exitCode = exitCode; - this.output = output; - this.error = error; + this.stdOut = stdOut; + this.stdErr = stdErr; } public List args() { @@ -139,12 +139,12 @@ public int exitCode() { return exitCode; } - public byte[] output() { - return output; + public byte[] stdOut() { + return stdOut; } - public byte[] error() { - return error; + public byte[] stdErr() { + return stdErr; } /** Returns true if the exit code was not zero. */ @@ -161,7 +161,7 @@ public boolean exitNotZero() { */ public String assertExitZero(Charset charset) { if (exitCode == 0) { - return new String(output, charset); + return new String(stdOut, charset); } else { throw new RuntimeException(toString()); } @@ -170,17 +170,28 @@ public String assertExitZero(Charset charset) { @Override public String toString() { StringBuilder builder = new StringBuilder(); - builder.append("## executed: " + args + "\n"); - builder.append("## exitCode: " + exitCode + "\n"); + builder.append("> arguments: " + args + "\n"); + builder.append("> exit code: " + exitCode + "\n"); BiConsumer perStream = (name, content) -> { String string = new String(content, Charset.defaultCharset()).trim(); - builder.append("## " + name + ": (" + (string.isEmpty() ? "empty" : "below") + ")\n"); - if (!string.isEmpty()) { - builder.append(string); + if (string.isEmpty()) { + builder.append("> " + name + ": (empty)\n"); + } else { + String[] lines = string.replace("\r", "").split("\n"); + if (lines.length == 1) { + builder.append("> " + name + ": " + lines[0] + "\n"); + } else { + builder.append("> " + name + ": (below)\n"); + for (String line : lines) { + builder.append("> "); + builder.append(line); + builder.append('\n'); + } + } } }; - perStream.accept("stdout", output); - perStream.accept("stderr", error); + perStream.accept(" stdout", stdOut); + perStream.accept(" stderr", stdErr); return builder.toString(); } } From a114eb6c662c185bac7288f05469054eea551d03 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 22:28:23 -0700 Subject: [PATCH 22/27] Update all changelogs and the contributing guide. --- CHANGES.md | 6 ++++++ CONTRIBUTING.md | 9 +++++++++ plugin-gradle/CHANGES.md | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index fad1120a61..808c18e29c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,12 @@ 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 +* The ability to shell out to formatters with their own executables. + * `ProcessRunner` makes it easy to efficiently and debuggably call foreign executables, and pipe their stdout and stderr to strings. + * `ForeignExe` finds executables on the path (or other strategies), and confirms that they have the correct version (to facilitate Spotless' caching). If the executable is not present or the wrong version, it points the user towards how to fix the problem. + * These classes were used to add support for [python black](https://github.com/psf/black) and [clang-format](https://clang.llvm.org/docs/ClangFormat.html). + * Incidental to this effort, `FormatterFunc.Closeable` now has new methods which make resource-handling safer. The old method is still available as `ofDangerous`, but it should not be used outside of a testing situation. There are some legacy usages of `ofDangerous` in the codebase, and it would be nice to fix them, but the existing usages are using it safely. ## [2.2.2] - 2020-08-21 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b3ef78542..7d2546cee1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,6 +104,15 @@ Here's a checklist for creating a new step for Spotless: In order for Spotless' model to work, each step needs to look only at the `String` input, otherwise they cannot compose. However, there are some cases where the source `File` is useful, such as to look at the file extension. In this case, you can pass a `FormatterFunc.NeedsFile` instead of a `FormatterFunc`. This should only be used in [rare circumstances](https://github.com/diffplug/spotless/pull/637), be careful that you don't accidentally depend on the bytes inside of the `File`! +### Integrating outside the JVM + +There are many great formatters (prettier, clang-format, black, etc.) which live entirely outside the JVM. We have two main strategies for these: + +- shell out to an external command for every file (used by clang-format and black) // TODO: link +- open a headless server and make http calls to it from Spotless (used by prettier) // TODO: link + +Because of Spotless' up-to-date checking and [git ratcheting](https://github.com/diffplug/spotless/tree/main/plugin-gradle#ratchet), Spotless actually doesn't have to call formatters very often, so even an expensive shell call for every single invocation isn't that bad. Anything that works is better than nothing, and we can always speed things up later if it feels too slow (but it probably won't). + ## How to enable the _ext projects The `_ext` projects are disabled per default, since: diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index be873e543d..8ecfac4764 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,10 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +- It is now much easier for Spotless to [integrate formatters with native executables](../../CONTRIBUTING.md#integrating-outside-the-jvm). + - Added support for [python](../#python), specifically [black](../#black). + - Added support for [clang-format](../#clang-format) for all formats. ## [5.1.2] - 2020-08-21 ### Fixed From 800d1d56dfcd42e1030bc97cedef45c7b7089568 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 22:42:30 -0700 Subject: [PATCH 23/27] Minor improvement to clang-format error messages. --- .../java/com/diffplug/spotless/cpp/ClangFormatStep.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index ce7645b902..011b29b6f7 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -71,15 +71,15 @@ private State createState() throws IOException, InterruptedException { "You can download clang-format from https://releases.llvm.org and " + "then point Spotless to it with `pathToExe('/path/to/clang-format')` " + "or you can use your platform's package manager:" + - "\n win: choco install llvm --version {version}" + - "\n mac: brew install clang-format TODO: how to specify version?" + + "\n win: choco install llvm --version {version} (try dropping version if it fails)" + + "\n mac: brew install clang-format (TODO: how to specify version?)" + "\n linux: apt install clang-format (try clang-format-{version} with dropped minor versions)"; String exeAbsPath = ForeignExe.nameAndVersion("clang-format", version) .pathToExe(pathToExe) .fixCantFind(howToInstall) .fixWrongVersion( "You can tell Spotless to use the version you already have with `clangFormat('{versionFound}')`" + - "or you can download the currently specified version, {version}.\n\n" + howToInstall) + "or you can download the currently specified version, {version}.\n" + howToInstall) .confirmVersionAndGetAbsolutePath(); return new State(this, exeAbsPath); } From e106d38787da1487957c65726b532823f5941cb0 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 22:42:49 -0700 Subject: [PATCH 24/27] Add integration tests for clang-format and python/black. --- .../spotless/ClangFormatIntegrationTest.java | 43 +++++++++++++++++++ .../gradle/spotless/PythonGradleTest.java | 43 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ClangFormatIntegrationTest.java create mode 100644 plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PythonGradleTest.java diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ClangFormatIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ClangFormatIntegrationTest.java new file mode 100644 index 0000000000..7f74842c1b --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ClangFormatIntegrationTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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.Test; +import org.junit.experimental.categories.Category; + +import com.diffplug.spotless.category.ClangTest; + +@Category(ClangTest.class) +public class ClangFormatIntegrationTest extends GradleIntegrationHarness { + @Test + public void csharp() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " format 'csharp', {", + " target 'src/**/*.cs'", + " clangFormat()", + " }", + "}"); + setFile("src/test.cs").toResource("clang/example.cs"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/test.cs").sameAsResource("clang/example.cs.clean"); + } +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PythonGradleTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PythonGradleTest.java new file mode 100644 index 0000000000..eec3b9d0de --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PythonGradleTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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.Test; +import org.junit.experimental.categories.Category; + +import com.diffplug.spotless.category.BlackTest; + +@Category(BlackTest.class) +public class PythonGradleTest extends GradleIntegrationHarness { + @Test + public void black() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " python {", + " target 'src/**/*.py'", + " black()", + " }", + "}"); + setFile("src/test.py").toResource("python/black/black.dirty"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/test.py").sameAsResource("python/black/black.clean"); + } +} From 52453ae1ee3e2da4b937ce2457cf5b50be1f2be1 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 23:19:15 -0700 Subject: [PATCH 25/27] Link the changelogs to the PR. --- CHANGES.md | 2 +- plugin-gradle/CHANGES.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 808c18e29c..0567f21a6a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added -* The ability to shell out to formatters with their own executables. +* The ability to shell out to formatters with their own executables. ([#672](https://github.com/diffplug/spotless/pull/672)) * `ProcessRunner` makes it easy to efficiently and debuggably call foreign executables, and pipe their stdout and stderr to strings. * `ForeignExe` finds executables on the path (or other strategies), and confirms that they have the correct version (to facilitate Spotless' caching). If the executable is not present or the wrong version, it points the user towards how to fix the problem. * These classes were used to add support for [python black](https://github.com/psf/black) and [clang-format](https://clang.llvm.org/docs/ClangFormat.html). diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 6e1ee22efd..73037ab305 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -4,7 +4,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added -- It is now much easier for Spotless to [integrate formatters with native executables](../../CONTRIBUTING.md#integrating-outside-the-jvm). +- It is now much easier for Spotless to [integrate formatters with native executables](../../CONTRIBUTING.md#integrating-outside-the-jvm). ([#672](https://github.com/diffplug/spotless/pull/672)) - Added support for [python](../#python), specifically [black](../#black). - Added support for [clang-format](../#clang-format) for all formats. ### Fixed From c786acd45a07d5c1189ef24ae40ef176f8e321fe Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 23:19:50 -0700 Subject: [PATCH 26/27] Link clang-format binary management to its tracking issue #673. --- .../main/java/com/diffplug/spotless/cpp/ClangFormatStep.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java index 011b29b6f7..ade6978359 100644 --- a/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/cpp/ClangFormatStep.java @@ -73,7 +73,8 @@ private State createState() throws IOException, InterruptedException { "or you can use your platform's package manager:" + "\n win: choco install llvm --version {version} (try dropping version if it fails)" + "\n mac: brew install clang-format (TODO: how to specify version?)" + - "\n linux: apt install clang-format (try clang-format-{version} with dropped minor versions)"; + "\n linux: apt install clang-format (try clang-format-{version} with dropped minor versions)" + + "\n github issue to handle this better: https://github.com/diffplug/spotless/issues/673"; String exeAbsPath = ForeignExe.nameAndVersion("clang-format", version) .pathToExe(pathToExe) .fixCantFind(howToInstall) From 55d45e863e611b6a53b4d10def50665b52efe3b9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 24 Aug 2020 23:27:20 -0700 Subject: [PATCH 27/27] Link python black package management to its tracking issue #674. --- .../main/java/com/diffplug/spotless/python/BlackStep.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java index 3e3405d182..01369b14ae 100644 --- a/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java +++ b/lib/src/main/java/com/diffplug/spotless/python/BlackStep.java @@ -60,10 +60,11 @@ public FormatterStep create() { } private State createState() throws IOException, InterruptedException { + String trackingIssue = "\n github issue to handle this better: https://github.com/diffplug/spotless/issues/674"; String exeAbsPath = ForeignExe.nameAndVersion("black", version) .pathToExe(pathToExe) - .fixCantFind("Try running `pip install black=={version}`, or else tell Spotless where it is with `black().pathToExe('path/to/executable')`") - .fixWrongVersion("Try running `pip install --force-reinstall black=={version}`, or else specify `black('{versionFound}')` to Spotless") + .fixCantFind("Try running `pip install black=={version}`, or else tell Spotless where it is with `black().pathToExe('path/to/executable')`" + trackingIssue) + .fixWrongVersion("Try running `pip install --force-reinstall black=={version}`, or else specify `black('{versionFound}')` to Spotless" + trackingIssue) .confirmVersionAndGetAbsolutePath(); return new State(this, exeAbsPath); }