Skip to content

Commit

Permalink
Implement buf format Gradle step (#1208)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Jul 16, 2023
2 parents 1b24f48 + 0fc9036 commit 78a6dbc
Show file tree
Hide file tree
Showing 16 changed files with 422 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}} |',
Expand Down Expand Up @@ -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: |
Expand Down
3 changes: 2 additions & 1 deletion gradle/special-tests.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ apply plugin: 'com.adarshr.test-logger'
def special = [
'Npm',
'Black',
'Clang'
'Clang',
'Buf'
]

boolean isCiServer = System.getenv().containsKey("CI")
Expand Down
99 changes: 99 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/protobuf/BufStep.java
Original file line number Diff line number Diff line change
@@ -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<String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
1 change: 1 addition & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* Add 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
Expand Down
35 changes: 35 additions & 0 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -517,6 +518,40 @@ black().pathToExe('C:/myuser/.pyenv/versions/3.8.0/scripts/black.exe')
<a name="applying-freshmark-to-markdown-files"></a>
## 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="https://buf.build/">buf</a>. */
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 <a href=https://github.com/bufbuild/buf-gradle-plugin>{@code buf-gradle-plugin}</a>,
* the {@code buf} executable can be resolved from its {@code bufTool} configuration:
*
* <pre>
* {@code
* spotless {
* protobuf {
* buf().pathToExe(configurations.getByName(BUF_BINARY_CONFIGURATION_NAME).getSingleFile().getAbsolutePath())
* }
* }
* }
* </pre>
*
* Be sure to disable the {@code buf-gradle-plugin}'s execution of {@code buf format}:
*
* <pre>
* {@code
* buf {
* enforceFormat = false
* }
* }
* </pre>
*/
public BufFormatExtension pathToExe(String pathToExe) {
step = step.withPathToExe(pathToExe);
replaceStep(createStep());
return this;
}

private FormatterStep createStep() {
return step.create();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ public void json(Action<JsonExtension> closure) {
format(JsonExtension.NAME, JsonExtension.class, closure);
}

/** Configures the special protobuf-specific extension. */
public void protobuf(Action<ProtobufExtension> closure) {
requireNonNull(closure);
format(ProtobufExtension.NAME, ProtobufExtension.class, closure);
}

/** Configures the special YAML-specific extension. */
public void yaml(Action<YamlExtension> closure) {
requireNonNull(closure);
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
Loading

0 comments on commit 78a6dbc

Please sign in to comment.