Skip to content

Commit 78a6dbc

Browse files
authored
Implement buf format Gradle step (#1208)
2 parents 1b24f48 + 0fc9036 commit 78a6dbc

File tree

16 files changed

+422
-3
lines changed

16 files changed

+422
-3
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1111

1212
## [Unreleased]
1313
### Added
14+
* Added support for Protobuf formatting based on [Buf](https://buf.build/). (#1208)
1415
* `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))
1516
### Fixed
1617
* Update documented default `semanticSort` to `false`. ([#1728](https://github.com/diffplug/spotless/pull/1728))

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ lib('markdown.FlexmarkStep') +'{{no}} | {{yes}}
9797
lib('npm.EslintFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
9898
lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
9999
lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
100-
lib('pom.SortPomStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
100+
lib('pom.SortPomStepStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
101+
lib('protobuf.BufStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
101102
lib('python.BlackStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
102103
lib('rome.RomeStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
103104
lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
@@ -147,7 +148,8 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}}
147148
| [`npm.EslintFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
148149
| [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
149150
| [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
150-
| [`pom.SortPomStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
151+
| [`pom.SortPomStepStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStepStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
152+
| [`protobuf.BufStep`](lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
151153
| [`python.BlackStep`](lib/src/main/java/com/diffplug/spotless/python/BlackStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
152154
| [`rome.RomeStep`](lib/src/main/java/com/diffplug/spotless/rome/RomeStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
153155
| [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :+1: | :white_large_square: |

gradle/special-tests.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ apply plugin: 'com.adarshr.test-logger'
22
def special = [
33
'Npm',
44
'Black',
5-
'Clang'
5+
'Clang',
6+
'Buf'
67
]
78

89
boolean isCiServer = System.getenv().containsKey("CI")
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2022-2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.protobuf;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.io.Serializable;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.Arrays;
23+
import java.util.List;
24+
import java.util.regex.Pattern;
25+
26+
import javax.annotation.Nullable;
27+
28+
import com.diffplug.spotless.ForeignExe;
29+
import com.diffplug.spotless.FormatterFunc;
30+
import com.diffplug.spotless.FormatterStep;
31+
import com.diffplug.spotless.ProcessRunner;
32+
33+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
34+
35+
public class BufStep {
36+
public static String name() {
37+
return "buf";
38+
}
39+
40+
public static String defaultVersion() {
41+
return "1.24.0";
42+
}
43+
44+
private final String version;
45+
private final @Nullable String pathToExe;
46+
47+
private BufStep(String version, @Nullable String pathToExe) {
48+
this.version = version;
49+
this.pathToExe = pathToExe;
50+
}
51+
52+
public static BufStep withVersion(String version) {
53+
return new BufStep(version, null);
54+
}
55+
56+
public BufStep withPathToExe(String pathToExe) {
57+
return new BufStep(version, pathToExe);
58+
}
59+
60+
public FormatterStep create() {
61+
return FormatterStep.createLazy(name(), this::createState, State::toFunc);
62+
}
63+
64+
private State createState() throws IOException, InterruptedException {
65+
String instructions = "https://docs.buf.build/installation";
66+
String exeAbsPath = ForeignExe.nameAndVersion("buf", version)
67+
.pathToExe(pathToExe)
68+
.versionRegex(Pattern.compile("(\\S*)"))
69+
.fixCantFind("Try following the instructions at " + instructions + ", or else tell Spotless where it is with {@code buf().pathToExe('path/to/executable')}")
70+
.confirmVersionAndGetAbsolutePath();
71+
return new State(this, exeAbsPath);
72+
}
73+
74+
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
75+
static class State implements Serializable {
76+
private static final long serialVersionUID = -1825662356883926318L;
77+
// used for up-to-date checks and caching
78+
final String version;
79+
// used for executing
80+
final transient List<String> args;
81+
82+
State(BufStep step, String exeAbsPath) {
83+
this.version = step.version;
84+
this.args = Arrays.asList(exeAbsPath, "format");
85+
}
86+
87+
String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException {
88+
String[] processArgs = args.toArray(new String[args.size() + 1]);
89+
// add an argument to the end
90+
processArgs[args.size()] = file.getAbsolutePath();
91+
return runner.exec(input.getBytes(StandardCharsets.UTF_8), processArgs).assertExitZero(StandardCharsets.UTF_8);
92+
}
93+
94+
FormatterFunc.Closeable toFunc() {
95+
ProcessRunner runner = new ProcessRunner();
96+
return FormatterFunc.Closeable.of(runner, this::format);
97+
}
98+
}
99+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2022-2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.protobuf;
17+
18+
public class ProtobufConstants {
19+
public static final String LICENSE_HEADER_DELIMITER = "syntax";
20+
}

plugin-gradle/CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
55
## [Unreleased]
66
### Added
77
* Add target option `targetExcludeIfContentContains` and `targetExcludeIfContentContainsRegex` to exclude files based on their text content. ([#1749](https://github.com/diffplug/spotless/pull/1749))
8+
* Add support for Protobuf formatting based on [Buf](https://buf.build/) ([#1208](https://github.com/diffplug/spotless/pull/1208)).
89
* Add an overload for `FormatExtension.addStep` which provides access to the `FormatExtension`'s `Provisioner`, enabling custom steps to make use of third-party dependencies.
910
### Fixed
1011
* Correctly support the syntax

plugin-gradle/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui
5959
- [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [prettier](#prettier))
6060
- [Scala](#scala) ([scalafmt](#scalafmt))
6161
- [C/C++](#cc) ([clang-format](#clang-format), [eclipse cdt](#eclipse-cdt))
62+
- [Protobuf](#protobuf) ([buf](#buf), [clang-format](#clang-format))
6263
- [Python](#python) ([black](#black))
6364
- [FreshMark](#freshmark) aka markdown
6465
- [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter))
@@ -517,6 +518,40 @@ black().pathToExe('C:/myuser/.pyenv/versions/3.8.0/scripts/black.exe')
517518
518519
<a name="applying-freshmark-to-markdown-files"></a>
519520
521+
## Protobuf
522+
523+
### buf
524+
525+
`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)
526+
527+
**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.
528+
529+
```gradle
530+
spotless {
531+
protobuf {
532+
// by default the target is every '.proto' file in the project
533+
buf()
534+
535+
licenseHeader '/* (C) $YEAR */' // or licenseHeaderFile
536+
}
537+
}
538+
```
539+
540+
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:
541+
542+
```gradle
543+
spotless {
544+
protobuf {
545+
buf().pathToExe(configurations.getByName(BUF_BINARY_CONFIGURATION_NAME).getSingleFile().getAbsolutePath())
546+
}
547+
}
548+
549+
// Be sure to disable the buf-gradle-plugin's execution of `buf format`:
550+
buf {
551+
enforceFormat = false
552+
}
553+
```
554+
520555
## FreshMark
521556
522557
`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)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2022-2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.gradle.spotless;
17+
18+
import static com.diffplug.spotless.protobuf.ProtobufConstants.LICENSE_HEADER_DELIMITER;
19+
20+
import java.util.Objects;
21+
22+
import javax.inject.Inject;
23+
24+
import com.diffplug.spotless.FormatterStep;
25+
import com.diffplug.spotless.protobuf.BufStep;
26+
27+
public class ProtobufExtension extends FormatExtension implements HasBuiltinDelimiterForLicense {
28+
static final String NAME = "protobuf";
29+
30+
@Inject
31+
public ProtobufExtension(SpotlessExtension spotless) {
32+
super(spotless);
33+
}
34+
35+
@Override
36+
public LicenseHeaderConfig licenseHeader(String licenseHeader) {
37+
return licenseHeader(licenseHeader, LICENSE_HEADER_DELIMITER);
38+
}
39+
40+
@Override
41+
public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile) {
42+
return licenseHeaderFile(licenseHeaderFile, LICENSE_HEADER_DELIMITER);
43+
}
44+
45+
/** If the user hasn't specified files, assume all protobuf files should be checked. */
46+
@Override
47+
protected void setupTask(SpotlessTask task) {
48+
if (target == null) {
49+
target = parseTarget("**/*.proto");
50+
}
51+
super.setupTask(task);
52+
}
53+
54+
/** Adds the specified version of <a href="https://buf.build/">buf</a>. */
55+
public BufFormatExtension buf(String version) {
56+
Objects.requireNonNull(version);
57+
return new BufFormatExtension(version);
58+
}
59+
60+
public BufFormatExtension buf() {
61+
return buf(BufStep.defaultVersion());
62+
}
63+
64+
public class BufFormatExtension {
65+
BufStep step;
66+
67+
BufFormatExtension(String version) {
68+
this.step = BufStep.withVersion(version);
69+
if (!steps.isEmpty()) {
70+
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.");
71+
}
72+
addStep(createStep());
73+
}
74+
75+
/**
76+
* When used in conjunction with the <a href=https://github.com/bufbuild/buf-gradle-plugin>{@code buf-gradle-plugin}</a>,
77+
* the {@code buf} executable can be resolved from its {@code bufTool} configuration:
78+
*
79+
* <pre>
80+
* {@code
81+
* spotless {
82+
* protobuf {
83+
* buf().pathToExe(configurations.getByName(BUF_BINARY_CONFIGURATION_NAME).getSingleFile().getAbsolutePath())
84+
* }
85+
* }
86+
* }
87+
* </pre>
88+
*
89+
* Be sure to disable the {@code buf-gradle-plugin}'s execution of {@code buf format}:
90+
*
91+
* <pre>
92+
* {@code
93+
* buf {
94+
* enforceFormat = false
95+
* }
96+
* }
97+
* </pre>
98+
*/
99+
public BufFormatExtension pathToExe(String pathToExe) {
100+
step = step.withPathToExe(pathToExe);
101+
replaceStep(createStep());
102+
return this;
103+
}
104+
105+
private FormatterStep createStep() {
106+
return step.create();
107+
}
108+
}
109+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ public void json(Action<JsonExtension> closure) {
193193
format(JsonExtension.NAME, JsonExtension.class, closure);
194194
}
195195

196+
/** Configures the special protobuf-specific extension. */
197+
public void protobuf(Action<ProtobufExtension> closure) {
198+
requireNonNull(closure);
199+
format(ProtobufExtension.NAME, ProtobufExtension.class, closure);
200+
}
201+
196202
/** Configures the special YAML-specific extension. */
197203
public void yaml(Action<YamlExtension> closure) {
198204
requireNonNull(closure);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2022-2023 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.gradle.spotless;
17+
18+
import java.io.IOException;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
import com.diffplug.spotless.tag.BufTest;
23+
24+
@BufTest
25+
class BufIntegrationTest extends GradleIntegrationHarness {
26+
@Test
27+
void buf() throws IOException {
28+
setFile("build.gradle").toLines(
29+
"plugins {",
30+
" id 'com.diffplug.spotless'",
31+
"}",
32+
"spotless {",
33+
" protobuf {",
34+
" buf()",
35+
" }",
36+
"}");
37+
setFile("buf.proto").toResource("protobuf/buf/buf.proto");
38+
gradleRunner().withArguments("spotlessApply").build();
39+
assertFile("buf.proto").sameAsResource("protobuf/buf/buf.proto.clean");
40+
}
41+
42+
@Test
43+
void bufWithLicense() throws IOException {
44+
setFile("build.gradle").toLines(
45+
"plugins {",
46+
" id 'com.diffplug.spotless'",
47+
"}",
48+
"spotless {",
49+
" protobuf {",
50+
" buf()",
51+
" licenseHeader '/* (C) 2022 */'",
52+
" }",
53+
"}");
54+
setFile("license.proto").toResource("protobuf/buf/license.proto");
55+
gradleRunner().withArguments("spotlessApply").build();
56+
assertFile("license.proto").sameAsResource("protobuf/buf/license.proto.clean");
57+
}
58+
}

0 commit comments

Comments
 (0)