diff --git a/CHANGES.md b/CHANGES.md index 6484eadf77..9c37d36604 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added +* Added support for Protobuf formatting based on [Buf](https://buf.build/). (#1208) * `enum OnMatch { INCLUDE, EXCLUDE }` so that `FormatterStep.filterByContent` can not only include based on the pattern but also exclude. ([#1749](https://github.com/diffplug/spotless/pull/1749)) ### Fixed * Update documented default `semanticSort` to `false`. ([#1728](https://github.com/diffplug/spotless/pull/1728)) diff --git a/README.md b/README.md index aceb9c22e9..3e0c4e17e1 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,8 @@ lib('markdown.FlexmarkStep') +'{{no}} | {{yes}} lib('npm.EslintFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', -lib('pom.SortPomStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |', +lib('pom.SortPomStepStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |', +lib('protobuf.BufStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |', lib('python.BlackStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |', lib('rome.RomeStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', @@ -147,7 +148,8 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}} | [`npm.EslintFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | -| [`pom.SortPomStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: | +| [`pom.SortPomStepStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStepStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: | +| [`protobuf.BufStep`](lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | | [`python.BlackStep`](lib/src/main/java/com/diffplug/spotless/python/BlackStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | | [`rome.RomeStep`](lib/src/main/java/com/diffplug/spotless/rome/RomeStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :+1: | :white_large_square: | diff --git a/gradle/special-tests.gradle b/gradle/special-tests.gradle index 52f82e4e1b..d4de449bdd 100644 --- a/gradle/special-tests.gradle +++ b/gradle/special-tests.gradle @@ -2,7 +2,8 @@ apply plugin: 'com.adarshr.test-logger' def special = [ 'Npm', 'Black', - 'Clang' + 'Clang', + 'Buf' ] boolean isCiServer = System.getenv().containsKey("CI") diff --git a/lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java b/lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java new file mode 100644 index 0000000000..c9b9efa543 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java @@ -0,0 +1,99 @@ +/* + * Copyright 2022-2023 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.protobuf; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.ForeignExe; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ProcessRunner; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class BufStep { + public static String name() { + return "buf"; + } + + public static String defaultVersion() { + return "1.24.0"; + } + + private final String version; + private final @Nullable String pathToExe; + + private BufStep(String version, @Nullable String pathToExe) { + this.version = version; + this.pathToExe = pathToExe; + } + + public static BufStep withVersion(String version) { + return new BufStep(version, null); + } + + public BufStep withPathToExe(String pathToExe) { + return new BufStep(version, pathToExe); + } + + public FormatterStep create() { + return FormatterStep.createLazy(name(), this::createState, State::toFunc); + } + + private State createState() throws IOException, InterruptedException { + String instructions = "https://docs.buf.build/installation"; + String exeAbsPath = ForeignExe.nameAndVersion("buf", version) + .pathToExe(pathToExe) + .versionRegex(Pattern.compile("(\\S*)")) + .fixCantFind("Try following the instructions at " + instructions + ", or else tell Spotless where it is with {@code buf().pathToExe('path/to/executable')}") + .confirmVersionAndGetAbsolutePath(); + 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 + final String version; + // used for executing + final transient List args; + + State(BufStep step, String exeAbsPath) { + this.version = step.version; + this.args = Arrays.asList(exeAbsPath, "format"); + } + + 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()] = file.getAbsolutePath(); + return runner.exec(input.getBytes(StandardCharsets.UTF_8), processArgs).assertExitZero(StandardCharsets.UTF_8); + } + + FormatterFunc.Closeable toFunc() { + ProcessRunner runner = new ProcessRunner(); + return FormatterFunc.Closeable.of(runner, this::format); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/protobuf/ProtobufConstants.java b/lib/src/main/java/com/diffplug/spotless/protobuf/ProtobufConstants.java new file mode 100644 index 0000000000..cd91e2671c --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/protobuf/ProtobufConstants.java @@ -0,0 +1,20 @@ +/* + * Copyright 2022-2023 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.protobuf; + +public class ProtobufConstants { + public static final String LICENSE_HEADER_DELIMITER = "syntax"; +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index b3b700691a..33bd1ede30 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added * Add target option `targetExcludeIfContentContains` and `targetExcludeIfContentContainsRegex` to exclude files based on their text content. ([#1749](https://github.com/diffplug/spotless/pull/1749)) +* Add support for Protobuf formatting based on [Buf](https://buf.build/) ([#1208](https://github.com/diffplug/spotless/pull/1208)). * Add an overload for `FormatExtension.addStep` which provides access to the `FormatExtension`'s `Provisioner`, enabling custom steps to make use of third-party dependencies. ### Fixed * Correctly support the syntax diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 704e3b694a..55b6e98a30 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -59,6 +59,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) - [C/C++](#cc) ([clang-format](#clang-format), [eclipse cdt](#eclipse-cdt)) + - [Protobuf](#protobuf) ([buf](#buf), [clang-format](#clang-format)) - [Python](#python) ([black](#black)) - [FreshMark](#freshmark) aka markdown - [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter)) @@ -517,6 +518,40 @@ black().pathToExe('C:/myuser/.pyenv/versions/3.8.0/scripts/black.exe') +## Protobuf + +### buf + +`com.diffplug.gradle.spotless.ProtobufExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.19.0/com/diffplug/gradle/spotless/ProtobufExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ProtobufExtension.java) + +**WARNING** this step **must** be the first step in the chain, steps before it will be ignored. Thumbs up [this issue](https://github.com/bufbuild/buf/issues/1035) for a resolution, see [here](https://github.com/diffplug/spotless/pull/1208#discussion_r1264439669) for more details on the problem. + +```gradle +spotless { + protobuf { + // by default the target is every '.proto' file in the project + buf() + + licenseHeader '/* (C) $YEAR */' // or licenseHeaderFile + } +} +``` + +When used in conjunction with the [buf-gradle-plugin](https://github.com/bufbuild/buf-gradle-plugin), the `buf` executable can be resolved from its `bufTool` configuration: + +```gradle +spotless { + protobuf { + buf().pathToExe(configurations.getByName(BUF_BINARY_CONFIGURATION_NAME).getSingleFile().getAbsolutePath()) + } +} + +// Be sure to disable the buf-gradle-plugin's execution of `buf format`: +buf { + enforceFormat = false +} +``` + ## FreshMark `com.diffplug.gradle.spotless.FreshMarkExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.19.0/com/diffplug/gradle/spotless/FreshMarkExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FreshMarkExtension.java) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ProtobufExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ProtobufExtension.java new file mode 100644 index 0000000000..7b0a6bb30a --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/ProtobufExtension.java @@ -0,0 +1,109 @@ +/* + * Copyright 2022-2023 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 static com.diffplug.spotless.protobuf.ProtobufConstants.LICENSE_HEADER_DELIMITER; + +import java.util.Objects; + +import javax.inject.Inject; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.protobuf.BufStep; + +public class ProtobufExtension extends FormatExtension implements HasBuiltinDelimiterForLicense { + static final String NAME = "protobuf"; + + @Inject + public ProtobufExtension(SpotlessExtension spotless) { + super(spotless); + } + + @Override + public LicenseHeaderConfig licenseHeader(String licenseHeader) { + return licenseHeader(licenseHeader, LICENSE_HEADER_DELIMITER); + } + + @Override + public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile) { + return licenseHeaderFile(licenseHeaderFile, LICENSE_HEADER_DELIMITER); + } + + /** If the user hasn't specified files, assume all protobuf files should be checked. */ + @Override + protected void setupTask(SpotlessTask task) { + if (target == null) { + target = parseTarget("**/*.proto"); + } + super.setupTask(task); + } + + /** Adds the specified version of buf. */ + public BufFormatExtension buf(String version) { + Objects.requireNonNull(version); + return new BufFormatExtension(version); + } + + public BufFormatExtension buf() { + return buf(BufStep.defaultVersion()); + } + + public class BufFormatExtension { + BufStep step; + + BufFormatExtension(String version) { + this.step = BufStep.withVersion(version); + if (!steps.isEmpty()) { + throw new IllegalArgumentException("buf() must be the first step, move other steps after it. Thumbs up [this issue](https://github.com/bufbuild/buf/issues/1035) for a resolution, see [here](https://github.com/diffplug/spotless/pull/1208#discussion_r1264439669) for more details on the problem."); + } + addStep(createStep()); + } + + /** + * When used in conjunction with the {@code buf-gradle-plugin}, + * the {@code buf} executable can be resolved from its {@code bufTool} configuration: + * + *
+		 * {@code
+		 * spotless {
+		 *   protobuf {
+		 *     buf().pathToExe(configurations.getByName(BUF_BINARY_CONFIGURATION_NAME).getSingleFile().getAbsolutePath())
+		 *   }
+		 * }
+		 * }
+		 * 
+ * + * Be sure to disable the {@code buf-gradle-plugin}'s execution of {@code buf format}: + * + *
+		 * {@code
+		 * buf {
+		 *   enforceFormat = false
+		 * }
+		 * }
+		 * 
+ */ + public BufFormatExtension pathToExe(String pathToExe) { + step = step.withPathToExe(pathToExe); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return step.create(); + } + } +} 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 e13e408cfd..c3cf5123c0 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 @@ -193,6 +193,12 @@ public void json(Action closure) { format(JsonExtension.NAME, JsonExtension.class, closure); } + /** Configures the special protobuf-specific extension. */ + public void protobuf(Action closure) { + requireNonNull(closure); + format(ProtobufExtension.NAME, ProtobufExtension.class, closure); + } + /** Configures the special YAML-specific extension. */ public void yaml(Action closure) { requireNonNull(closure); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BufIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BufIntegrationTest.java new file mode 100644 index 0000000000..b91512c7ed --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/BufIntegrationTest.java @@ -0,0 +1,58 @@ +/* + * Copyright 2022-2023 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.jupiter.api.Test; + +import com.diffplug.spotless.tag.BufTest; + +@BufTest +class BufIntegrationTest extends GradleIntegrationHarness { + @Test + void buf() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " protobuf {", + " buf()", + " }", + "}"); + setFile("buf.proto").toResource("protobuf/buf/buf.proto"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("buf.proto").sameAsResource("protobuf/buf/buf.proto.clean"); + } + + @Test + void bufWithLicense() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "spotless {", + " protobuf {", + " buf()", + " licenseHeader '/* (C) 2022 */'", + " }", + "}"); + setFile("license.proto").toResource("protobuf/buf/license.proto"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("license.proto").sameAsResource("protobuf/buf/license.proto.clean"); + } +} diff --git a/testlib/src/main/java/com/diffplug/spotless/tag/BufTest.java b/testlib/src/main/java/com/diffplug/spotless/tag/BufTest.java new file mode 100644 index 0000000000..90f32ec6c3 --- /dev/null +++ b/testlib/src/main/java/com/diffplug/spotless/tag/BufTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2022-2023 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.tag; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Tag; + +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +@Tag("Buf") +public @interface BufTest {} diff --git a/testlib/src/main/resources/protobuf/buf/buf.proto b/testlib/src/main/resources/protobuf/buf/buf.proto new file mode 100644 index 0000000000..a90d58bbd0 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/buf.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/protobuf/buf/buf.proto.clean b/testlib/src/main/resources/protobuf/buf/buf.proto.clean new file mode 100644 index 0000000000..faa8d91f24 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/buf.proto.clean @@ -0,0 +1,5 @@ +message Testing { + required string field1 = 1; + required int32 field2 = 2; + optional string field3 = 3; +} diff --git a/testlib/src/main/resources/protobuf/buf/license.proto b/testlib/src/main/resources/protobuf/buf/license.proto new file mode 100644 index 0000000000..aabedc5093 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/license.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +message Testing { + required string field1 = 1; + required int32 field2 = 2; + optional string field3 = 3; +} diff --git a/testlib/src/main/resources/protobuf/buf/license.proto.clean b/testlib/src/main/resources/protobuf/buf/license.proto.clean new file mode 100644 index 0000000000..11761efc28 --- /dev/null +++ b/testlib/src/main/resources/protobuf/buf/license.proto.clean @@ -0,0 +1,8 @@ +/* (C) 2022 */ +syntax = "proto3"; + +message Testing { + required string field1 = 1; + required int32 field2 = 2; + optional string field3 = 3; +} diff --git a/testlib/src/test/java/com/diffplug/spotless/protobuf/BufStepTest.java b/testlib/src/test/java/com/diffplug/spotless/protobuf/BufStepTest.java new file mode 100644 index 0000000000..ce22eb5ed2 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/protobuf/BufStepTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022-2023 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.protobuf; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.StepHarnessWithFile; +import com.diffplug.spotless.tag.BufTest; + +@BufTest +class BufStepTest extends ResourceHarness { + @Test + void test() throws Exception { + try (StepHarnessWithFile harness = StepHarnessWithFile.forStep(this, BufStep.withVersion(BufStep.defaultVersion()).create())) { + harness.testResource("protobuf/buf/buf.proto", "protobuf/buf/buf.proto.clean"); + } + } +}