diff --git a/.gitattributes b/.gitattributes
index e6141d8894..dc4cb6b0bd 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,4 @@
* text eol=lf
+*.bat eol=crlf
*.png binary
*.jar binary
diff --git a/.github/workflows/changelog-print.yml b/.github/workflows/changelog-print.yml
index 330f78cda0..a3009e3a39 100644
--- a/.github/workflows/changelog-print.yml
+++ b/.github/workflows/changelog-print.yml
@@ -15,7 +15,8 @@ jobs:
with:
java-version: 11
distribution: 'temurin'
- cache: 'gradle'
- name: gradle caching
uses: gradle/gradle-build-action@v2
+ with:
+ gradle-home-cache-cleanup: true
- run: ./gradlew changelogPrint
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1f08c201f5..86cf955059 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,7 +26,10 @@ jobs:
with:
distribution: "temurin"
java-version: 11
- cache: gradle
+ - name: gradle caching
+ uses: gradle/gradle-build-action@v2
+ with:
+ gradle-home-cache-cleanup: true
- name: spotlessCheck
run: ./gradlew spotlessCheck --build-cache
- name: assemble testClasses
@@ -60,7 +63,10 @@ jobs:
with:
distribution: "temurin"
java-version: ${{ matrix.jre }}
- cache: gradle
+ - name: gradle caching
+ uses: gradle/gradle-build-action@v2
+ with:
+ gradle-home-cache-cleanup: true
- name: build (maven-only)
if: matrix.kind == 'maven'
run: ./gradlew :plugin-maven:build -x spotlessCheck --build-cache
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 9a1dcdf609..d41dc8fc30 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -42,9 +42,10 @@ jobs:
with:
java-version: 11
distribution: 'temurin'
- cache: 'gradle'
- name: gradle caching
uses: gradle/gradle-build-action@v2
+ with:
+ gradle-home-cache-cleanup: true
- name: publish all
if: "${{ github.event.inputs.to_publish == 'all' }}"
run: |
diff --git a/CHANGES.md b/CHANGES.md
index 0bdcf657b2..e8af5aec03 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -12,11 +12,15 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* `ProcessRunner` has added some convenience methods so it can be used for maven testing. ([#1496](https://github.com/diffplug/spotless/pull/1496))
+* `ProcessRunner` allows to limit captured output to a certain number of bytes. ([#1511](https://github.com/diffplug/spotless/pull/1511))
+* `ProcessRunner` is now capable of handling long-running tasks where waiting for exit is delegated to the caller. ([#1511](https://github.com/diffplug/spotless/pull/1511))
+* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500))
### Fixed
* The default list of type annotations used by `formatAnnotations` has had 8 more annotations from the Checker Framework added [#1494](https://github.com/diffplug/spotless/pull/1494)
### Changes
-#### Removed
-* Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475))
+* Rename `YamlJacksonStep` into `JacksonYamlStep` while normalizing Jackson usage ([#1492](https://github.com/diffplug/spotless/pull/1492))
+* Convert `gson` integration to use a compile-only source set ([#1510](https://github.com/diffplug/spotless/pull/1510)).
+* ** POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475))
* `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019.
* From now on, we will support no more than 2 breaking changes at a time.
diff --git a/README.md b/README.md
index 637781d894..0743b69427 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}}
extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
lib('java.FormatAnnotationsStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('json.gson.GsonStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
+lib('json.JacksonJsonStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('json.JsonSimpleStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
lib('kotlin.KtLintStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
@@ -77,7 +78,7 @@ lib('python.BlackStep') +'{{yes}} | {{no}}
lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
lib('sql.DBeaverSQLFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
-lib('yaml.YamlJacksonStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |',
+lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
'| [(Your FormatterStep here)](CONTRIBUTING.md#how-to-add-a-new-formatterstep) | {{no}} | {{no}} | {{no}} | {{no}} |',
].join('\n');
-->
@@ -109,6 +110,7 @@ lib('yaml.YamlJacksonStep') +'{{no}} | {{yes}}
| [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`java.FormatAnnotationsStep`](lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`json.gson.GsonStep`](lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
+| [`json.JacksonJsonStep`](lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`json.JsonSimpleStep`](lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`kotlin.KtfmtStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
@@ -123,7 +125,7 @@ lib('yaml.YamlJacksonStep') +'{{no}} | {{yes}}
| [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`sql.DBeaverSQLFormatterStep`](lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
| [`wtp.EclipseWtpFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
-| [`yaml.YamlJacksonStep`](lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: |
+| [`yaml.JacksonYamlStep`](lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
| [(Your FormatterStep here)](CONTRIBUTING.md#how-to-add-a-new-formatterstep) | :white_large_square: | :white_large_square: | :white_large_square: | :white_large_square: |
diff --git a/_ext/eclipse-wtp/build.gradle b/_ext/eclipse-wtp/build.gradle
index 92a78578f6..feacfbf4c1 100644
--- a/_ext/eclipse-wtp/build.gradle
+++ b/_ext/eclipse-wtp/build.gradle
@@ -100,7 +100,7 @@ sourceSets {
* All test classes need to run separately since they all instatiate different setups of the
* Eclipse framework.
*/
-test {
+tasks.withType(Test).configureEach {
//Skip default tests, which would run every test case.
exclude '**'
}
diff --git a/_ext/gradle/java-setup.gradle b/_ext/gradle/java-setup.gradle
index 598077d73c..9572fc2955 100644
--- a/_ext/gradle/java-setup.gradle
+++ b/_ext/gradle/java-setup.gradle
@@ -22,4 +22,6 @@ dependencies {
testImplementation project(':testlib')
}
-test { useJUnitPlatform() }
+tasks.withType(Test).configureEach {
+ useJUnitPlatform()
+}
diff --git a/gradle/special-tests.gradle b/gradle/special-tests.gradle
index c435bc7e81..fd5f602243 100644
--- a/gradle/special-tests.gradle
+++ b/gradle/special-tests.gradle
@@ -7,7 +7,7 @@ def special = [
]
boolean isCiServer = System.getenv().containsKey("CI")
-tasks.named('test') {
+tasks.withType(Test).configureEach {
// See com.diffplug.spotless.tag package for available JUnit 5 @Tag annotations
useJUnitPlatform {
excludeTags special as String[]
diff --git a/lib-extra/build.gradle b/lib-extra/build.gradle
index 08bf286d8d..8381d14041 100644
--- a/lib-extra/build.gradle
+++ b/lib-extra/build.gradle
@@ -26,7 +26,7 @@ dependencies {
spotbugs { reportLevel = 'low' } // low|medium|high (low = sensitive to even minor mistakes)
apply from: rootProject.file('gradle/special-tests.gradle')
-tasks.named('test') {
+tasks.withType(Test).configureEach {
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_16)) {
// needed for EclipseCdtFormatterStepTest
jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED'
diff --git a/lib/build.gradle b/lib/build.gradle
index fc2c1c04a0..82595da399 100644
--- a/lib/build.gradle
+++ b/lib/build.gradle
@@ -15,7 +15,8 @@ def NEEDS_GLUE = [
'flexmark',
'diktat',
'scalafmt',
- 'jackson'
+ 'jackson',
+ 'gson'
]
for (glue in NEEDS_GLUE) {
sourceSets.register(glue) {
@@ -41,6 +42,11 @@ versionCompatibility {
}
}
+tasks.named("check").configure {
+ dependsOn(tasks.named("testCompatibilityAdapters"))
+ dependsOn(tasks.named("testCompatibility"))
+}
+
dependencies {
compileOnly 'org.slf4j:slf4j-api:2.0.0'
// zero runtime reqs is a hard requirements for spotless-lib
@@ -56,6 +62,7 @@ dependencies {
palantirJavaFormatCompileOnly 'com.palantir.javaformat:palantir-java-format:1.1.0' // this version needs to stay compilable against Java 8 for CI Job testNpm
// used jackson-based formatters
+ jacksonCompileOnly 'com.fasterxml.jackson.core:jackson-databind:2.14.1'
jacksonCompileOnly 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.14.1'
String VER_KTFMT = '0.42'
@@ -91,12 +98,31 @@ dependencies {
// used for markdown formatting
flexmarkCompileOnly 'com.vladsch.flexmark:flexmark-all:0.62.2'
+
+ gsonCompileOnly 'com.google.code.gson:gson:2.10.1'
}
// we'll hold the core lib to a high standard
spotbugs { reportLevel = 'low' } // low|medium|high (low = sensitive to even minor mistakes)
apply from: rootProject.file('gradle/special-tests.gradle')
+tasks.withType(Test).configureEach {
+ def jdkVersion = JavaVersion.current().majorVersion.toInteger()
+ def args = []
+ if (jdkVersion >= 16) {
+ // https://docs.gradle.org/7.5/userguide/upgrading_version_7.html#removes_implicit_add_opens_for_test_workers
+ args += [
+ "--add-opens=java.base/java.lang=ALL-UNNAMED",
+ "--add-opens=java.base/java.util=ALL-UNNAMED",
+ ]
+ }
+ if (jdkVersion >= 18) {
+ // https://openjdk.org/jeps/411
+ args += "-Djava.security.manager=allow"
+ }
+ jvmArgs(args)
+}
+
jar {
for (glue in NEEDS_GLUE) {
from sourceSets.getByName(glue).output.classesDirs
diff --git a/lib/src/gson/java/com/diffplug/spotless/glue/gson/GsonFormatterFunc.java b/lib/src/gson/java/com/diffplug/spotless/glue/gson/GsonFormatterFunc.java
new file mode 100644
index 0000000000..b19476a1a8
--- /dev/null
+++ b/lib/src/gson/java/com/diffplug/spotless/glue/gson/GsonFormatterFunc.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 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.glue.gson;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Collections;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.stream.JsonWriter;
+
+import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.ThrowingEx;
+import com.diffplug.spotless.json.gson.GsonConfig;
+
+public class GsonFormatterFunc implements FormatterFunc {
+
+ private static final String FAILED_TO_PARSE_ERROR_MESSAGE = "Unable to format JSON";
+
+ private final Gson gson;
+ private final GsonConfig gsonConfig;
+ private final String generatedIndent;
+
+ public GsonFormatterFunc(GsonConfig gsonConfig) {
+ GsonBuilder gsonBuilder = new GsonBuilder().serializeNulls();
+ if (!gsonConfig.isEscapeHtml()) {
+ gsonBuilder = gsonBuilder.disableHtmlEscaping();
+ }
+ this.gson = gsonBuilder.create();
+ this.gsonConfig = gsonConfig;
+ this.generatedIndent = generateIndent(gsonConfig.getIndentSpaces());
+ }
+
+ @Override
+ public String apply(String inputString) {
+ String result;
+ if (inputString.isEmpty()) {
+ result = "";
+ } else {
+ JsonElement jsonElement = gson.fromJson(inputString, JsonElement.class);
+ if (jsonElement == null) {
+ throw new AssertionError(FAILED_TO_PARSE_ERROR_MESSAGE);
+ }
+ if (gsonConfig.isSortByKeys() && jsonElement.isJsonObject()) {
+ jsonElement = sortByKeys(jsonElement.getAsJsonObject());
+ }
+ try (StringWriter stringWriter = new StringWriter()) {
+ JsonWriter jsonWriter = new JsonWriter(stringWriter);
+ jsonWriter.setIndent(this.generatedIndent);
+ gson.toJson(jsonElement, jsonWriter);
+ result = stringWriter + "\n";
+ } catch (IOException ioException) {
+ throw ThrowingEx.asRuntime(ioException);
+ }
+ }
+ return result;
+ }
+
+ private JsonElement sortByKeys(JsonObject jsonObject) {
+ JsonObject result = new JsonObject();
+ jsonObject.keySet().stream().sorted()
+ .forEach(key -> {
+ JsonElement element = jsonObject.get(key);
+ if (element.isJsonObject()) {
+ element = sortByKeys(element.getAsJsonObject());
+ }
+ result.add(key, element);
+ });
+ return result;
+ }
+
+ private String generateIndent(int indentSpaces) {
+ return String.join("", Collections.nCopies(indentSpaces, " "));
+ }
+
+}
diff --git a/lib/src/jackson/java/com/diffplug/spotless/glue/json/AJacksonFormatterFunc.java b/lib/src/jackson/java/com/diffplug/spotless/glue/json/AJacksonFormatterFunc.java
new file mode 100644
index 0000000000..6f363ad1b7
--- /dev/null
+++ b/lib/src/jackson/java/com/diffplug/spotless/glue/json/AJacksonFormatterFunc.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021-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.glue.json;
+
+import java.io.IOException;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.PrettyPrinter;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.json.JacksonConfig;
+
+/**
+ * A {@link FormatterFunc} based on Jackson library
+ */
+// https://github.com/FasterXML/jackson-dataformats-text/issues/372
+public abstract class AJacksonFormatterFunc implements FormatterFunc {
+ private JacksonConfig jacksonConfig;
+
+ public AJacksonFormatterFunc(JacksonConfig jacksonConfig) {
+ this.jacksonConfig = jacksonConfig;
+ }
+
+ @Override
+ public String apply(String input) throws Exception {
+ ObjectMapper objectMapper = makeObjectMapper();
+
+ return format(objectMapper, input);
+ }
+
+ protected String format(ObjectMapper objectMapper, String input) throws IllegalArgumentException, IOException {
+ try {
+ // ObjectNode is not compatible with SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS
+ Map objectNode = objectMapper.readValue(input, Map.class);
+ String output = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectNode);
+
+ return output;
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Unable to format. input='" + input + "'", e);
+ }
+ }
+
+ /**
+ * @return a {@link JsonFactory}. May be overridden to handle alternative formats.
+ * @see jackson-dataformats-text
+ */
+ protected abstract JsonFactory makeJsonFactory();
+
+ protected ObjectMapper makeObjectMapper() {
+ JsonFactory jsonFactory = makeJsonFactory();
+ ObjectMapper objectMapper = new ObjectMapper(jsonFactory);
+
+ objectMapper.setDefaultPrettyPrinter(makePrettyPrinter());
+
+ // Configure the ObjectMapper
+ // https://github.com/FasterXML/jackson-databind#commonly-used-features
+ jacksonConfig.getFeatureToToggle().forEach((rawFeature, toggle) -> {
+ // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection
+ SerializationFeature feature = SerializationFeature.valueOf(rawFeature);
+
+ objectMapper.configure(feature, toggle);
+ });
+
+ return objectMapper;
+ }
+
+ protected PrettyPrinter makePrettyPrinter() {
+ return new DefaultPrettyPrinter();
+ }
+}
diff --git a/lib/src/jackson/java/com/diffplug/spotless/glue/json/JacksonJsonFormatterFunc.java b/lib/src/jackson/java/com/diffplug/spotless/glue/json/JacksonJsonFormatterFunc.java
new file mode 100644
index 0000000000..ae455fbbb4
--- /dev/null
+++ b/lib/src/jackson/java/com/diffplug/spotless/glue/json/JacksonJsonFormatterFunc.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2021-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.glue.json;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonFactoryBuilder;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.util.DefaultIndenter;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
+import com.fasterxml.jackson.core.util.Separators;
+
+import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.json.JacksonJsonConfig;
+
+/**
+ * A {@link FormatterFunc} based on Jackson library
+ */
+// https://github.com/FasterXML/jackson-dataformats-text/issues/372
+public class JacksonJsonFormatterFunc extends AJacksonFormatterFunc {
+ private JacksonJsonConfig jacksonConfig;
+
+ public JacksonJsonFormatterFunc(JacksonJsonConfig jacksonConfig) {
+ super(jacksonConfig);
+ this.jacksonConfig = jacksonConfig;
+ }
+
+ /**
+ * @return a {@link JsonFactory}. May be overridden to handle alternative formats.
+ * @see jackson-dataformats-text
+ */
+ protected JsonFactory makeJsonFactory() {
+ JsonFactory jsonFactory = new JsonFactoryBuilder().build();
+
+ // Configure the ObjectMapper
+ // https://github.com/FasterXML/jackson-databind#commonly-used-features
+ jacksonConfig.getJsonFeatureToToggle().forEach((rawFeature, toggle) -> {
+ // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection
+ JsonGenerator.Feature feature = JsonGenerator.Feature.valueOf(rawFeature);
+
+ jsonFactory.configure(feature, toggle);
+ });
+
+ return jsonFactory;
+ }
+
+ @Override
+ protected DefaultPrettyPrinter makePrettyPrinter() {
+ boolean spaceBeforeSeparator = jacksonConfig.isSpaceBeforeSeparator();
+
+ // DefaultIndenter default constructor relies on 2 whitespaces as default tabulation
+ // By we want to force '\n' as eol given Spotless provides LF-input (whatever the actual File content/current OS)
+ DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter(" ", "\n");
+ DefaultPrettyPrinter printer = new SpotlessJsonPrettyPrinter(spaceBeforeSeparator);
+
+ printer.indentObjectsWith(indenter);
+ printer.indentArraysWith(indenter);
+ return printer;
+ }
+
+ protected static class SpotlessJsonPrettyPrinter extends DefaultPrettyPrinter {
+ private static final long serialVersionUID = 1L;
+ private final boolean spaceBeforeSeparator;
+
+ public SpotlessJsonPrettyPrinter(boolean spaceBeforeSeparator) {
+ this.spaceBeforeSeparator = spaceBeforeSeparator;
+ }
+
+ @Override
+ public DefaultPrettyPrinter createInstance() {
+ return new SpotlessJsonPrettyPrinter(spaceBeforeSeparator);
+ }
+
+ @Override
+ public DefaultPrettyPrinter withSeparators(Separators separators) {
+ this._separators = separators;
+ if (spaceBeforeSeparator) {
+ // This is Jackson default behavior
+ this._objectFieldValueSeparatorWithSpaces = " " + separators.getObjectFieldValueSeparator() + " ";
+ } else {
+ this._objectFieldValueSeparatorWithSpaces = separators.getObjectFieldValueSeparator() + " ";
+ }
+ return this;
+ }
+ }
+}
diff --git a/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/JacksonYamlFormatterFunc.java b/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/JacksonYamlFormatterFunc.java
new file mode 100644
index 0000000000..89304d8d0a
--- /dev/null
+++ b/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/JacksonYamlFormatterFunc.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2021-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.glue.yaml;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.List;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactoryBuilder;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
+
+import com.diffplug.spotless.glue.json.AJacksonFormatterFunc;
+import com.diffplug.spotless.yaml.JacksonYamlConfig;
+
+public class JacksonYamlFormatterFunc extends AJacksonFormatterFunc {
+ final JacksonYamlConfig yamlConfig;
+
+ public JacksonYamlFormatterFunc(JacksonYamlConfig jacksonConfig) {
+ super(jacksonConfig);
+ this.yamlConfig = jacksonConfig;
+
+ if (jacksonConfig == null) {
+ throw new IllegalArgumentException("ARG");
+ }
+ }
+
+ protected JsonFactory makeJsonFactory() {
+ YAMLFactoryBuilder yamlFactoryBuilder = new YAMLFactoryBuilder(new YAMLFactory());
+
+ // Configure the ObjectMapper
+ // https://github.com/FasterXML/jackson-databind#commonly-used-features
+ yamlConfig.getYamlFeatureToToggle().forEach((rawFeature, toggle) -> {
+ // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection
+ YAMLGenerator.Feature feature = YAMLGenerator.Feature.valueOf(rawFeature);
+
+ yamlFactoryBuilder.configure(feature, toggle);
+ });
+
+ return yamlFactoryBuilder.build();
+ }
+
+ @Override
+ protected String format(ObjectMapper objectMapper, String input) throws IllegalArgumentException, IOException {
+ try {
+ // https://stackoverflow.com/questions/25222327/deserialize-pojos-from-multiple-yaml-documents-in-a-single-file-in-jackson
+ // https://github.com/FasterXML/jackson-dataformats-text/issues/66#issuecomment-375328648
+ JsonParser yamlParser = objectMapper.getFactory().createParser(input);
+ List documents = objectMapper.readValues(yamlParser, JsonNode.class).readAll();
+
+ // https://github.com/FasterXML/jackson-dataformats-text/issues/66#issuecomment-554265055
+ // https://github.com/FasterXML/jackson-dataformats-text/issues/66#issuecomment-554265055
+ StringWriter stringWriter = new StringWriter();
+ objectMapper.writer().writeValues(stringWriter).writeAll(documents).close();
+ return stringWriter.toString();
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Unable to format. input='" + input + "'", e);
+ }
+ }
+}
diff --git a/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/YamlJacksonFormatterFunc.java b/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/YamlJacksonFormatterFunc.java
deleted file mode 100644
index 30cf538159..0000000000
--- a/lib/src/jackson/java/com/diffplug/spotless/glue/yaml/YamlJacksonFormatterFunc.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2021-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.glue.yaml;
-
-import java.io.IOException;
-import java.util.List;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
-
-import com.diffplug.spotless.FormatterFunc;
-
-public class YamlJacksonFormatterFunc implements FormatterFunc {
- private List enabledFeatures;
- private List disabledFeatures;
-
- public YamlJacksonFormatterFunc(List enabledFeatures, List disabledFeatures) {
- this.enabledFeatures = enabledFeatures;
- this.disabledFeatures = disabledFeatures;
- }
-
- @Override
- public String apply(String input) throws Exception {
- ObjectMapper objectMapper = makeObjectMapper();
-
- return format(objectMapper, input);
- }
-
- protected ObjectMapper makeObjectMapper() {
- YAMLFactory yamlFactory = new YAMLFactory();
- ObjectMapper objectMapper = new ObjectMapper(yamlFactory);
-
- // Configure the ObjectMapper
- // https://github.com/FasterXML/jackson-databind#commonly-used-features
- for (String rawFeature : enabledFeatures) {
- // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection
- SerializationFeature feature = SerializationFeature.valueOf(rawFeature);
-
- objectMapper.enable(feature);
- }
-
- for (String rawFeature : disabledFeatures) {
- // https://stackoverflow.com/questions/3735927/java-instantiating-an-enum-using-reflection
- SerializationFeature feature = SerializationFeature.valueOf(rawFeature);
-
- objectMapper.disable(feature);
- }
- return objectMapper;
- }
-
- protected String format(ObjectMapper objectMapper, String input) throws IllegalArgumentException, IOException {
- // We may consider adding manually an initial '---' prefix to help management of multiple documents
- // if (!input.trim().startsWith("---")) {
- // input = "---" + "\n" + input;
- // }
-
- try {
- // https://stackoverflow.com/questions/25222327/deserialize-pojos-from-multiple-yaml-documents-in-a-single-file-in-jackson
- // https://github.com/FasterXML/jackson-dataformats-text/issues/66#issuecomment-375328648
- // 2023-01: For now, we get 'Cannot deserialize value of type `com.fasterxml.jackson.databind.node.ObjectNode` from Array value'
- // JsonParser yamlParser = objectMapper.getFactory().createParser(input);
- // List docs = objectMapper.readValues(yamlParser, ObjectNode.class).readAll();
- // return objectMapper.writeValueAsString(docs);
-
- // 2023-01: This returns JSON instead of YAML
- // This will transit with a JsonNode
- // A JsonNode may keep the comments from the input node
- // JsonNode jsonNode = objectMapper.readTree(input);
- //Not 'toPrettyString' as one could require no INDENT_OUTPUT
- // return jsonNode.toPrettyString();
- ObjectNode objectNode = objectMapper.readValue(input, ObjectNode.class);
- return objectMapper.writeValueAsString(objectNode);
- } catch (JsonProcessingException e) {
- throw new AssertionError("Unable to format YAML. input='" + input + "'", e);
- }
- }
-
- // Spotbugs
- private static class ObjectNodeTypeReference extends TypeReference {}
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java
index 41c664cafe..4e48042184 100644
--- a/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java
+++ b/lib/src/main/java/com/diffplug/spotless/ProcessRunner.java
@@ -15,6 +15,8 @@
*/
package com.diffplug.spotless;
+import static java.util.Objects.requireNonNull;
+
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
@@ -29,9 +31,12 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
-import edu.umd.cs.findbugs.annotations.Nullable;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
@@ -47,10 +52,21 @@
public class ProcessRunner implements AutoCloseable {
private final ExecutorService threadStdOut = Executors.newSingleThreadExecutor();
private final ExecutorService threadStdErr = Executors.newSingleThreadExecutor();
- private final ByteArrayOutputStream bufStdOut = new ByteArrayOutputStream();
- private final ByteArrayOutputStream bufStdErr = new ByteArrayOutputStream();
+ private final ByteArrayOutputStream bufStdOut;
+ private final ByteArrayOutputStream bufStdErr;
- public ProcessRunner() {}
+ public ProcessRunner() {
+ this(-1);
+ }
+
+ public static ProcessRunner usingRingBuffersOfCapacity(int limit) {
+ return new ProcessRunner(limit);
+ }
+
+ private ProcessRunner(int limitedBuffers) {
+ this.bufStdOut = limitedBuffers >= 0 ? new RingBufferByteArrayOutputStream(limitedBuffers) : new ByteArrayOutputStream();
+ this.bufStdErr = limitedBuffers >= 0 ? new RingBufferByteArrayOutputStream(limitedBuffers) : new ByteArrayOutputStream();
+ }
/** Executes the given shell command (using {@code cmd} on windows and {@code sh} on unix). */
public Result shell(String cmd) throws IOException, InterruptedException {
@@ -95,6 +111,36 @@ public Result exec(@Nullable byte[] stdin, List args) throws IOException
/** Creates a process with the given arguments, the given byte array is written to stdin immediately. */
public Result exec(@Nullable File cwd, @Nullable Map environment, @Nullable byte[] stdin, List args) throws IOException, InterruptedException {
+ LongRunningProcess process = start(cwd, environment, stdin, args);
+ try {
+ // wait for the process to finish
+ process.waitFor();
+ // collect the output
+ return process.result();
+ } catch (ExecutionException e) {
+ throw ThrowingEx.asRuntime(e);
+ }
+ }
+
+ /**
+ * Creates a process with the given arguments, the given byte array is written to stdin immediately.
+ *
+ * Delegates to {@link #start(File, Map, byte[], boolean, List)} with {@code false} for {@code redirectErrorStream}.
+ */
+ public LongRunningProcess start(@Nullable File cwd, @Nullable Map environment, @Nullable byte[] stdin, List args) throws IOException {
+ return start(cwd, environment, stdin, false, args);
+ }
+
+ /**
+ * Creates a process with the given arguments, the given byte array is written to stdin immediately.
+ *
+ * The process is not waited for, so the caller is responsible for calling {@link LongRunningProcess#waitFor()} (if needed).
+ *
+ * To dispose this {@code ProcessRunner} instance, either call {@link #close()} or {@link LongRunningProcess#close()}. After
+ * {@link #close()} or {@link LongRunningProcess#close()} has been called, this {@code ProcessRunner} instance must not be used anymore.
+ */
+ public LongRunningProcess start(@Nullable File cwd, @Nullable Map environment, @Nullable byte[] stdin, boolean redirectErrorStream, List args) throws IOException {
+ checkState();
ProcessBuilder builder = new ProcessBuilder(args);
if (cwd != null) {
builder.directory(cwd);
@@ -105,20 +151,20 @@ public Result exec(@Nullable File cwd, @Nullable Map environment
if (stdin == null) {
stdin = new byte[0];
}
+ if (redirectErrorStream) {
+ builder.redirectErrorStream(true);
+ }
+
Process process = builder.start();
Future outputFut = threadStdOut.submit(() -> drainToBytes(process.getInputStream(), bufStdOut));
- Future errorFut = threadStdErr.submit(() -> drainToBytes(process.getErrorStream(), bufStdErr));
+ Future errorFut = null;
+ if (!redirectErrorStream) {
+ errorFut = threadStdErr.submit(() -> drainToBytes(process.getErrorStream(), bufStdErr));
+ }
// 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);
- }
+ return new LongRunningProcess(process, args, outputFut, errorFut);
}
private static void drain(InputStream input, OutputStream output) throws IOException {
@@ -141,17 +187,24 @@ public void close() {
threadStdErr.shutdown();
}
+ /** Checks if this {@code ProcessRunner} instance is still usable. */
+ private void checkState() {
+ if (threadStdOut.isShutdown() || threadStdErr.isShutdown()) {
+ throw new IllegalStateException("ProcessRunner has been closed and must not be used anymore.");
+ }
+ }
+
@SuppressFBWarnings({"EI_EXPOSE_REP", "EI_EXPOSE_REP2"})
public static class Result {
private final List args;
private final int exitCode;
private final byte[] stdOut, stdErr;
- public Result(List args, int exitCode, byte[] stdOut, byte[] stdErr) {
+ public Result(@Nonnull List args, int exitCode, @Nonnull byte[] stdOut, @Nullable byte[] stdErr) {
this.args = args;
this.exitCode = exitCode;
this.stdOut = stdOut;
- this.stdErr = stdErr;
+ this.stdErr = (stdErr == null ? new byte[0] : stdErr);
}
public List args() {
@@ -222,8 +275,86 @@ public String toString() {
}
};
perStream.accept(" stdout", stdOut);
- perStream.accept(" stderr", stdErr);
+ if (stdErr.length > 0) {
+ perStream.accept(" stderr", stdErr);
+ }
return builder.toString();
}
}
+
+ /**
+ * A long-running process that can be waited for.
+ */
+ public class LongRunningProcess extends Process implements AutoCloseable {
+
+ private final Process delegate;
+ private final List args;
+ private final Future outputFut;
+ private final Future errorFut;
+
+ public LongRunningProcess(@Nonnull Process delegate, @Nonnull List args, @Nonnull Future outputFut, @Nullable Future errorFut) {
+ this.delegate = requireNonNull(delegate);
+ this.args = args;
+ this.outputFut = outputFut;
+ this.errorFut = errorFut;
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return delegate.getOutputStream();
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return delegate.getInputStream();
+ }
+
+ @Override
+ public InputStream getErrorStream() {
+ return delegate.getErrorStream();
+ }
+
+ @Override
+ public int waitFor() throws InterruptedException {
+ return delegate.waitFor();
+ }
+
+ @Override
+ public boolean waitFor(long timeout, TimeUnit unit) throws InterruptedException {
+ return delegate.waitFor(timeout, unit);
+ }
+
+ @Override
+ public int exitValue() {
+ return delegate.exitValue();
+ }
+
+ @Override
+ public void destroy() {
+ delegate.destroy();
+ }
+
+ @Override
+ public Process destroyForcibly() {
+ return delegate.destroyForcibly();
+ }
+
+ @Override
+ public boolean isAlive() {
+ return delegate.isAlive();
+ }
+
+ public Result result() throws ExecutionException, InterruptedException {
+ int exitCode = waitFor();
+ return new Result(args, exitCode, this.outputFut.get(), (this.errorFut != null ? this.errorFut.get() : null));
+ }
+
+ @Override
+ public void close() {
+ if (isAlive()) {
+ destroy();
+ }
+ ProcessRunner.this.close();
+ }
+ }
}
diff --git a/lib/src/main/java/com/diffplug/spotless/RingBufferByteArrayOutputStream.java b/lib/src/main/java/com/diffplug/spotless/RingBufferByteArrayOutputStream.java
new file mode 100644
index 0000000000..da4fc6aa04
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/RingBufferByteArrayOutputStream.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+class RingBufferByteArrayOutputStream extends ByteArrayOutputStream {
+
+ private final int limit;
+
+ private int zeroIndexPointer = 0;
+
+ private boolean isOverLimit = false;
+
+ public RingBufferByteArrayOutputStream(int limit) {
+ this(limit, 32);
+ }
+
+ public RingBufferByteArrayOutputStream(int limit, int initialCapacity) {
+ super(initialCapacity);
+ if (limit < initialCapacity) {
+ throw new IllegalArgumentException("Limit must be greater than initial capacity. Limit: " + limit + ", initial capacity: " + initialCapacity);
+ }
+ if (limit < 2) {
+ throw new IllegalArgumentException("Limit must be greater than or equal to 2 but is " + limit);
+ }
+ if (limit % 2 != 0) {
+ throw new IllegalArgumentException("Limit must be an even number but is " + limit); // to fit 16 bit unicode chars
+ }
+ this.limit = limit;
+ }
+
+ // ---- writing
+ @Override
+ public synchronized void write(int b) {
+ if (count < limit) {
+ super.write(b);
+ return;
+ }
+ isOverLimit = true;
+ buf[zeroIndexPointer] = (byte) b;
+ zeroIndexPointer = (zeroIndexPointer + 1) % limit;
+ }
+
+ @Override
+ public synchronized void write(byte[] b, int off, int len) {
+ int remaining = limit - count;
+ if (remaining >= len) {
+ super.write(b, off, len);
+ return;
+ }
+ if (remaining > 0) {
+ // write what we can "normally"
+ super.write(b, off, remaining);
+ // rest delegated
+ write(b, off + remaining, len - remaining);
+ return;
+ }
+ // we are over the limit
+ isOverLimit = true;
+ // write till limit is reached
+ int writeTillLimit = Math.min(len, limit - zeroIndexPointer);
+ System.arraycopy(b, off, buf, zeroIndexPointer, writeTillLimit);
+ zeroIndexPointer = (zeroIndexPointer + writeTillLimit) % limit;
+ if (writeTillLimit < len) {
+ // write rest
+ write(b, off + writeTillLimit, len - writeTillLimit);
+ }
+ }
+
+ @Override
+ public synchronized void reset() {
+ super.reset();
+ zeroIndexPointer = 0;
+ isOverLimit = false;
+ }
+
+ // ---- output
+ @Override
+ public synchronized void writeTo(OutputStream out) throws IOException {
+ if (!isOverLimit) {
+ super.writeTo(out);
+ return;
+ }
+ out.write(buf, zeroIndexPointer, limit - zeroIndexPointer);
+ out.write(buf, 0, zeroIndexPointer);
+ }
+
+ @Override
+ public synchronized byte[] toByteArray() {
+ if (!isOverLimit) {
+ return super.toByteArray();
+ }
+ byte[] result = new byte[limit];
+ System.arraycopy(buf, zeroIndexPointer, result, 0, limit - zeroIndexPointer);
+ System.arraycopy(buf, 0, result, limit - zeroIndexPointer, zeroIndexPointer);
+ return result;
+ }
+
+ @SuppressFBWarnings(value = "DM_DEFAULT_ENCODING", justification = "We want to use the default encoding here since this is contract on ByteArrayOutputStream")
+ @Override
+ public synchronized String toString() {
+ if (!isOverLimit) {
+ return super.toString();
+ }
+ return new String(buf, zeroIndexPointer, limit - zeroIndexPointer) + new String(buf, 0, zeroIndexPointer);
+ }
+
+ @Override
+ public synchronized String toString(String charsetName) throws UnsupportedEncodingException {
+ if (!isOverLimit) {
+ return super.toString(charsetName);
+ }
+ return new String(buf, zeroIndexPointer, limit - zeroIndexPointer, charsetName) + new String(buf, 0, zeroIndexPointer, charsetName);
+ }
+
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/JacksonConfig.java b/lib/src/main/java/com/diffplug/spotless/json/JacksonConfig.java
new file mode 100644
index 0000000000..f0dba8f072
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/json/JacksonConfig.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 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.json;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A DTO holding the basic for Jackson-based formatters
+ */
+public class JacksonConfig implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private static final Map DEFAULT_FEATURE_TOGGLES;
+
+ static {
+ Map defaultFeatureToggles = new LinkedHashMap<>();
+ // We activate by default the PrettyPrinter from Jackson
+ // @see com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT
+ defaultFeatureToggles.put("INDENT_OUTPUT", true);
+ DEFAULT_FEATURE_TOGGLES = defaultFeatureToggles;
+ }
+
+ protected Map featureToToggle = new LinkedHashMap<>(DEFAULT_FEATURE_TOGGLES);
+
+ public Map getFeatureToToggle() {
+ return Collections.unmodifiableMap(featureToToggle);
+ }
+
+ public void setFeatureToToggle(Map featureToToggle) {
+ this.featureToToggle = featureToToggle;
+ }
+
+ public void appendFeatureToToggle(Map features) {
+ this.featureToToggle.putAll(features);
+ }
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonConfig.java b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonConfig.java
new file mode 100644
index 0000000000..efff594663
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonConfig.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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.json;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Specialization of {@link JacksonConfig} for JSON documents
+ */
+public class JacksonJsonConfig extends JacksonConfig {
+ private static final long serialVersionUID = 1L;
+
+ protected Map jsonFeatureToToggle = new LinkedHashMap<>();
+
+ // https://github.com/revelc/formatter-maven-plugin/pull/280
+ // By default, Jackson adds a ' ' before separator, which is not standard with most IDE/JSON libraries
+ protected boolean spaceBeforeSeparator = false;
+
+ public Map getJsonFeatureToToggle() {
+ return Collections.unmodifiableMap(jsonFeatureToToggle);
+ }
+
+ /**
+ * Refers to com.fasterxml.jackson.core.JsonGenerator.Feature
+ */
+ public void setJsonFeatureToToggle(Map jsonFeatureToToggle) {
+ this.jsonFeatureToToggle = jsonFeatureToToggle;
+ }
+
+ /**
+ * Refers to com.fasterxml.jackson.core.JsonGenerator.Feature
+ */
+ public void appendJsonFeatureToToggle(Map features) {
+ this.jsonFeatureToToggle.putAll(features);
+ }
+
+ public boolean isSpaceBeforeSeparator() {
+ return spaceBeforeSeparator;
+ }
+
+ public void setSpaceBeforeSeparator(boolean spaceBeforeSeparator) {
+ this.spaceBeforeSeparator = spaceBeforeSeparator;
+ }
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java
new file mode 100644
index 0000000000..f15edbbd42
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/json/JacksonJsonStep.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2021-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.json;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Objects;
+
+import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.JarState;
+import com.diffplug.spotless.Provisioner;
+
+/**
+ * Simple YAML formatter which reformats the file according to Jackson YAMLFactory.
+ */
+// https://stackoverflow.com/questions/14515994/convert-json-string-to-pretty-print-json-output-using-jackson
+public class JacksonJsonStep {
+ static final String MAVEN_COORDINATE = "com.fasterxml.jackson.core:jackson-databind:";
+ // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
+ static final String DEFAULT_VERSION = "2.14.1";
+
+ private JacksonJsonStep() {}
+
+ public static String defaultVersion() {
+ return DEFAULT_VERSION;
+ }
+
+ public static FormatterStep create(JacksonJsonConfig jacksonConfig,
+ String jacksonVersion,
+ Provisioner provisioner) {
+ Objects.requireNonNull(provisioner, "provisioner cannot be null");
+ return FormatterStep.createLazy("json",
+ () -> new State(jacksonConfig, jacksonVersion, provisioner),
+ State::toFormatter);
+ }
+
+ public static FormatterStep create(Provisioner provisioner) {
+ return create(new JacksonJsonConfig(), defaultVersion(), provisioner);
+ }
+
+ private static final class State implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final JacksonConfig jacksonConfig;
+
+ private final JarState jarState;
+
+ private State(JacksonConfig jacksonConfig,
+ String jacksonVersion,
+ Provisioner provisioner) throws IOException {
+ this.jacksonConfig = jacksonConfig;
+
+ this.jarState = JarState.from(JacksonJsonStep.MAVEN_COORDINATE + jacksonVersion, provisioner);
+ }
+
+ FormatterFunc toFormatter() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
+ InstantiationException, IllegalAccessException {
+ Class> formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.json.JacksonJsonFormatterFunc");
+ Constructor> constructor = formatterFunc.getConstructor(JacksonJsonConfig.class);
+ return (FormatterFunc) constructor.newInstance(jacksonConfig);
+ }
+ }
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java b/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java
index a970149133..85ccbfa6e2 100644
--- a/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 DiffPlug
+ * Copyright 2021-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -81,7 +81,7 @@ FormatterFunc toFormatter() {
return format(arrayConstructor, arrayToString, s);
}
- throw new AssertionError(String.format("Unable to determine JSON type, expected a '{' or '[' but found '%s'", first));
+ throw new IllegalArgumentException(String.format("Unable to determine JSON type, expected a '{' or '[' but found '%s'", first));
};
}
@@ -89,8 +89,8 @@ private String format(Constructor> constructor, Method toString, String input)
try {
Object parsed = constructor.newInstance(input);
return toString.invoke(parsed, indentSpaces) + "\n";
- } catch (InvocationTargetException ex) {
- throw new AssertionError("Unable to format JSON", ex.getCause());
+ } catch (InvocationTargetException e) {
+ throw new IllegalArgumentException("Unable to format JSON", e);
}
}
}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonBuilderWrapper.java b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonBuilderWrapper.java
deleted file mode 100644
index c2e56f39b8..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonBuilderWrapper.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2022 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.json.gson;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-
-import com.diffplug.spotless.JarState;
-
-class GsonBuilderWrapper extends GsonWrapperBase {
-
- private final Constructor> constructor;
- private final Method serializeNullsMethod;
- private final Method disableHtmlEscapingMethod;
- private final Method createMethod;
-
- GsonBuilderWrapper(JarState jarState) {
- Class> clazz = loadClass(jarState.getClassLoader(), "com.google.gson.GsonBuilder");
- this.constructor = getConstructor(clazz);
- this.serializeNullsMethod = getMethod(clazz, "serializeNulls");
- this.disableHtmlEscapingMethod = getMethod(clazz, "disableHtmlEscaping");
- this.createMethod = getMethod(clazz, "create");
- }
-
- Object createGsonBuilder() {
- return newInstance(constructor);
- }
-
- Object serializeNulls(Object gsonBuilder) {
- return invoke(serializeNullsMethod, gsonBuilder);
- }
-
- Object disableHtmlEscaping(Object gsonBuilder) {
- return invoke(disableHtmlEscapingMethod, gsonBuilder);
- }
-
- Object create(Object gsonBuilder) {
- return invoke(createMethod, gsonBuilder);
- }
-
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonConfig.java b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonConfig.java
new file mode 100644
index 0000000000..a11c1ee296
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonConfig.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 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.json.gson;
+
+import java.io.Serializable;
+
+public class GsonConfig implements Serializable {
+ private static final long serialVersionUID = 6039715618937332633L;
+
+ private boolean sortByKeys;
+ private boolean escapeHtml;
+ private int indentSpaces;
+ private String version;
+
+ public GsonConfig(boolean sortByKeys, boolean escapeHtml, int indentSpaces, String version) {
+ this.sortByKeys = sortByKeys;
+ this.escapeHtml = escapeHtml;
+ this.indentSpaces = indentSpaces;
+ this.version = version;
+ }
+
+ public boolean isSortByKeys() {
+ return sortByKeys;
+ }
+
+ public void setSortByKeys(boolean sortByKeys) {
+ this.sortByKeys = sortByKeys;
+ }
+
+ public boolean isEscapeHtml() {
+ return escapeHtml;
+ }
+
+ public void setEscapeHtml(boolean escapeHtml) {
+ this.escapeHtml = escapeHtml;
+ }
+
+ public int getIndentSpaces() {
+ return indentSpaces;
+ }
+
+ public void setIndentSpaces(int indentSpaces) {
+ this.indentSpaces = indentSpaces;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java
index 06519ae39d..ec90255b77 100644
--- a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 DiffPlug
+ * 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.
@@ -17,8 +17,8 @@
import java.io.IOException;
import java.io.Serializable;
-import java.io.StringWriter;
-import java.util.Collections;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
import java.util.Objects;
import com.diffplug.spotless.FormatterFunc;
@@ -28,78 +28,38 @@
public class GsonStep {
private static final String MAVEN_COORDINATES = "com.google.code.gson:gson";
+ private static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Gson; maybe you set an incompatible version?";
+ @Deprecated
public static FormatterStep create(int indentSpaces, boolean sortByKeys, boolean escapeHtml, String version, Provisioner provisioner) {
+ return create(new GsonConfig(sortByKeys, escapeHtml, indentSpaces, version), provisioner);
+ }
+
+ public static FormatterStep create(GsonConfig gsonConfig, Provisioner provisioner) {
Objects.requireNonNull(provisioner, "provisioner cannot be null");
- return FormatterStep.createLazy("gson", () -> new State(indentSpaces, sortByKeys, escapeHtml, version, provisioner), State::toFormatter);
+ return FormatterStep.createLazy("gson", () -> new State(gsonConfig, provisioner), State::toFormatter);
}
private static final class State implements Serializable {
- private static final long serialVersionUID = -1493479043249379485L;
+ private static final long serialVersionUID = -3240568265160440420L;
- private final int indentSpaces;
- private final boolean sortByKeys;
- private final boolean escapeHtml;
private final JarState jarState;
+ private final GsonConfig gsonConfig;
- private State(int indentSpaces, boolean sortByKeys, boolean escapeHtml, String version, Provisioner provisioner) throws IOException {
- this.indentSpaces = indentSpaces;
- this.sortByKeys = sortByKeys;
- this.escapeHtml = escapeHtml;
- this.jarState = JarState.from(MAVEN_COORDINATES + ":" + version, provisioner);
+ private State(GsonConfig gsonConfig, Provisioner provisioner) throws IOException {
+ this.gsonConfig = gsonConfig;
+ this.jarState = JarState.from(MAVEN_COORDINATES + ":" + gsonConfig.getVersion(), provisioner);
}
FormatterFunc toFormatter() {
- JsonWriterWrapper jsonWriterWrapper = new JsonWriterWrapper(jarState);
- JsonElementWrapper jsonElementWrapper = new JsonElementWrapper(jarState);
- JsonObjectWrapper jsonObjectWrapper = new JsonObjectWrapper(jarState, jsonElementWrapper);
- GsonBuilderWrapper gsonBuilderWrapper = new GsonBuilderWrapper(jarState);
- GsonWrapper gsonWrapper = new GsonWrapper(jarState, jsonElementWrapper, jsonWriterWrapper);
-
- Object gsonBuilder = gsonBuilderWrapper.serializeNulls(gsonBuilderWrapper.createGsonBuilder());
- if (!escapeHtml) {
- gsonBuilder = gsonBuilderWrapper.disableHtmlEscaping(gsonBuilder);
+ try {
+ Class> formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.gson.GsonFormatterFunc");
+ Constructor> constructor = formatterFunc.getConstructor(GsonConfig.class);
+ return (FormatterFunc) constructor.newInstance(gsonConfig);
+ } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException
+ | InstantiationException | IllegalAccessException | NoClassDefFoundError cause) {
+ throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
}
- Object gson = gsonBuilderWrapper.create(gsonBuilder);
-
- return inputString -> {
- String result;
- if (inputString.isEmpty()) {
- result = "";
- } else {
- Object jsonElement = gsonWrapper.fromJson(gson, inputString, jsonElementWrapper.getWrappedClass());
- if (jsonElement == null) {
- throw new AssertionError(GsonWrapperBase.FAILED_TO_PARSE_ERROR_MESSAGE);
- }
- if (sortByKeys && jsonElementWrapper.isJsonObject(jsonElement)) {
- jsonElement = sortByKeys(jsonObjectWrapper, jsonElementWrapper, jsonElement);
- }
- try (StringWriter stringWriter = new StringWriter()) {
- Object jsonWriter = jsonWriterWrapper.createJsonWriter(stringWriter);
- jsonWriterWrapper.setIndent(jsonWriter, generateIndent(indentSpaces));
- gsonWrapper.toJson(gson, jsonElement, jsonWriter);
- result = stringWriter + "\n";
- }
- }
- return result;
- };
- }
-
- private Object sortByKeys(JsonObjectWrapper jsonObjectWrapper, JsonElementWrapper jsonElementWrapper, Object jsonObject) {
- Object result = jsonObjectWrapper.createJsonObject();
- jsonObjectWrapper.keySet(jsonObject).stream().sorted()
- .forEach(key -> {
- Object element = jsonObjectWrapper.get(jsonObject, key);
- if (jsonElementWrapper.isJsonObject(element)) {
- element = sortByKeys(jsonObjectWrapper, jsonElementWrapper, element);
- }
- jsonObjectWrapper.add(result, key, element);
- });
- return result;
- }
-
- private String generateIndent(int indentSpaces) {
- return String.join("", Collections.nCopies(indentSpaces, " "));
}
}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonWrapper.java b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonWrapper.java
deleted file mode 100644
index eaca499eed..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonWrapper.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2022 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.json.gson;
-
-import java.lang.reflect.Method;
-
-import com.diffplug.spotless.JarState;
-
-class GsonWrapper extends GsonWrapperBase {
-
- private final Method fromJsonMethod;
- private final Method toJsonMethod;
-
- GsonWrapper(JarState jarState, JsonElementWrapper jsonElementWrapper, JsonWriterWrapper jsonWriterWrapper) {
- Class> clazz = loadClass(jarState.getClassLoader(), "com.google.gson.Gson");
- this.fromJsonMethod = getMethod(clazz, "fromJson", String.class, Class.class);
- this.toJsonMethod = getMethod(clazz, "toJson", jsonElementWrapper.getWrappedClass(), jsonWriterWrapper.getWrappedClass());
- }
-
- Object fromJson(Object gson, String json, Class> type) {
- return invoke(fromJsonMethod, gson, json, type);
- }
-
- void toJson(Object gson, Object jsonElement, Object jsonWriter) {
- invoke(toJsonMethod, gson, jsonElement, jsonWriter);
- }
-
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonWrapperBase.java b/lib/src/main/java/com/diffplug/spotless/json/gson/GsonWrapperBase.java
deleted file mode 100644
index 24d12a5722..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/json/gson/GsonWrapperBase.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2022 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.json.gson;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-abstract class GsonWrapperBase {
-
- static final String INCOMPATIBLE_ERROR_MESSAGE = "There was a problem interacting with Gson; maybe you set an incompatible version?";
- static final String FAILED_TO_PARSE_ERROR_MESSAGE = "Unable to format JSON";
-
- protected final Class> loadClass(ClassLoader classLoader, String className) {
- try {
- return classLoader.loadClass(className);
- } catch (ClassNotFoundException cause) {
- throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
- }
- }
-
- protected final Constructor> getConstructor(Class> clazz, Class>... argumentTypes) {
- try {
- return clazz.getConstructor(argumentTypes);
- } catch (NoSuchMethodException cause) {
- throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
- }
- }
-
- protected final Method getMethod(Class> clazz, String name, Class>... argumentTypes) {
- try {
- return clazz.getMethod(name, argumentTypes);
- } catch (NoSuchMethodException cause) {
- throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
- }
- }
-
- protected final T newInstance(Constructor constructor, Object... args) {
- try {
- return constructor.newInstance(args);
- } catch (InstantiationException | IllegalAccessException cause) {
- throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
- } catch (InvocationTargetException cause) {
- throw new AssertionError(FAILED_TO_PARSE_ERROR_MESSAGE, cause.getCause());
- }
- }
-
- protected Object invoke(Method method, Object targetObject, Object... args) {
- try {
- return method.invoke(targetObject, args);
- } catch (IllegalAccessException cause) {
- throw new IllegalStateException(INCOMPATIBLE_ERROR_MESSAGE, cause);
- } catch (InvocationTargetException cause) {
- throw new AssertionError(FAILED_TO_PARSE_ERROR_MESSAGE, cause.getCause());
- }
- }
-
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/JsonElementWrapper.java b/lib/src/main/java/com/diffplug/spotless/json/gson/JsonElementWrapper.java
deleted file mode 100644
index ffdfa649ce..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/json/gson/JsonElementWrapper.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2022 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.json.gson;
-
-import java.lang.reflect.Method;
-
-import com.diffplug.spotless.JarState;
-
-class JsonElementWrapper extends GsonWrapperBase {
-
- private final Class> clazz;
- private final Method isJsonObjectMethod;
-
- JsonElementWrapper(JarState jarState) {
- this.clazz = loadClass(jarState.getClassLoader(), "com.google.gson.JsonElement");
- this.isJsonObjectMethod = getMethod(clazz, "isJsonObject");
- }
-
- boolean isJsonObject(Object jsonElement) {
- return (boolean) invoke(isJsonObjectMethod, jsonElement);
- }
-
- Class> getWrappedClass() {
- return clazz;
- }
-
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/JsonObjectWrapper.java b/lib/src/main/java/com/diffplug/spotless/json/gson/JsonObjectWrapper.java
deleted file mode 100644
index 35ec0d876b..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/json/gson/JsonObjectWrapper.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2022 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.json.gson;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.util.Set;
-
-import com.diffplug.spotless.JarState;
-
-class JsonObjectWrapper extends GsonWrapperBase {
-
- private final Constructor> constructor;
- private final Method keySetMethod;
- private final Method getMethod;
- private final Method addMethod;
-
- JsonObjectWrapper(JarState jarState, JsonElementWrapper jsonElementWrapper) {
- Class> clazz = loadClass(jarState.getClassLoader(), "com.google.gson.JsonObject");
- this.constructor = getConstructor(clazz);
- this.keySetMethod = getMethod(clazz, "keySet");
- this.getMethod = getMethod(clazz, "get", String.class);
- this.addMethod = getMethod(clazz, "add", String.class, jsonElementWrapper.getWrappedClass());
- }
-
- Object createJsonObject() {
- return newInstance(constructor);
- }
-
- @SuppressWarnings("unchecked")
- Set keySet(Object jsonObject) {
- return (Set) invoke(keySetMethod, jsonObject);
- }
-
- Object get(Object jsonObject, String key) {
- return invoke(getMethod, jsonObject, key);
- }
-
- void add(Object jsonObject, String key, Object element) {
- invoke(addMethod, jsonObject, key, element);
- }
-
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/json/gson/JsonWriterWrapper.java b/lib/src/main/java/com/diffplug/spotless/json/gson/JsonWriterWrapper.java
deleted file mode 100644
index c9d682e2c2..0000000000
--- a/lib/src/main/java/com/diffplug/spotless/json/gson/JsonWriterWrapper.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2022 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.json.gson;
-
-import java.io.Writer;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-
-import com.diffplug.spotless.JarState;
-
-class JsonWriterWrapper extends GsonWrapperBase {
-
- private final Class> clazz;
- private final Constructor> constructor;
- private final Method setIndentMethod;
-
- JsonWriterWrapper(JarState jarState) {
- this.clazz = loadClass(jarState.getClassLoader(), "com.google.gson.stream.JsonWriter");
- this.constructor = getConstructor(clazz, Writer.class);
- this.setIndentMethod = getMethod(clazz, "setIndent", String.class);
- }
-
- Object createJsonWriter(Writer writer) {
- return newInstance(constructor, writer);
- }
-
- void setIndent(Object jsonWriter, String indent) {
- invoke(setIndentMethod, jsonWriter, indent);
- }
-
- Class> getWrappedClass() {
- return clazz;
- }
-
-}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java
index 81c5b6ce78..74979d90aa 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/EslintFormatterStep.java
@@ -94,9 +94,11 @@ private static class State extends NpmFormatterStepStateBase implements Serializ
"/com/diffplug/spotless/npm/common-serve.js",
"/com/diffplug/spotless/npm/eslint-serve.js"),
npmPathResolver.resolveNpmrcContent()),
- projectDir,
- buildDir,
- npmPathResolver.resolveNpmExecutable());
+ new NpmFormatterStepLocations(
+ projectDir,
+ buildDir,
+ npmPathResolver.resolveNpmExecutable(),
+ npmPathResolver.resolveNodeExecutable()));
this.eslintConfig = localCopyFiles(requireNonNull(eslintConfig));
}
@@ -119,7 +121,7 @@ public FormatterFunc createFormatterFunc() {
FormattedPrinter.SYSOUT.print("creating formatter function (starting server)");
ServerProcessInfo eslintRestServer = npmRunServer();
EslintRestService restService = new EslintRestService(eslintRestServer.getBaseUrl());
- return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(projectDir, nodeModulesDir, eslintConfig, restService));
+ return Closeable.ofDangerous(() -> endServer(restService, eslintRestServer), new EslintFilePathPassingFormatterFunc(locations.projectDir(), nodeModulesDir, eslintConfig, restService));
} catch (IOException e) {
throw ThrowingEx.asRuntime(e);
}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeExecutableResolver.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeExecutableResolver.java
new file mode 100644
index 0000000000..eb30ae78b8
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NodeExecutableResolver.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 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.npm;
+
+import java.io.File;
+import java.util.Optional;
+
+class NodeExecutableResolver {
+
+ private NodeExecutableResolver() {
+ // no instance
+ }
+
+ static String nodeExecutableName() {
+ String nodeName = "node";
+ if (PlatformInfo.normalizedOS() == PlatformInfo.OS.WINDOWS) {
+ nodeName += ".exe";
+ }
+ return nodeName;
+ }
+
+ static Optional tryFindNextTo(File npmExecutable) {
+ if (npmExecutable == null) {
+ return Optional.empty();
+ }
+ File nodeExecutable = new File(npmExecutable.getParentFile(), nodeExecutableName());
+ if (nodeExecutable.exists() && nodeExecutable.isFile() && nodeExecutable.canExecute()) {
+ return Optional.of(nodeExecutable);
+ }
+ return Optional.empty();
+ }
+
+ public static String explainMessage() {
+ return "Spotless was unable to find a node executable.\n" +
+ "Either specify the node executable explicitly or make sure it can be found next to the npm executable.";
+ }
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java
new file mode 100644
index 0000000000..0c417733e8
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepLocations.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 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.npm;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.File;
+import java.io.Serializable;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+class NpmFormatterStepLocations implements Serializable {
+
+ private static final long serialVersionUID = -1055408537924029969L;
+ @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
+ private final transient File projectDir;
+
+ @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
+ private final transient File buildDir;
+
+ @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
+ private final transient File npmExecutable;
+
+ @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
+ private final transient File nodeExecutable;
+
+ public NpmFormatterStepLocations(File projectDir, File buildDir, File npmExecutable, File nodeExecutable) {
+ this.projectDir = requireNonNull(projectDir);
+ this.buildDir = requireNonNull(buildDir);
+ this.npmExecutable = requireNonNull(npmExecutable);
+ this.nodeExecutable = requireNonNull(nodeExecutable);
+ }
+
+ public File projectDir() {
+ return projectDir;
+ }
+
+ public File buildDir() {
+ return buildDir;
+ }
+
+ public File npmExecutable() {
+ return npmExecutable;
+ }
+
+ public File nodeExecutable() {
+ return nodeExecutable;
+ }
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java
index 49a166e45c..4f555a177e 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java
@@ -48,23 +48,17 @@ abstract class NpmFormatterStepStateBase implements Serializable {
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
public final transient File nodeModulesDir;
- @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
- private final transient File npmExecutable;
-
- @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
- public final transient File projectDir;
+ public final NpmFormatterStepLocations locations;
private final NpmConfig npmConfig;
private final String stepName;
- protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, File projectDir, File buildDir, File npm) throws IOException {
+ protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, NpmFormatterStepLocations locations) throws IOException {
this.stepName = requireNonNull(stepName);
this.npmConfig = requireNonNull(npmConfig);
- this.projectDir = requireNonNull(projectDir);
- this.npmExecutable = npm;
-
- NodeServerLayout layout = prepareNodeServer(buildDir);
+ this.locations = locations;
+ NodeServerLayout layout = prepareNodeServer(locations.buildDir());
this.nodeModulesDir = layout.nodeModulesDir();
this.packageJsonSignature = FileSignature.signAsList(layout.packageJsonFile());
}
@@ -88,7 +82,7 @@ private NodeServerLayout prepareNodeServer(File buildDir) throws IOException {
}
private void runNpmInstall(File npmProjectDir) throws IOException {
- new NpmProcess(npmProjectDir, this.npmExecutable).install();
+ new NpmProcess(npmProjectDir, this.locations.npmExecutable(), this.locations.nodeExecutable()).install();
}
protected ServerProcessInfo npmRunServer() throws ServerStartException, IOException {
@@ -102,7 +96,7 @@ protected ServerProcessInfo npmRunServer() throws ServerStartException, IOExcept
final File serverPortFile = new File(this.nodeModulesDir, "server.port");
NpmResourceHelper.deleteFileIfExists(serverPortFile);
// start the http server in node
- Process server = new NpmProcess(this.nodeModulesDir, this.npmExecutable).start();
+ Process server = new NpmProcess(this.nodeModulesDir, this.locations.npmExecutable(), this.locations.nodeExecutable()).start();
// await the readiness of the http server - wait for at most 60 seconds
try {
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmPathResolver.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmPathResolver.java
index a9a15bd49d..4759d2f914 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/NpmPathResolver.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmPathResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 DiffPlug
+ * Copyright 2020-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,8 @@
package com.diffplug.spotless.npm;
import java.io.File;
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -24,20 +25,58 @@ public class NpmPathResolver {
private final File explicitNpmExecutable;
+ private final File explicitNodeExecutable;
+
private final File explicitNpmrcFile;
private final List additionalNpmrcLocations;
- public NpmPathResolver(File explicitNpmExecutable, File explicitNpmrcFile, File... additionalNpmrcLocations) {
+ public NpmPathResolver(File explicitNpmExecutable, File explicitNodeExecutable, File explicitNpmrcFile, List additionalNpmrcLocations) {
this.explicitNpmExecutable = explicitNpmExecutable;
+ this.explicitNodeExecutable = explicitNodeExecutable;
this.explicitNpmrcFile = explicitNpmrcFile;
- this.additionalNpmrcLocations = Arrays.asList(additionalNpmrcLocations);
+ this.additionalNpmrcLocations = Collections.unmodifiableList(new ArrayList<>(additionalNpmrcLocations));
}
+ /**
+ * Finds the npm executable to use.
+ *
+ * Either the explicit npm executable is returned, or - if an explicit node executable is configured - tries to find
+ * the npm executable relative to the node executable.
+ * Falls back to looking for npm on the user's system using {@link NpmExecutableResolver}
+ *
+ * @return the npm executable to use
+ * @throws IllegalStateException if no npm executable could be found
+ */
public File resolveNpmExecutable() {
- return Optional.ofNullable(this.explicitNpmExecutable)
- .orElseGet(() -> NpmExecutableResolver.tryFind()
- .orElseThrow(() -> new IllegalStateException("Can't automatically determine npm executable and none was specifically supplied!\n\n" + NpmExecutableResolver.explainMessage())));
+ if (this.explicitNpmExecutable != null) {
+ return this.explicitNpmExecutable;
+ }
+ if (this.explicitNodeExecutable != null) {
+ File nodeExecutableCandidate = new File(this.explicitNodeExecutable.getParentFile(), NpmExecutableResolver.npmExecutableName());
+ if (nodeExecutableCandidate.canExecute()) {
+ return nodeExecutableCandidate;
+ }
+ }
+ return NpmExecutableResolver.tryFind()
+ .orElseThrow(() -> new IllegalStateException("Can't automatically determine npm executable and none was specifically supplied!\n\n" + NpmExecutableResolver.explainMessage()));
+ }
+
+ /**
+ * Finds the node executable to use.
+ *
+ * Either the explicit node executable is returned, or tries to find the node executable relative to the npm executable
+ * found by {@link #resolveNpmExecutable()}.
+ * @return the node executable to use
+ * @throws IllegalStateException if no node executable could be found
+ */
+ public File resolveNodeExecutable() {
+ if (this.explicitNodeExecutable != null) {
+ return this.explicitNodeExecutable;
+ }
+ File npmExecutable = resolveNpmExecutable();
+ return NodeExecutableResolver.tryFindNextTo(npmExecutable)
+ .orElseThrow(() -> new IllegalStateException("Can't automatically determine node executable and none was specifically supplied!\n\n" + NodeExecutableResolver.explainMessage()));
}
public String resolveNpmrcContent() {
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java
index 8be94a9ea3..6384900d82 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java
@@ -19,18 +19,30 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
+import com.diffplug.spotless.ProcessRunner;
+import com.diffplug.spotless.ProcessRunner.LongRunningProcess;
+
class NpmProcess {
private final File workingDir;
private final File npmExecutable;
- NpmProcess(File workingDir, File npmExecutable) {
+ private final File nodeExecutable;
+
+ private final ProcessRunner processRunner;
+
+ NpmProcess(File workingDir, File npmExecutable, File nodeExecutable) {
this.workingDir = workingDir;
this.npmExecutable = npmExecutable;
+ this.nodeExecutable = nodeExecutable;
+ processRunner = ProcessRunner.usingRingBuffersOfCapacity(100 * 1024); // 100kB
}
void install() {
@@ -41,31 +53,27 @@ void install() {
"--prefer-offline");
}
- Process start() {
+ LongRunningProcess start() {
// adding --scripts-prepend-node-path=true due to https://github.com/diffplug/spotless/issues/619#issuecomment-648018679
return npm("start", "--scripts-prepend-node-path=true");
}
private void npmAwait(String... args) {
- final Process npmProcess = npm(args);
-
- try {
+ try (LongRunningProcess npmProcess = npm(args)) {
if (npmProcess.waitFor() != 0) {
- throw new NpmProcessException("Running npm command '" + commandLine(args) + "' failed with exit code: " + npmProcess.exitValue());
+ throw new NpmProcessException("Running npm command '" + commandLine(args) + "' failed with exit code: " + npmProcess.exitValue() + "\n\n" + npmProcess.result());
}
} catch (InterruptedException e) {
throw new NpmProcessException("Running npm command '" + commandLine(args) + "' was interrupted.", e);
+ } catch (ExecutionException e) {
+ throw new NpmProcessException("Running npm command '" + commandLine(args) + "' failed.", e);
}
}
- private Process npm(String... args) {
+ private LongRunningProcess npm(String... args) {
List processCommand = processCommand(args);
try {
- return new ProcessBuilder()
- .inheritIO()
- .directory(this.workingDir)
- .command(processCommand)
- .start();
+ return processRunner.start(this.workingDir, environmentVariables(), null, true, processCommand);
} catch (IOException e) {
throw new NpmProcessException("Failed to launch npm command '" + commandLine(args) + "'.", e);
}
@@ -78,6 +86,12 @@ private List processCommand(String... args) {
return command;
}
+ private Map environmentVariables() {
+ Map environmentVariables = new HashMap<>();
+ environmentVariables.put("PATH", this.nodeExecutable.getParentFile().getAbsolutePath() + File.pathSeparator + System.getenv("PATH"));
+ return environmentVariables;
+ }
+
private String commandLine(String... args) {
return "npm " + Arrays.stream(args).collect(Collectors.joining(" "));
}
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 22a162eb0b..22dd4586ee 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java
@@ -74,9 +74,11 @@ private static class State extends NpmFormatterStepStateBase implements Serializ
"/com/diffplug/spotless/npm/common-serve.js",
"/com/diffplug/spotless/npm/prettier-serve.js"),
npmPathResolver.resolveNpmrcContent()),
- projectDir,
- buildDir,
- npmPathResolver.resolveNpmExecutable());
+ new NpmFormatterStepLocations(
+ projectDir,
+ buildDir,
+ npmPathResolver.resolveNpmExecutable(),
+ npmPathResolver.resolveNodeExecutable()));
this.prettierConfig = requireNonNull(prettierConfig);
}
@@ -120,7 +122,14 @@ public String applyWithFile(String unix, File file) throws Exception {
FormattedPrinter.SYSOUT.print("formatting String '" + unix.substring(0, Math.min(50, unix.length())) + "[...]' in file '" + file + "'");
final String prettierConfigOptionsWithFilepath = assertFilepathInConfigOptions(file);
- return restService.format(unix, prettierConfigOptionsWithFilepath);
+ try {
+ return restService.format(unix, prettierConfigOptionsWithFilepath);
+ } catch (SimpleRestClient.SimpleRestResponseException e) {
+ if (e.getStatusCode() != 200 && e.getResponseMessage().contains("No parser could be inferred")) {
+ throw new PrettierMissingParserException(file, e);
+ }
+ throw e;
+ }
}
private String assertFilepathInConfigOptions(File file) {
@@ -139,4 +148,5 @@ private String assertFilepathInConfigOptions(File file) {
return "{" + filePathOption + (hasAnyConfigOption ? "," : "") + prettierConfigOptions.substring(startOfConfigOption + 1);
}
}
+
}
diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierMissingParserException.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierMissingParserException.java
new file mode 100644
index 0000000000..6956545135
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierMissingParserException.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 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.npm;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+class PrettierMissingParserException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ private static final Map EXTENSIONS_TO_PLUGINS;
+
+ static {
+ Map plugins = new HashMap<>();
+ // ---- official plugins
+ plugins.put(".php", "@prettier/plugin-php");
+ plugins.put(".pug", "@prettier/plugin-pug");
+ plugins.put(".rb", "@prettier/plugin-ruby");
+ plugins.put(".xml", "@prettier/plugin-xml");
+
+ // ---- community plugins
+ // default namings: astro, elm, java, jsonata, prisma, properties, sh, sql, svelte, toml
+ plugins.put(".trigger", "prettier-plugin-apex");
+ plugins.put(".cls", "prettier-plugin-apex");
+ plugins.put(".html.erb", "prettier-plugin-erb");
+ Arrays.asList(".glsl",
+ ".fp",
+ ".frag",
+ ".frg",
+ ".fs",
+ ".fsh",
+ ".fshader",
+ ".geo",
+ ".geom",
+ ".glslf",
+ ".glslv",
+ ".gs",
+ ".gshader",
+ ".rchit",
+ ".rmiss",
+ ".shader",
+ ".tesc",
+ ".tese",
+ ".vert",
+ ".vrx",
+ ".vsh",
+ ".vshader").forEach(ext -> plugins.put(ext, "prettier-plugin-glsl"));
+ Arrays.asList(".go.html",
+ ".gohtml",
+ ".gotmpl",
+ ".go.tmpl",
+ ".tmpl",
+ ".tpl",
+ ".html.tmpl",
+ ".html.tpl").forEach(ext -> plugins.put(ext, "prettier-plugin-go-template"));
+ plugins.put(".kt", "kotlin");
+ plugins.put(".mo", "motoko");
+ Arrays.asList(".nginx", ".nginxconf").forEach(ext -> plugins.put(ext, "prettier-plugin-nginx"));
+ plugins.put(".sol", "prettier-plugin-solidity");
+
+ EXTENSIONS_TO_PLUGINS = Collections.unmodifiableMap(plugins);
+ }
+
+ private final File file;
+
+ public PrettierMissingParserException(@Nonnull File file, Exception cause) {
+ super("Prettier could not infer a parser for file '" + file + "'. Maybe you need to include a prettier plugin in devDependencies?\n\n" + recommendPlugin(file), cause);
+ this.file = Objects.requireNonNull(file);
+ }
+
+ private static String recommendPlugin(File file) {
+ String pluginName = guessPlugin(file);
+ return "A good candidate for file '" + file + "' is '" + pluginName + "\n"
+ + "See if you can find it on \n"
+ + "or search on npmjs.com for a plugin matching that name: "
+ + String.format("", pluginName)
+ + "\n\n"
+ + "For instructions on how to include plugins for prettier in spotless see our documentation:\n"
+ + "- for gradle \n"
+ + "- for maven ";
+ }
+
+ private static String guessPlugin(File file) {
+ return EXTENSIONS_TO_PLUGINS.entrySet().stream()
+ .filter(entry -> file.getName().endsWith(entry.getKey()))
+ .findFirst()
+ .map(entry -> entry.getValue())
+ .orElse("prettier-plugin-" + extension(file));
+ }
+
+ public String fileType() {
+ return extension(file);
+ }
+
+ private static String extension(File file) {
+ return file.getName().substring(file.getName().lastIndexOf('.') + 1);
+ }
+}
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 98d694ec33..14d6b1bbd0 100644
--- a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java
@@ -80,9 +80,11 @@ public State(String stepName, Map versions, File projectDir, Fil
"/com/diffplug/spotless/npm/common-serve.js",
"/com/diffplug/spotless/npm/tsfmt-serve.js"),
npmPathResolver.resolveNpmrcContent()),
- projectDir,
- buildDir,
- npmPathResolver.resolveNpmExecutable());
+ new NpmFormatterStepLocations(
+ projectDir,
+ buildDir,
+ npmPathResolver.resolveNpmExecutable(),
+ npmPathResolver.resolveNodeExecutable()));
this.buildDir = requireNonNull(buildDir);
this.configFile = configFile;
this.inlineTsFmtSettings = inlineTsFmtSettings == null ? new TreeMap<>() : new TreeMap<>(inlineTsFmtSettings);
diff --git a/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlConfig.java b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlConfig.java
new file mode 100644
index 0000000000..166ee5f307
--- /dev/null
+++ b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlConfig.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 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.yaml;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.diffplug.spotless.json.JacksonConfig;
+
+/**
+ * Specialization of {@link JacksonConfig} for YAML documents
+ */
+public class JacksonYamlConfig extends JacksonConfig {
+ private static final long serialVersionUID = 1L;
+
+ protected Map yamlFeatureToToggle = new LinkedHashMap<>();
+
+ public Map getYamlFeatureToToggle() {
+ return Collections.unmodifiableMap(yamlFeatureToToggle);
+ }
+
+ /**
+ * Refers to com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature
+ */
+ public void setYamlFeatureToToggle(Map yamlFeatureToToggle) {
+ this.yamlFeatureToToggle = yamlFeatureToToggle;
+ }
+
+ /**
+ * Refers to com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature
+ */
+ public void appendYamlFeatureToToggle(Map features) {
+ this.yamlFeatureToToggle.putAll(features);
+ }
+
+}
diff --git a/lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonStep.java b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java
similarity index 70%
rename from lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonStep.java
rename to lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java
index db2525ab97..a87e40f420 100644
--- a/lib/src/main/java/com/diffplug/spotless/yaml/YamlJacksonStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/yaml/JacksonYamlStep.java
@@ -19,8 +19,6 @@
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
-import java.util.Arrays;
-import java.util.List;
import java.util.Objects;
import com.diffplug.spotless.FormatterFunc;
@@ -32,54 +30,52 @@
* Simple YAML formatter which reformats the file according to Jackson YAMLFactory.
*/
// https://stackoverflow.com/questions/14515994/convert-json-string-to-pretty-print-json-output-using-jackson
-public class YamlJacksonStep {
+// https://stackoverflow.com/questions/60891174/i-want-to-load-a-yaml-file-possibly-edit-the-data-and-then-dump-it-again-how
+public class JacksonYamlStep {
static final String MAVEN_COORDINATE = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:";
// https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml
static final String DEFAULT_VERSION = "2.14.1";
- private YamlJacksonStep() {}
+ private JacksonYamlStep() {}
public static String defaultVersion() {
return DEFAULT_VERSION;
}
- public static FormatterStep create(List enabledFeatures,
- List disabledFeatures,
+ public static FormatterStep create(JacksonYamlConfig jacksonConfig,
String jacksonVersion,
Provisioner provisioner) {
+ Objects.requireNonNull(jacksonConfig, "jacksonConfig cannot be null");
Objects.requireNonNull(provisioner, "provisioner cannot be null");
return FormatterStep.createLazy("yaml",
- () -> new State(enabledFeatures, disabledFeatures, jacksonVersion, provisioner),
+ () -> new State(jacksonConfig, jacksonVersion, provisioner),
State::toFormatter);
}
public static FormatterStep create(Provisioner provisioner) {
- return create(Arrays.asList("INDENT_OUTPUT"), Arrays.asList(), defaultVersion(), provisioner);
+ return create(new JacksonYamlConfig(), defaultVersion(), provisioner);
}
private static final class State implements Serializable {
private static final long serialVersionUID = 1L;
- private final List enabledFeatures;
- private final List disabledFeatures;
+ private final JacksonYamlConfig jacksonConfig;
private final JarState jarState;
- private State(List enabledFeatures,
- List disabledFeatures,
+ private State(JacksonYamlConfig jacksonConfig,
String jacksonVersion,
Provisioner provisioner) throws IOException {
- this.enabledFeatures = enabledFeatures;
- this.disabledFeatures = disabledFeatures;
+ this.jacksonConfig = jacksonConfig;
- this.jarState = JarState.from(YamlJacksonStep.MAVEN_COORDINATE + jacksonVersion, provisioner);
+ this.jarState = JarState.from(JacksonYamlStep.MAVEN_COORDINATE + jacksonVersion, provisioner);
}
FormatterFunc toFormatter() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
InstantiationException, IllegalAccessException {
- Class> formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.yaml.YamlJacksonFormatterFunc");
- Constructor> constructor = formatterFunc.getConstructor(List.class, List.class);
- return (FormatterFunc) constructor.newInstance(enabledFeatures, disabledFeatures);
+ Class> formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.yaml.JacksonYamlFormatterFunc");
+ Constructor> constructor = formatterFunc.getConstructor(JacksonYamlConfig.class);
+ return (FormatterFunc) constructor.newInstance(jacksonConfig);
}
}
}
diff --git a/lib/src/test/java/com/diffplug/spotless/RingBufferByteArrayOutputStreamTest.java b/lib/src/test/java/com/diffplug/spotless/RingBufferByteArrayOutputStreamTest.java
new file mode 100644
index 0000000000..94fa49dbc1
--- /dev/null
+++ b/lib/src/test/java/com/diffplug/spotless/RingBufferByteArrayOutputStreamTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 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;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.stream.Stream;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class RingBufferByteArrayOutputStreamTest {
+
+ private final byte[] bytes = new byte[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
+
+ @ParameterizedTest(name = "{index} writeStrategy: {0}")
+ @MethodSource("writeStrategies")
+ void toStringBehavesNormallyWithinLimit(String name, ByteWriteStrategy writeStrategy) {
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(12, 1);
+ writeStrategy.write(stream, bytes);
+ Assertions.assertThat(stream.toString()).isEqualTo("0123456789");
+ }
+
+ @ParameterizedTest(name = "{index} writeStrategy: {0}")
+ @MethodSource("writeStrategies")
+ void toStringBehavesOverwritingOverLimit(String name, ByteWriteStrategy writeStrategy) {
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(4, 1);
+ writeStrategy.write(stream, bytes);
+ Assertions.assertThat(stream.toString()).hasSize(4);
+ Assertions.assertThat(stream.toString()).isEqualTo("6789");
+ }
+
+ @ParameterizedTest(name = "{index} writeStrategy: {0}")
+ @MethodSource("writeStrategies")
+ void toStringBehavesNormallyAtExactlyLimit(String name, ByteWriteStrategy writeStrategy) {
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(bytes.length, 1);
+ writeStrategy.write(stream, bytes);
+ Assertions.assertThat(stream.toString()).isEqualTo("0123456789");
+ }
+
+ @ParameterizedTest(name = "{index} writeStrategy: {0}")
+ @MethodSource("writeStrategies")
+ void toByteArrayBehavesNormallyWithinLimit(String name, ByteWriteStrategy writeStrategy) {
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(12, 1);
+ writeStrategy.write(stream, bytes);
+ Assertions.assertThat(stream.toByteArray()).isEqualTo(bytes);
+ }
+
+ @ParameterizedTest(name = "{index} writeStrategy: {0}")
+ @MethodSource("writeStrategies")
+ void toByteArrayBehavesOverwritingOverLimit(String name, ByteWriteStrategy writeStrategy) {
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(4, 1);
+ writeStrategy.write(stream, bytes);
+ Assertions.assertThat(stream.toByteArray()).hasSize(4);
+ Assertions.assertThat(stream.toByteArray()).isEqualTo(new byte[]{'6', '7', '8', '9'});
+ }
+
+ @ParameterizedTest(name = "{index} writeStrategy: {0}")
+ @MethodSource("writeStrategies")
+ void toByteArrayBehavesOverwritingAtExactlyLimit(String name, ByteWriteStrategy writeStrategy) {
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(bytes.length, 1);
+ writeStrategy.write(stream, bytes);
+ Assertions.assertThat(stream.toByteArray()).isEqualTo(bytes);
+ }
+
+ @ParameterizedTest(name = "{index} writeStrategy: {0}")
+ @MethodSource("writeStrategies")
+ void writeToBehavesNormallyWithinLimit(String name, ByteWriteStrategy writeStrategy) throws IOException {
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(12, 1);
+ writeStrategy.write(stream, bytes);
+ ByteArrayOutputStream target = new ByteArrayOutputStream();
+ stream.writeTo(target);
+ Assertions.assertThat(target.toByteArray()).isEqualTo(bytes);
+ }
+
+ @ParameterizedTest(name = "{index} writeStrategy: {0}")
+ @MethodSource("writeStrategies")
+ void writeToBehavesOverwritingOverLimit(String name, ByteWriteStrategy writeStrategy) throws IOException {
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(4, 1);
+ writeStrategy.write(stream, bytes);
+ ByteArrayOutputStream target = new ByteArrayOutputStream();
+ stream.writeTo(target);
+ Assertions.assertThat(target.toByteArray()).hasSize(4);
+ Assertions.assertThat(target.toByteArray()).isEqualTo(new byte[]{'6', '7', '8', '9'});
+ }
+
+ @ParameterizedTest(name = "{index} writeStrategy: {0}")
+ @MethodSource("writeStrategies")
+ void writeToBehavesNormallyAtExactlyLimit(String name, ByteWriteStrategy writeStrategy) throws IOException {
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(bytes.length, 1);
+ writeStrategy.write(stream, bytes);
+ ByteArrayOutputStream target = new ByteArrayOutputStream();
+ stream.writeTo(target);
+ Assertions.assertThat(target.toByteArray()).isEqualTo(bytes);
+ }
+
+ @Test
+ void writeToBehavesCorrectlyWhenOverLimitMultipleCalls() {
+ // this test explicitly captures a border case where the buffer is not empty but can exactly fit what we are writing
+ RingBufferByteArrayOutputStream stream = new RingBufferByteArrayOutputStream(2, 1);
+ stream.write('0');
+ stream.write(new byte[]{'1', '2'}, 0, 2);
+ Assertions.assertThat(stream.toString()).hasSize(2);
+ Assertions.assertThat(stream.toString()).isEqualTo("12");
+ }
+
+ private static Stream writeStrategies() {
+ return Stream.of(
+ Arguments.of("writeAllAtOnce", allAtOnce()),
+ Arguments.of("writeOneByteAtATime", oneByteAtATime()),
+ Arguments.of("writeTwoBytesAtATime", twoBytesAtATime()),
+ Arguments.of("writeOneAndThenTwoBytesAtATime", oneAndThenTwoBytesAtATime()),
+ Arguments.of("firstFourBytesAndThenTheRest", firstFourBytesAndThenTheRest()));
+ }
+
+ private static ByteWriteStrategy allAtOnce() {
+ return (stream, bytes) -> stream.write(bytes, 0, bytes.length);
+ }
+
+ private static ByteWriteStrategy oneByteAtATime() {
+ return (stream, bytes) -> {
+ for (byte b : bytes) {
+ stream.write(b);
+ }
+ };
+ }
+
+ private static ByteWriteStrategy twoBytesAtATime() {
+ return (stream, bytes) -> {
+ for (int i = 0; i < bytes.length; i += 2) {
+ stream.write(bytes, i, 2);
+ }
+ };
+ }
+
+ private static ByteWriteStrategy oneAndThenTwoBytesAtATime() {
+ return (stream, bytes) -> {
+ int written = 0;
+ for (int i = 0; i + 3 < bytes.length; i += 3) {
+ stream.write(bytes, i, 1);
+ stream.write(bytes, i + 1, 2);
+ written += 3;
+ }
+ if (written < bytes.length) {
+ stream.write(bytes, written, bytes.length - written);
+ }
+
+ };
+ }
+
+ private static ByteWriteStrategy firstFourBytesAndThenTheRest() {
+ return (stream, bytes) -> {
+ stream.write(bytes, 0, 4);
+ stream.write(bytes, 4, bytes.length - 4);
+ };
+ }
+
+ @FunctionalInterface
+ private interface ByteWriteStrategy {
+ void write(RingBufferByteArrayOutputStream stream, byte[] bytes);
+ }
+
+}
diff --git a/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java b/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java
index 6825b3d818..8804f86b08 100644
--- a/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java
+++ b/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java
@@ -22,6 +22,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
@@ -33,12 +34,13 @@ public class KtLintCompat0Dot48Dot0AdapterTest {
public void testDefaults(@TempDir Path path) throws IOException {
KtLintCompat0Dot48Dot0Adapter ktLintCompat0Dot48Dot0Adapter = new KtLintCompat0Dot48Dot0Adapter();
String text = loadAndWriteText(path, "empty_class_body.kt");
+ final Path filePath = Paths.get(path.toString(), "empty_class_body.kt");
Map userData = new HashMap<>();
Map editorConfigOverrideMap = new HashMap<>();
- String formatted = ktLintCompat0Dot48Dot0Adapter.format(text, path, false, false, null, userData, editorConfigOverrideMap);
+ String formatted = ktLintCompat0Dot48Dot0Adapter.format(text, filePath, false, false, null, userData, editorConfigOverrideMap);
assertEquals("class empty_class_body\n", formatted);
}
@@ -46,6 +48,7 @@ public void testDefaults(@TempDir Path path) throws IOException {
public void testEditorConfigCanDisable(@TempDir Path path) throws IOException {
KtLintCompat0Dot48Dot0Adapter ktLintCompat0Dot48Dot0Adapter = new KtLintCompat0Dot48Dot0Adapter();
String text = loadAndWriteText(path, "fails_no_semicolons.kt");
+ final Path filePath = Paths.get(path.toString(), "fails_no_semicolons.kt");
Map userData = new HashMap<>();
@@ -53,7 +56,7 @@ public void testEditorConfigCanDisable(@TempDir Path path) throws IOException {
editorConfigOverrideMap.put("indent_style", "tab");
editorConfigOverrideMap.put("ktlint_standard_no-semi", "disabled");
- String formatted = ktLintCompat0Dot48Dot0Adapter.format(text, path, false, false, null, userData, editorConfigOverrideMap);
+ String formatted = ktLintCompat0Dot48Dot0Adapter.format(text, filePath, false, false, null, userData, editorConfigOverrideMap);
assertEquals("class fails_no_semicolons {\n\tval i = 0;\n}\n", formatted);
}
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index bc4f2bd5e5..17bf123b41 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -4,11 +4,14 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
+* Support `jackson()` for YAML files ([#1492](https://github.com/diffplug/spotless/pull/1492))
+* Support `jackson()` for JSON files ([#1492](https://github.com/diffplug/spotless/pull/1492))
+* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500))
+* Prettier will now suggest to install plugins if a parser cannot be inferred from the file extension ([#1511](https://github.com/diffplug/spotless/pull/1511))
### Fixed
* The default list of type annotations used by `formatAnnotations` has had 8 more annotations from the Checker Framework added [#1494](https://github.com/diffplug/spotless/pull/1494)
### Changes
-#### Removed
-* Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475))
+* **POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475))
* `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019.
* From now on, we will support no more than 2 breaking changes at a time.
diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md
index f2567a5924..b6e95807d9 100644
--- a/plugin-gradle/README.md
+++ b/plugin-gradle/README.md
@@ -737,11 +737,12 @@ For details, see the [npm detection](#npm-detection) and [`.npmrc` detection](#n
```gradle
spotless {
json {
- target 'src/**/*.json' // you have to set the target manually
- simple() // has its own section below
+ target 'src/**/*.json' // you have to set the target manually
+ simple() // has its own section below
prettier().config(['parser': 'json']) // see Prettier section below
- eclipseWtp('json') // see Eclipse web tools platform section
- gson() // has its own section below
+ eclipseWtp('json') // see Eclipse web tools platform section
+ gson() // has its own section below
+ jackson() // has its own section below
}
}
```
@@ -780,10 +781,60 @@ spotless {
Notes:
* There's no option in Gson to leave HTML as-is (i.e. escaped HTML would remain escaped, raw would remain raw). Either
-all HTML characters are written escaped or none. Set `escapeHtml` if you prefer the former.
+ all HTML characters are written escaped or none. Set `escapeHtml` if you prefer the former.
* `sortByKeys` will apply lexicographic order on the keys of the input JSON. See the
-[javadoc of String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#compareTo(java.lang.String))
-for details.
+ [javadoc of String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#compareTo(java.lang.String))
+ for details.
+
+### Jackson
+
+Uses Jackson for json files.
+
+```gradle
+spotless {
+ json {
+ target 'src/**/*.json'
+ jackson()
+ .spaceBeforeSeparator(false) // optional: add a whitespace before key separator. False by default
+ .feature('INDENT_OUTPUT', true) // optional: true by default
+ .feature('ORDER_MAP_ENTRIES_BY_KEYS', true) // optional: false by default
+ .feature('ANY_OTHER_FEATURE', true|false) // optional: any SerializationFeature can be toggled on or off
+ .jsonFeature('ANY_OTHER_FEATURE', true|false) // any JsonGenerator.Feature can be toggled on or off
+ }
+}
+```
+
+
+## YAML
+
+- `com.diffplug.gradle.spotless.YamlExtension` [javadoc](https://javadoc.io/doc/com.diffplug.spotless/spotless-plugin-gradle/6.13.0/com/diffplug/gradle/spotless/JsonExtension.html), [code](https://github.com/diffplug/spotless/blob/main/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/YamlExtension.java)
+
+```gradle
+spotless {
+ yaml {
+ target 'src/**/*.yaml' // you have to set the target manually
+ jackson() // has its own section below
+ }
+}
+```
+
+### Jackson
+
+Uses Jackson for `yaml` files.
+
+```gradle
+spotless {
+ yaml {
+ target 'src/**/*.yaml'
+ jackson()
+ .spaceBeforeSeparator(false) // optional: add a whitespace before key separator. False by default
+ .feature('INDENT_OUTPUT', true) // optional: true by default
+ .feature('ORDER_MAP_ENTRIES_BY_KEYS', true) // optional: false by default
+ .feature('ANY_OTHER_FEATURE', true|false) // optional: any SerializationFeature can be toggled on or off
+ .yamlFeature('ANY_OTHER_FEATURE', true|false) // any YAMLGenerator.Feature can be toggled on or off
+ }
+}
+```
@@ -844,18 +895,23 @@ spotless {
### npm detection
Prettier is based on NodeJS, so a working NodeJS installation (especially npm) is required on the host running spotless.
-Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm binary to use.
+Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm
+and/or node binary to use.
```gradle
spotless {
format 'javascript', {
- prettier().npmExecutable('/usr/bin/npm').config(...)
+ prettier().npmExecutable('/usr/bin/npm').nodeExecutable('/usr/bin/node').config(...)
```
+If you provide both `npmExecutable` and `nodeExecutable`, spotless will use these paths. If you specify only one of the
+two, spotless will assume the other one is in the same directory.
+
### `.npmrc` detection
Spotless picks up npm configuration stored in a `.npmrc` file either in the project directory or in your user home.
-Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with `npmExecutable`, of course.)
+Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with
+`npmExecutable` and `nodeExecutable`, of course.)
```gradle
spotless {
diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle
index 11258db0e6..3e44335276 100644
--- a/plugin-gradle/build.gradle
+++ b/plugin-gradle/build.gradle
@@ -28,7 +28,7 @@ dependencies {
}
apply from: rootProject.file('gradle/special-tests.gradle')
-tasks.named('test') {
+tasks.withType(Test).configureEach {
testLogging.showStandardStreams = true
}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AJacksonGradleConfig.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AJacksonGradleConfig.java
new file mode 100644
index 0000000000..c017d41d04
--- /dev/null
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/AJacksonGradleConfig.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 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.util.Collections;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.json.JacksonConfig;
+import com.diffplug.spotless.json.JacksonJsonStep;
+
+public abstract class AJacksonGradleConfig {
+ protected final FormatExtension formatExtension;
+
+ protected JacksonConfig jacksonConfig;
+
+ protected String version = JacksonJsonStep.defaultVersion();
+
+ // Make sure to call 'formatExtension.addStep(createStep());' in the extented constructors
+ public AJacksonGradleConfig(JacksonConfig jacksonConfig, FormatExtension formatExtension) {
+ this.formatExtension = formatExtension;
+
+ this.jacksonConfig = jacksonConfig;
+ }
+
+ public AJacksonGradleConfig feature(String feature, boolean toggle) {
+ this.jacksonConfig.appendFeatureToToggle(Collections.singletonMap(feature, toggle));
+ formatExtension.replaceStep(createStep());
+ return this;
+ }
+
+ public AJacksonGradleConfig version(String version) {
+ this.version = version;
+ formatExtension.replaceStep(createStep());
+ return this;
+ }
+
+ protected abstract FormatterStep createStep();
+}
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 4e33c45ed0..8012102200 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
@@ -23,6 +23,7 @@
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@@ -524,6 +525,9 @@ public abstract static class NpmStepConfig> {
@Nullable
protected Object npmFile;
+ @Nullable
+ protected Object nodeFile;
+
@Nullable
protected Object npmrcFile;
@@ -543,6 +547,13 @@ public T npmExecutable(final Object npmFile) {
return (T) this;
}
+ @SuppressWarnings("unchecked")
+ public T nodeExecutable(final Object nodeFile) {
+ this.nodeFile = nodeFile;
+ replaceStep();
+ return (T) this;
+ }
+
public T npmrc(final Object npmrcFile) {
this.npmrcFile = npmrcFile;
replaceStep();
@@ -553,6 +564,10 @@ File npmFileOrNull() {
return fileOrNull(npmFile);
}
+ File nodeFileOrNull() {
+ return fileOrNull(nodeFile);
+ }
+
File npmrcFileOrNull() {
return fileOrNull(npmrcFile);
}
@@ -604,7 +619,7 @@ protected FormatterStep createStep() {
provisioner(),
project.getProjectDir(),
project.getBuildDir(),
- new NpmPathResolver(npmFileOrNull(), npmrcFileOrNull(), project.getProjectDir(), project.getRootDir()),
+ new NpmPathResolver(npmFileOrNull(), nodeFileOrNull(), npmrcFileOrNull(), Arrays.asList(project.getProjectDir(), project.getRootDir())),
new com.diffplug.spotless.npm.PrettierConfig(
this.prettierConfigFile != null ? project.file(this.prettierConfigFile) : null,
this.prettierConfig));
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java
index dd476b6a69..e829c2b53a 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java
@@ -17,6 +17,7 @@
import static java.util.Objects.requireNonNull;
+import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -107,7 +108,7 @@ public FormatterStep createStep() {
provisioner(),
project.getProjectDir(),
project.getBuildDir(),
- new NpmPathResolver(npmFileOrNull(), npmrcFileOrNull(), project.getProjectDir(), project.getRootDir()),
+ new NpmPathResolver(npmFileOrNull(), nodeFileOrNull(), npmrcFileOrNull(), Arrays.asList(project.getProjectDir(), project.getRootDir())),
eslintConfig());
}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java
index 25182fa4bc..39b158ce1e 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2022 DiffPlug
+ * Copyright 2016-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,15 +15,19 @@
*/
package com.diffplug.gradle.spotless;
+import java.util.Collections;
+
import javax.inject.Inject;
import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.json.JacksonJsonConfig;
+import com.diffplug.spotless.json.JacksonJsonStep;
import com.diffplug.spotless.json.JsonSimpleStep;
import com.diffplug.spotless.json.gson.GsonStep;
public class JsonExtension extends FormatExtension {
private static final int DEFAULT_INDENTATION = 4;
- private static final String DEFAULT_GSON_VERSION = "2.8.9";
+ private static final String DEFAULT_GSON_VERSION = "2.10.1";
static final String NAME = "json";
@Inject
@@ -47,6 +51,10 @@ public GsonConfig gson() {
return new GsonConfig();
}
+ public JacksonJsonGradleConfig jackson() {
+ return new JacksonJsonGradleConfig(this);
+ }
+
public class SimpleConfig {
private int indent;
@@ -104,8 +112,37 @@ public GsonConfig version(String version) {
}
private FormatterStep createStep() {
- return GsonStep.create(indentSpaces, sortByKeys, escapeHtml, version, provisioner());
+ return GsonStep.create(new com.diffplug.spotless.json.gson.GsonConfig(sortByKeys, escapeHtml, indentSpaces, version), provisioner());
}
}
+ public static class JacksonJsonGradleConfig extends AJacksonGradleConfig {
+ protected JacksonJsonConfig jacksonConfig;
+
+ public JacksonJsonGradleConfig(JacksonJsonConfig jacksonConfig, FormatExtension formatExtension) {
+ super(jacksonConfig, formatExtension);
+ this.jacksonConfig = jacksonConfig;
+
+ formatExtension.addStep(createStep());
+ }
+
+ public JacksonJsonGradleConfig(FormatExtension formatExtension) {
+ this(new JacksonJsonConfig(), formatExtension);
+ }
+
+ /**
+ * Refers to com.fasterxml.jackson.core.JsonGenerator.Feature
+ */
+ public AJacksonGradleConfig jsonFeature(String feature, boolean toggle) {
+ this.jacksonConfig.appendJsonFeatureToToggle(Collections.singletonMap(feature, toggle));
+ formatExtension.replaceStep(createStep());
+ return this;
+ }
+
+ // 'final' as it is called in the constructor
+ @Override
+ protected final FormatterStep createStep() {
+ return JacksonJsonStep.create(jacksonConfig, version, formatExtension.provisioner());
+ }
+ }
}
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 1c65fad310..0a31a15764 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 YAML-specific extension. */
+ public void yaml(Action closure) {
+ requireNonNull(closure);
+ format(YamlExtension.NAME, YamlExtension.class, closure);
+ }
+
/** Configures a custom extension. */
public void format(String name, Action closure) {
requireNonNull(name, "name");
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java
index 0824caa769..2283afccff 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java
@@ -17,6 +17,7 @@
import static java.util.Objects.requireNonNull;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
@@ -116,7 +117,7 @@ public FormatterStep createStep() {
provisioner(),
project.getProjectDir(),
project.getBuildDir(),
- new NpmPathResolver(npmFileOrNull(), npmrcFileOrNull(), project.getProjectDir(), project.getRootDir()),
+ new NpmPathResolver(npmFileOrNull(), nodeFileOrNull(), npmrcFileOrNull(), Arrays.asList(project.getProjectDir(), project.getRootDir())),
typedConfigFile(),
config);
}
@@ -212,7 +213,7 @@ public FormatterStep createStep() {
provisioner(),
project.getProjectDir(),
project.getBuildDir(),
- new NpmPathResolver(npmFileOrNull(), npmrcFileOrNull(), project.getProjectDir(), project.getRootDir()),
+ new NpmPathResolver(npmFileOrNull(), nodeFileOrNull(), npmrcFileOrNull(), Arrays.asList(project.getProjectDir(), project.getRootDir())),
eslintConfig());
}
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/YamlExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/YamlExtension.java
new file mode 100644
index 0000000000..abc2dce359
--- /dev/null
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/YamlExtension.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016-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.util.Collections;
+
+import javax.inject.Inject;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.yaml.JacksonYamlConfig;
+import com.diffplug.spotless.yaml.JacksonYamlStep;
+
+public class YamlExtension extends FormatExtension {
+ static final String NAME = "yaml";
+
+ @Inject
+ public YamlExtension(SpotlessExtension spotless) {
+ super(spotless);
+ }
+
+ @Override
+ protected void setupTask(SpotlessTask task) {
+ if (target == null) {
+ throw noDefaultTargetException();
+ }
+ super.setupTask(task);
+ }
+
+ public AJacksonGradleConfig jackson() {
+ return new JacksonYamlGradleConfig(this);
+ }
+
+ public class JacksonYamlGradleConfig extends AJacksonGradleConfig {
+ protected JacksonYamlConfig jacksonConfig;
+
+ public JacksonYamlGradleConfig(JacksonYamlConfig jacksonConfig, FormatExtension formatExtension) {
+ super(jacksonConfig, formatExtension);
+
+ this.jacksonConfig = jacksonConfig;
+
+ formatExtension.addStep(createStep());
+ }
+
+ public JacksonYamlGradleConfig(FormatExtension formatExtension) {
+ this(new JacksonYamlConfig(), formatExtension);
+ }
+
+ /**
+ * Refers to com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature
+ */
+ public AJacksonGradleConfig yamlFeature(String feature, boolean toggle) {
+ this.jacksonConfig.appendYamlFeatureToToggle(Collections.singletonMap(feature, toggle));
+ formatExtension.replaceStep(createStep());
+ return this;
+ }
+
+ // 'final' as it is called in the constructor
+ @Override
+ protected final FormatterStep createStep() {
+ return JacksonYamlStep.create(jacksonConfig, version, provisioner());
+ }
+ }
+}
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java
index 9e2f490429..1763fd088f 100644
--- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/GradleIntegrationHarness.java
@@ -21,6 +21,7 @@
import java.io.IOException;
import java.util.List;
import java.util.ListIterator;
+import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -117,11 +118,34 @@ protected GradleRunner gradleRunner() throws IOException {
}
/** Dumps the complete file contents of the folder to the console. */
- protected String getContents() throws IOException {
+ protected String getContents() {
return getContents(subPath -> !subPath.startsWith(".gradle"));
}
- protected String getContents(Predicate subpathsToInclude) throws IOException {
+ protected String getContents(Predicate subpathsToInclude) {
+ return StringPrinter.buildString(printer -> Errors.rethrow().run(() -> iterateFiles(subpathsToInclude, (subpath, file) -> {
+ printer.println("### " + subpath + " ###");
+ try {
+ printer.println(read(subpath));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ })));
+ }
+
+ /** Dumps the filtered file listing of the folder to the console. */
+ protected String listFiles(Predicate subpathsToInclude) {
+ return StringPrinter.buildString(printer -> iterateFiles(subpathsToInclude, (subPath, file) -> {
+ printer.println(subPath + " [" + getFileAttributes(file) + "]");
+ }));
+ }
+
+ /** Dumps the file listing of the folder to the console. */
+ protected String listFiles() {
+ return listFiles(subPath -> !subPath.startsWith(".gradle"));
+ }
+
+ protected void iterateFiles(Predicate subpathsToInclude, BiConsumer consumer) {
TreeDef treeDef = TreeDef.forFile(Errors.rethrow());
List files = TreeStream.depthFirst(treeDef, rootFolder())
.filter(File::isFile)
@@ -129,16 +153,17 @@ protected String getContents(Predicate subpathsToInclude) throws IOExcep
ListIterator iterator = files.listIterator(files.size());
int rootLength = rootFolder().getAbsolutePath().length() + 1;
- return StringPrinter.buildString(printer -> Errors.rethrow().run(() -> {
- while (iterator.hasPrevious()) {
- File file = iterator.previous();
- String subPath = file.getAbsolutePath().substring(rootLength);
- if (subpathsToInclude.test(subPath)) {
- printer.println("### " + subPath + " ###");
- printer.println(read(subPath));
- }
+ while (iterator.hasPrevious()) {
+ File file = iterator.previous();
+ String subPath = file.getAbsolutePath().substring(rootLength);
+ if (subpathsToInclude.test(subPath)) {
+ consumer.accept(subPath, file);
}
- }));
+ }
+ }
+
+ protected String getFileAttributes(File file) {
+ return (file.canRead() ? "r" : "-") + (file.canWrite() ? "w" : "-") + (file.canExecute() ? "x" : "-");
}
protected void checkRunsThenUpToDate() throws IOException {
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java
index e2fb9f70aa..a878615eaa 100644
--- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/JsonExtensionTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2022 DiffPlug
+ * Copyright 2021-2023 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,9 +30,9 @@ void simpleDefaultFormatting() throws IOException {
"repositories { mavenCentral() }",
"spotless {",
" json {",
- " target 'examples/**/*.json'",
- " simple()",
- "}",
+ " target 'examples/**/*.json'",
+ " simple()",
+ " }",
"}");
setFile("src/main/resources/example.json").toResource("json/nestedObjectBefore.json");
setFile("examples/main/resources/example.json").toResource("json/nestedObjectBefore.json");
@@ -51,9 +51,9 @@ void simpleFormattingWithCustomNumberOfSpaces() throws IOException {
"repositories { mavenCentral() }",
"spotless {",
" json {",
- " target 'src/**/*.json'",
- " simple().indentWithSpaces(6)",
- "}",
+ " target 'src/**/*.json'",
+ " simple().indentWithSpaces(6)",
+ " }",
"}");
setFile("src/main/resources/example.json").toResource("json/singletonArrayBefore.json");
gradleRunner().withArguments("spotlessApply").build();
@@ -70,9 +70,9 @@ void gsonDefaultFormatting() throws IOException {
"repositories { mavenCentral() }",
"spotless {",
" json {",
- " target 'examples/**/*.json'",
- " gson()",
- "}",
+ " target 'examples/**/*.json'",
+ " gson()",
+ " }",
"}");
setFile("src/main/resources/example.json").toResource("json/nestedObjectBefore.json");
setFile("examples/main/resources/example.json").toResource("json/nestedObjectBefore.json");
@@ -91,9 +91,9 @@ void gsonFormattingWithCustomNumberOfSpaces() throws IOException {
"repositories { mavenCentral() }",
"spotless {",
" json {",
- " target 'src/**/*.json'",
- " gson().indentWithSpaces(6)",
- "}",
+ " target 'src/**/*.json'",
+ " gson().indentWithSpaces(6)",
+ " }",
"}");
setFile("src/main/resources/example.json").toResource("json/singletonArrayBefore.json");
gradleRunner().withArguments("spotlessApply").build();
@@ -110,9 +110,9 @@ void gsonFormattingWithSortingByKeys() throws IOException {
"repositories { mavenCentral() }",
"spotless {",
" json {",
- " target 'src/**/*.json'",
- " gson().sortByKeys()",
- "}",
+ " target 'src/**/*.json'",
+ " gson().sortByKeys()",
+ " }",
"}");
setFile("src/main/resources/example.json").toResource("json/sortByKeysBefore.json");
gradleRunner().withArguments("spotlessApply").build();
@@ -129,13 +129,31 @@ void gsonFormattingWithHtmlEscape() throws IOException {
"repositories { mavenCentral() }",
"spotless {",
" json {",
- " target 'src/**/*.json'",
- " gson().escapeHtml()",
- "}",
+ " target 'src/**/*.json'",
+ " gson().escapeHtml()",
+ " }",
"}");
setFile("src/main/resources/example.json").toResource("json/escapeHtmlGsonBefore.json");
gradleRunner().withArguments("spotlessApply").build();
assertFile("src/main/resources/example.json").sameAsResource("json/escapeHtmlGsonAfter.json");
}
+ @Test
+ void jacksonFormattingWithSortingByKeys() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'java'",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " json {",
+ " target 'src/**/*.json'",
+ " jackson().feature('ORDER_MAP_ENTRIES_BY_KEYS', true)",
+ " }",
+ "}");
+ setFile("src/main/resources/example.json").toResource("json/sortByKeysBefore.json");
+ gradleRunner().withArguments("spotlessApply").build();
+ assertFile("src/main/resources/example.json").sameAsResource("json/sortByKeysAfter_Jackson.json");
+ }
}
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java
new file mode 100644
index 0000000000..916d853920
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/NpmTestsWithoutNpmInstallationTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2016-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 org.assertj.core.api.Assertions;
+import org.gradle.testkit.runner.BuildResult;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.common.base.Predicates;
+
+class NpmTestsWithoutNpmInstallationTest extends GradleIntegrationHarness {
+
+ @Test
+ void useNodeAndNpmFromNodeGradlePlugin() throws Exception {
+ try {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ " id 'com.github.node-gradle.node' version '3.5.1'",
+ "}",
+ "repositories { mavenCentral() }",
+ "node {",
+ " download = true",
+ " version = '18.13.0'",
+ " npmVersion = '8.19.2'",
+ " workDir = file(\"${buildDir}/nodejs\")",
+ " npmWorkDir = file(\"${buildDir}/npm\")",
+ "}",
+ "def prettierConfig = [:]",
+ "prettierConfig['printWidth'] = 50",
+ "prettierConfig['parser'] = 'typescript'",
+ "def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm'",
+ "def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node'",
+ "spotless {",
+ " format 'mytypescript', {",
+ " target 'test.ts'",
+ " prettier()",
+ " .npmExecutable(\"${tasks.named('npmSetup').get().npmDir.get()}${npmExec}\")",
+ " .nodeExecutable(\"${tasks.named('nodeSetup').get().nodeDir.get()}${nodeExec}\")",
+ " .config(prettierConfig)",
+ " }",
+ "}");
+ setFile("test.ts").toResource("npm/prettier/config/typescript.dirty");
+ // make sure node binary is there
+ gradleRunner().withArguments("nodeSetup", "npmSetup").build();
+ // then run spotless using that node installation
+ final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean");
+ } catch (Exception e) {
+ printContents();
+ throw e;
+ }
+ }
+
+ private void printContents() {
+ System.out.println("************* Folder contents **************************");
+ System.out.println(listFiles(Predicates.and(path -> !path.startsWith(".gradle"), path -> !path.contains("/node_modules/"), path -> !path.contains("/include/"))));
+ System.out.println("********************************************************");
+ }
+
+ @Test
+ @Disabled("This test is disabled because we currently don't support using npm/node installed by the" +
+ "node-gradle-plugin as the installation takes place in the *execution* phase, but spotless needs it in the *configuration* phase.")
+ void useNodeAndNpmFromNodeGradlePluginInOneSweep() throws Exception {
+ try {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ " id 'com.github.node-gradle.node' version '3.5.1'",
+ "}",
+ "repositories { mavenCentral() }",
+ "node {",
+ " download = true",
+ " version = '18.13.0'",
+ " npmVersion = '8.19.2'",
+ " workDir = file(\"${buildDir}/nodejs\")",
+ " npmWorkDir = file(\"${buildDir}/npm\")",
+ "}",
+ "def prettierConfig = [:]",
+ "prettierConfig['printWidth'] = 50",
+ "prettierConfig['parser'] = 'typescript'",
+ "def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm'",
+ "def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node'",
+ "spotless {",
+ " format 'mytypescript', {",
+ " target 'test.ts'",
+ " prettier()",
+ " .npmExecutable(\"${tasks.named('npmSetup').get().npmDir.get()}${npmExec}\")",
+ " .nodeExecutable(\"${tasks.named('nodeSetup').get().nodeDir.get()}${nodeExec}\")",
+ " .config(prettierConfig)",
+ " }",
+ "}",
+ "tasks.named('spotlessMytypescript').configure {",
+ " it.dependsOn('nodeSetup', 'npmSetup')",
+ "}");
+ setFile("test.ts").toResource("npm/prettier/config/typescript.dirty");
+ final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean");
+ } catch (Exception e) {
+ printContents();
+ throw e;
+ }
+ }
+
+ @Test
+ void useNpmFromNodeGradlePlugin() throws Exception {
+ try {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ " id 'com.github.node-gradle.node' version '3.5.1'",
+ "}",
+ "repositories { mavenCentral() }",
+ "node {",
+ " download = true",
+ " version = '18.13.0'",
+ " workDir = file(\"${buildDir}/nodejs\")",
+ "}",
+ "def prettierConfig = [:]",
+ "prettierConfig['printWidth'] = 50",
+ "prettierConfig['parser'] = 'typescript'",
+ "def npmExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/npm.cmd' : '/bin/npm'",
+ "spotless {",
+ " format 'mytypescript', {",
+ " target 'test.ts'",
+ " prettier()",
+ " .npmExecutable(\"${tasks.named('npmSetup').get().npmDir.get()}${npmExec}\")",
+ " .config(prettierConfig)",
+ " }",
+ "}");
+ setFile("test.ts").toResource("npm/prettier/config/typescript.dirty");
+ // make sure node binary is there
+ gradleRunner().withArguments("nodeSetup", "npmSetup").build();
+ // then run spotless using that node installation
+ final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean");
+ } catch (Exception e) {
+ printContents();
+ throw e;
+ }
+ }
+
+ @Test
+ void useNpmNextToConfiguredNodePluginFromNodeGradlePlugin() throws Exception {
+ try {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ " id 'com.github.node-gradle.node' version '3.5.1'",
+ "}",
+ "repositories { mavenCentral() }",
+ "node {",
+ " download = true",
+ " version = '18.13.0'",
+ " workDir = file(\"${buildDir}/nodejs\")",
+ "}",
+ "def prettierConfig = [:]",
+ "prettierConfig['printWidth'] = 50",
+ "prettierConfig['parser'] = 'typescript'",
+ "def nodeExec = System.getProperty('os.name').toLowerCase().contains('windows') ? '/node.exe' : '/bin/node'",
+ "spotless {",
+ " format 'mytypescript', {",
+ " target 'test.ts'",
+ " prettier()",
+ " .nodeExecutable(\"${tasks.named('nodeSetup').get().nodeDir.get()}${nodeExec}\")",
+ " .config(prettierConfig)",
+ " }",
+ "}");
+ setFile("test.ts").toResource("npm/prettier/config/typescript.dirty");
+ // make sure node binary is there
+ gradleRunner().withArguments("nodeSetup", "npmSetup").build();
+ // then run spotless using that node installation
+ final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build();
+ Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL");
+ assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean");
+ } catch (Exception e) {
+ printContents();
+ throw e;
+ }
+ }
+}
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java
index bc8ad1148e..4c458e3ca7 100644
--- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java
@@ -135,6 +135,29 @@ void useJavaCommunityPlugin() throws IOException {
assertFile("JavaTest.java").sameAsResource("npm/prettier/plugins/java-test.clean");
}
+ @Test
+ void suggestsMissingJavaCommunityPlugin() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "def prettierConfig = [:]",
+ "prettierConfig['tabWidth'] = 4",
+ "def prettierPackages = [:]",
+ "prettierPackages['prettier'] = '2.0.5'",
+ "spotless {",
+ " format 'java', {",
+ " target 'JavaTest.java'",
+ " prettier(prettierPackages).config(prettierConfig)",
+ " }",
+ "}");
+ setFile("JavaTest.java").toResource("npm/prettier/plugins/java-test.dirty");
+ final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").buildAndFail();
+ Assertions.assertThat(spotlessApply.getOutput()).contains("could not infer a parser");
+ Assertions.assertThat(spotlessApply.getOutput()).contains("prettier-plugin-java");
+ }
+
@Test
void usePhpCommunityPlugin() throws IOException {
setFile("build.gradle").toLines(
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/YamlExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/YamlExtensionTest.java
new file mode 100644
index 0000000000..067c809846
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/YamlExtensionTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2021-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;
+
+class YamlExtensionTest extends GradleIntegrationHarness {
+ @Test
+ void testFormatYaml_WithJackson_defaultConfig_separatorComments() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'java'",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " yaml {",
+ " target 'src/**/*.yaml'",
+ " jackson()",
+ " }",
+ "}");
+ setFile("src/main/resources/example.yaml").toResource("yaml/separator_comments.yaml");
+ gradleRunner().withArguments("spotlessApply").build();
+ assertFile("src/main/resources/example.yaml").sameAsResource("yaml/separator_comments.clean.yaml");
+ }
+
+ // see YAMLGenerator.Feature.WRITE_DOC_START_MARKER
+ @Test
+ void testFormatYaml_WithJackson_skipDocStartMarker() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'java'",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " yaml {",
+ " target 'src/**/*.yaml'",
+ " jackson()",
+ " .yamlFeature('WRITE_DOC_START_MARKER', false)",
+ " .yamlFeature('MINIMIZE_QUOTES', true)",
+ " }",
+ "}");
+ setFile("src/main/resources/example.yaml").toResource("yaml/array_with_bracket.yaml");
+ gradleRunner().withArguments("spotlessApply", "--stacktrace").build();
+ assertFile("src/main/resources/example.yaml").sameAsResource("yaml/array_with_bracket.clean.no_start_marker.no_quotes.yaml");
+ }
+
+ @Test
+ void testFormatYaml_WithJackson_multipleDocuments() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'java'",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " yaml {",
+ " target 'src/**/*.yaml'",
+ " jackson()",
+ " }",
+ "}");
+ setFile("src/main/resources/example.yaml").toResource("yaml/multiple_documents.yaml");
+ gradleRunner().withArguments("spotlessApply", "--stacktrace").build();
+ assertFile("src/main/resources/example.yaml").sameAsResource("yaml/multiple_documents.clean.jackson.yaml");
+ }
+
+ @Test
+ void testFormatYaml_WithJackson_arrayAtRoot() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'java'",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "spotless {",
+ " yaml {",
+ " target 'src/**/*.yaml'",
+ " jackson()",
+ " }",
+ "}");
+ setFile("src/main/resources/example.yaml").toResource("yaml/array_at_root.yaml");
+ gradleRunner().withArguments("spotlessApply", "--stacktrace").build();
+ assertFile("src/main/resources/example.yaml").sameAsResource("yaml/array_at_root.clean.yaml");
+ }
+
+}
diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md
index ce07394b98..448011be3c 100644
--- a/plugin-maven/CHANGES.md
+++ b/plugin-maven/CHANGES.md
@@ -4,12 +4,17 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
+* Introduce `` ([#1492](https://github.com/diffplug/spotless/pull/1492))
+ * **POTENTIALLY BREAKING** `JacksonYaml` is now configured with a `Map` to configure features
+* Jackson (`json` and `yaml`) has new `spaceBeforeSeparator` option
+ * **POTENTIALLY BREAKING** `spaceBeforeSeparator` is defaulted to false while the formatter was behaving with `true`
+* Allow to specify node executable for node-based formatters using `nodeExecutable` parameter ([#1500](https://github.com/diffplug/spotless/pull/1500))
### Fixed
* The default list of type annotations used by `formatAnnotations` has had 8 more annotations from the Checker Framework added [#1494](https://github.com/diffplug/spotless/pull/1494)
### Changes
* Spotless' custom build was replaced by [`maven-plugin-development`](https://github.com/britter/maven-plugin-development). ([#1496](https://github.com/diffplug/spotless/pull/1496) fixes [#554](https://github.com/diffplug/spotless/issues/554))
-#### Removed
-* Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475))
+* Prettier will now suggest to install plugins if a parser cannot be inferred from the file extension ([#1511](https://github.com/diffplug/spotless/pull/1511))
+* **POTENTIALLY BREAKING** Removed support for KtLint 0.3x and 0.45.2 ([#1475](https://github.com/diffplug/spotless/pull/1475))
* `KtLint` does not maintain a stable API - before this PR, we supported every breaking change in the API since 2019.
* From now on, we will support no more than 2 breaking changes at a time.
diff --git a/plugin-maven/README.md b/plugin-maven/README.md
index 2a455dfe07..f0fc2bb818 100644
--- a/plugin-maven/README.md
+++ b/plugin-maven/README.md
@@ -864,6 +864,7 @@ For details, see the [npm detection](#npm-detection) and [`.npmrc` detection](#n
+
```
@@ -898,6 +899,26 @@ all HTML characters are written escaped or none. Set `escapeHtml` if you prefer
[javadoc of String](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/String.html#compareTo(java.lang.String))
for details.
+### Jackson
+
+Uses Jackson for formatting.
+
+```xml
+
+ 2.14.1
+
+ true
+ false
+ true|false
+
+
+ false
+ true|false
+
+ false
+
+```
+
@@ -924,12 +945,17 @@ Uses Jackson and YAMLFactory to pretty print objects:
```xml
2.14.1
-
- INDENT_OUTPUT
-
-
- DEFAULT_HAS_NO_DISABLED_FEATURE
-
+
+ true
+ false
+ true|false
+
+
+ true
+ false
+ true|false
+
+ false
```
@@ -1050,17 +1076,23 @@ Since spotless uses the actual npm prettier package behind the scenes, it is pos
### npm detection
Prettier is based on NodeJS, so to use it, a working NodeJS installation (especially npm) is required on the host running spotless.
-Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm binary to use.
+Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm
+and/or node binary to use.
```xml
/usr/bin/npm
+ /usr/bin/node
```
+If you provide both `npmExecutable` and `nodeExecutable`, spotless will use these paths. If you specify only one of the
+two, spotless will assume the other one is in the same directory.
+
### `.npmrc` detection
Spotless picks up npm configuration stored in a `.npmrc` file either in the project directory or in your user home.
-Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with `npmExecutable`, of course.)
+Alternatively you can supply spotless with a location of the `.npmrc` file to use. (This can be combined with
+`npmExecutable` and `nodeExecutable`, of course.)
```xml
diff --git a/plugin-maven/build.gradle b/plugin-maven/build.gradle
index ad51318d93..664101a0fd 100644
--- a/plugin-maven/build.gradle
+++ b/plugin-maven/build.gradle
@@ -1,6 +1,6 @@
plugins {
// https://www.benediktritter.de/maven-plugin-development/#release-history
- id 'de.benediktritter.maven-plugin-development' version '0.4.0'
+ id 'de.benediktritter.maven-plugin-development' version '0.4.1'
}
repositories { mavenCentral() }
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Gson.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Gson.java
index 7962ecb1f7..fb5a38e890 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Gson.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Gson.java
@@ -18,12 +18,13 @@
import org.apache.maven.plugins.annotations.Parameter;
import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.json.gson.GsonConfig;
import com.diffplug.spotless.json.gson.GsonStep;
import com.diffplug.spotless.maven.FormatterStepConfig;
import com.diffplug.spotless.maven.FormatterStepFactory;
public class Gson implements FormatterStepFactory {
- private static final String DEFAULT_GSON_VERSION = "2.8.9";
+ private static final String DEFAULT_GSON_VERSION = "2.10.1";
@Parameter
int indentSpaces = Json.DEFAULT_INDENTATION;
@@ -40,6 +41,6 @@ public class Gson implements FormatterStepFactory {
@Override
public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) {
int indentSpaces = this.indentSpaces;
- return GsonStep.create(indentSpaces, sortByKeys, escapeHtml, version, stepConfig.getProvisioner());
+ return GsonStep.create(new GsonConfig(sortByKeys, escapeHtml, indentSpaces, version), stepConfig.getProvisioner());
}
}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JacksonJson.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JacksonJson.java
new file mode 100644
index 0000000000..ecf925ddbe
--- /dev/null
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/JacksonJson.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 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.maven.json;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.maven.plugins.annotations.Parameter;
+
+import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.json.JacksonJsonConfig;
+import com.diffplug.spotless.json.JacksonJsonStep;
+import com.diffplug.spotless.maven.FormatterFactory;
+import com.diffplug.spotless.maven.FormatterStepConfig;
+import com.diffplug.spotless.maven.FormatterStepFactory;
+
+/**
+ * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element.
+ */
+public class JacksonJson implements FormatterStepFactory {
+
+ @Parameter
+ private String version = JacksonJsonStep.defaultVersion();
+
+ @Parameter
+ private boolean spaceBeforeSeparator = new JacksonJsonConfig().isSpaceBeforeSeparator();
+
+ @Parameter
+ private Map features = Collections.emptyMap();
+
+ @Parameter
+ private Map jsonFeatures = Collections.emptyMap();
+
+ @Override
+ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) {
+ JacksonJsonConfig jacksonConfig = new JacksonJsonConfig();
+
+ jacksonConfig.appendFeatureToToggle(features);
+ jacksonConfig.appendJsonFeatureToToggle(jsonFeatures);
+ jacksonConfig.setSpaceBeforeSeparator(spaceBeforeSeparator);
+
+ return JacksonJsonStep
+ .create(jacksonConfig, version, stepConfig.getProvisioner());
+ }
+}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java
index c326525d0c..852088278a 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java
@@ -44,4 +44,8 @@ public void addGson(Gson gson) {
addStepFactory(gson);
}
+ public void addJackson(JacksonJson jackson) {
+ addStepFactory(jackson);
+ }
+
}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/npm/AbstractNpmFormatterStepFactory.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/npm/AbstractNpmFormatterStepFactory.java
index 5467f4c3bc..e4a052106a 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/npm/AbstractNpmFormatterStepFactory.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/npm/AbstractNpmFormatterStepFactory.java
@@ -18,6 +18,7 @@
import java.io.File;
import java.util.AbstractMap;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
@@ -34,6 +35,9 @@ public abstract class AbstractNpmFormatterStepFactory implements FormatterStepFa
@Parameter
private String npmExecutable;
+ @Parameter
+ private String nodeExecutable;
+
@Parameter
private String npmrc;
@@ -42,6 +46,11 @@ protected File npm(FormatterStepConfig stepConfig) {
return npm;
}
+ protected File node(FormatterStepConfig stepConfig) {
+ File node = nodeExecutable != null ? stepConfig.getFileLocator().locateFile(nodeExecutable) : null;
+ return node;
+ }
+
protected File npmrc(FormatterStepConfig stepConfig) {
File npmrc = this.npmrc != null ? stepConfig.getFileLocator().locateFile(this.npmrc) : null;
return npmrc;
@@ -56,7 +65,7 @@ protected File baseDir(FormatterStepConfig stepConfig) {
}
protected NpmPathResolver npmPathResolver(FormatterStepConfig stepConfig) {
- return new NpmPathResolver(npm(stepConfig), npmrc(stepConfig), baseDir(stepConfig));
+ return new NpmPathResolver(npm(stepConfig), node(stepConfig), npmrc(stepConfig), Collections.singletonList(baseDir(stepConfig)));
}
protected boolean moreThanOneNonNull(Object... objects) {
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Jackson.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/JacksonYaml.java
similarity index 53%
rename from plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Jackson.java
rename to plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/JacksonYaml.java
index 2bc7617a38..d97cc41647 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Jackson.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/JacksonYaml.java
@@ -15,32 +15,40 @@
*/
package com.diffplug.spotless.maven.yaml;
-import java.util.Arrays;
-import java.util.List;
+import java.util.Collections;
+import java.util.Map;
import org.apache.maven.plugins.annotations.Parameter;
import com.diffplug.spotless.FormatterStep;
+import com.diffplug.spotless.maven.FormatterFactory;
import com.diffplug.spotless.maven.FormatterStepConfig;
import com.diffplug.spotless.maven.FormatterStepFactory;
-import com.diffplug.spotless.yaml.YamlJacksonStep;
+import com.diffplug.spotless.yaml.JacksonYamlConfig;
+import com.diffplug.spotless.yaml.JacksonYamlStep;
-public class Jackson implements FormatterStepFactory {
+/**
+ * A {@link FormatterFactory} implementation that corresponds to {@code ...} configuration element.
+ */
+public class JacksonYaml implements FormatterStepFactory {
@Parameter
- private String version = YamlJacksonStep.defaultVersion();
+ private String version = JacksonYamlStep.defaultVersion();
@Parameter
- private String[] enabledFeatures = new String[]{"INDENT_OUTPUT"};
+ private Map features = Collections.emptyMap();
@Parameter
- private String[] disabledFeatures = new String[0];
+ private Map yamlFeatures = Collections.emptyMap();
@Override
public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) {
- List enabledFeaturesAsList = Arrays.asList(enabledFeatures);
- List disabledFeaturesAsList = Arrays.asList(disabledFeatures);
- return YamlJacksonStep
- .create(enabledFeaturesAsList, disabledFeaturesAsList, version, stepConfig.getProvisioner());
+ JacksonYamlConfig jacksonConfig = new JacksonYamlConfig();
+
+ jacksonConfig.appendFeatureToToggle(features);
+ jacksonConfig.appendYamlFeatureToToggle(yamlFeatures);
+
+ return JacksonYamlStep
+ .create(jacksonConfig, version, stepConfig.getProvisioner());
}
}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Yaml.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Yaml.java
index 6cba1b2ebd..a6ceaaa592 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Yaml.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/yaml/Yaml.java
@@ -34,7 +34,7 @@ public String licenseHeaderDelimiter() {
return null;
}
- public void addJackson(Jackson jackson) {
+ public void addJackson(JacksonYaml jackson) {
addStepFactory(jackson);
}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java
index 5a724408c8..f4e94e573c 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java
@@ -59,6 +59,7 @@ public class MavenIntegrationHarness extends ResourceHarness {
private static final String EXECUTIONS = "executions";
private static final String MODULES = "modules";
private static final String DEPENDENCIES = "dependencies";
+ private static final String PLUGINS = "plugins";
private static final String MODULE_NAME = "name";
private static final int REMOTE_DEBUG_PORT = 5005;
@@ -151,6 +152,10 @@ protected void writePomWithPrettierSteps(String includes, String... steps) throw
writePom(formats(groupWithSteps("format", including(includes), steps)));
}
+ protected void writePomWithPrettierSteps(String[] plugins, String includes, String... steps) throws IOException {
+ writePom(null, formats(groupWithSteps("format", including(includes), steps)), null, plugins);
+ }
+
protected void writePomWithPomSteps(String... steps) throws IOException {
writePom(groupWithSteps("pom", including("pom_test.xml"), steps));
}
@@ -168,11 +173,11 @@ protected void writePomWithYamlSteps(String... steps) throws IOException {
}
protected void writePom(String... configuration) throws IOException {
- writePom(null, configuration, null);
+ writePom(null, configuration, null, null);
}
- protected void writePom(String[] executions, String[] configuration, String[] dependencies) throws IOException {
- String pomXmlContent = createPomXmlContent(null, executions, configuration, dependencies);
+ protected void writePom(String[] executions, String[] configuration, String[] dependencies, String[] plugins) throws IOException {
+ String pomXmlContent = createPomXmlContent(null, executions, configuration, dependencies, plugins);
setFile("pom.xml").toContent(pomXmlContent);
}
@@ -203,17 +208,17 @@ protected MavenRunner mavenRunnerWithRemoteDebug() throws IOException {
return mavenRunner().withRemoteDebug(REMOTE_DEBUG_PORT);
}
- protected String createPomXmlContent(String pluginVersion, String[] executions, String[] configuration, String[] dependencies) throws IOException {
- return createPomXmlContent("/pom-test.xml.mustache", pluginVersion, executions, configuration, dependencies);
+ protected String createPomXmlContent(String pluginVersion, String[] executions, String[] configuration, String[] dependencies, String[] plugins) throws IOException {
+ return createPomXmlContent("/pom-test.xml.mustache", pluginVersion, executions, configuration, dependencies, plugins);
}
- protected String createPomXmlContent(String pomTemplate, String pluginVersion, String[] executions, String[] configuration, String[] dependencies) throws IOException {
- Map params = buildPomXmlParams(pluginVersion, executions, configuration, null, dependencies);
+ protected String createPomXmlContent(String pomTemplate, String pluginVersion, String[] executions, String[] configuration, String[] dependencies, String[] plugins) throws IOException {
+ Map params = buildPomXmlParams(pluginVersion, executions, configuration, null, dependencies, plugins);
return createPomXmlContent(pomTemplate, params);
}
protected String createPomXmlContent(String pluginVersion, String[] executions, String[] configuration) throws IOException {
- return createPomXmlContent(pluginVersion, executions, configuration, null);
+ return createPomXmlContent(pluginVersion, executions, configuration, null, null);
}
protected String createPomXmlContent(String pomTemplate, Map params) throws IOException {
@@ -226,7 +231,7 @@ protected String createPomXmlContent(String pomTemplate, Map par
}
}
- protected static Map buildPomXmlParams(String pluginVersion, String[] executions, String[] configuration, String[] modules, String[] dependencies) {
+ protected static Map buildPomXmlParams(String pluginVersion, String[] executions, String[] configuration, String[] modules, String[] dependencies, String[] plugins) {
Map params = new HashMap<>();
params.put(SPOTLESS_MAVEN_PLUGIN_VERSION, pluginVersion == null ? getSystemProperty(SPOTLESS_MAVEN_PLUGIN_VERSION) : pluginVersion);
@@ -247,6 +252,10 @@ protected static Map buildPomXmlParams(String pluginVersion, Str
params.put(DEPENDENCIES, String.join("\n", dependencies));
}
+ if (plugins != null) {
+ params.put(PLUGINS, String.join("\n", plugins));
+ }
+
return params;
}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MultiModuleProjectTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MultiModuleProjectTest.java
index ae8378c566..6206affc04 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MultiModuleProjectTest.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MultiModuleProjectTest.java
@@ -145,7 +145,7 @@ private void createRootPom() throws IOException {
modulesList.addAll(subProjects.keySet());
String[] modules = modulesList.toArray(new String[0]);
- Map rootPomParams = buildPomXmlParams(null, null, configuration, modules, null);
+ Map rootPomParams = buildPomXmlParams(null, null, configuration, modules, null, null);
setFile("pom.xml").toContent(createPomXmlContent("/multi-module/pom-parent.xml.mustache", rootPomParams));
}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessCheckMojoTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessCheckMojoTest.java
index 566851c26c..92a78e3923 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessCheckMojoTest.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/SpotlessCheckMojoTest.java
@@ -62,6 +62,7 @@ void testSpotlessCheckBindingToVerifyPhase() throws Exception {
" ${basedir}/license.txt",
" ",
""},
+ null,
null);
testSpotlessCheck(UNFORMATTED_FILE, "verify", true);
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/UpToDateCheckingTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/UpToDateCheckingTest.java
index 6afcba7430..0a9fe8f2d4 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/UpToDateCheckingTest.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/incremental/UpToDateCheckingTest.java
@@ -98,7 +98,8 @@ private void writePomWithPluginManagementAndDependency() throws IOException {
" javax.inject",
" 1",
" ",
- ""}));
+ ""},
+ null));
}
@Test
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java
index 884aba79c7..1897cc764a 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/json/JsonTest.java
@@ -57,9 +57,7 @@ public void testFormatJson_WithGson_sortByKeys() throws Exception {
setFile("json_test.json").toResource("json/sortByKeysBefore.json");
- String output = mavenRunner().withArguments("spotless:apply").runNoError().stdOutUtf8();
- LOGGER.error(output);
- System.err.println(output);
+ mavenRunner().withArguments("spotless:apply").runNoError();
assertFile("json_test.json").sameAsResource("json/sortByKeysAfter.json");
}
@@ -72,4 +70,24 @@ public void testFormatJson_WithGson_defaultConfig_nestedObject() throws Exceptio
assertFile("json_test.json").sameAsResource("json/nestedObjectAfter.json");
}
+ @Test
+ public void testFormatJson_WithJackson_sortByKeys() throws Exception {
+ writePomWithJsonSteps("true");
+
+ setFile("json_test.json").toResource("json/sortByKeysBefore.json");
+
+ mavenRunner().withArguments("spotless:apply", "-X").runNoError();
+ assertFile("json_test.json").sameAsResource("json/sortByKeysAfter_Jackson.json");
+ }
+
+ @Test
+ public void testFormatJson_WithJackson_sortByKeys_spaceAfterKeySeparator() throws Exception {
+ writePomWithJsonSteps("truetrue");
+
+ setFile("json_test.json").toResource("json/sortByKeysBefore.json");
+
+ mavenRunner().withArguments("spotless:apply").runNoError();
+ assertFile("json_test.json").sameAsResource("json/sortByKeysAfter_Jackson_spaceAfterKeySeparator.json");
+ }
+
}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmFrontendMavenPlugin.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmFrontendMavenPlugin.java
new file mode 100644
index 0000000000..de030ab81c
--- /dev/null
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmFrontendMavenPlugin.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 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.maven.npm;
+
+/**
+ * Helper class to configure a maven pom to use frontend-maven-plugin.
+ * @see frontend-maven-plugin on github
+ */
+public final class NpmFrontendMavenPlugin {
+
+ public static final String GROUP_ID = "com.github.eirslett";
+ public static final String ARTIFACT_ID = "frontend-maven-plugin";
+ public static final String VERSION = "1.11.3"; // for now, we stick with this version, as it is the last to support maven 3.1 (see pom-test.xml.mustache)
+
+ public static final String GOAL_INSTALL_NODE_AND_NPM = "install-node-and-npm";
+
+ public static final String INSTALL_DIRECTORY = "target";
+
+ private NpmFrontendMavenPlugin() {
+ // prevent instantiation
+ }
+
+ public static String[] pomPluginLines(String nodeVersion, String npmVersion) {
+ return new String[]{
+ "",
+ String.format(" %s", GROUP_ID),
+ String.format(" %s", ARTIFACT_ID),
+ String.format(" %s", VERSION),
+ " ",
+ " ",
+ " install node and npm",
+ " ",
+ String.format(" %s", GOAL_INSTALL_NODE_AND_NPM),
+ " ",
+ " ",
+ " ",
+ " ",
+ (nodeVersion != null ? " " + nodeVersion + "" : ""),
+ (npmVersion != null ? " " + npmVersion + "" : ""),
+ String.format(" %s", INSTALL_DIRECTORY),
+ " ",
+ ""
+ };
+ }
+
+ public static String installNpmMavenGoal() {
+ return String.format("%s:%s:%s", GROUP_ID, ARTIFACT_ID, GOAL_INSTALL_NODE_AND_NPM);
+ }
+
+ public static String installedNpmPath() {
+ return String.format("%s/node/npm%s", INSTALL_DIRECTORY, System.getProperty("os.name").toLowerCase().contains("win") ? ".cmd" : "");
+ }
+}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmTestsWithDynamicallyInstalledNpmInstallationTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmTestsWithDynamicallyInstalledNpmInstallationTest.java
new file mode 100644
index 0000000000..78830bf09d
--- /dev/null
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/npm/NpmTestsWithDynamicallyInstalledNpmInstallationTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 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.maven.npm;
+
+import static com.diffplug.spotless.maven.npm.NpmFrontendMavenPlugin.installNpmMavenGoal;
+import static com.diffplug.spotless.maven.npm.NpmFrontendMavenPlugin.installedNpmPath;
+import static com.diffplug.spotless.maven.npm.NpmFrontendMavenPlugin.pomPluginLines;
+
+import org.junit.jupiter.api.Test;
+
+import com.diffplug.spotless.maven.MavenIntegrationHarness;
+
+public class NpmTestsWithDynamicallyInstalledNpmInstallationTest extends MavenIntegrationHarness {
+
+ @Test
+ void useDownloadedNpmInstallation() throws Exception {
+ writePomWithPrettierSteps(
+ pomPluginLines("v18.13.0", null),
+ "src/main/typescript/test.ts",
+ "",
+ " " + installedNpmPath() + "",
+ "");
+
+ String kind = "typescript";
+ String suffix = "ts";
+ String configPath = ".prettierrc.yml";
+ setFile(configPath).toResource("npm/prettier/filetypes/" + kind + "/" + ".prettierrc.yml");
+ String path = "src/main/" + kind + "/test." + suffix;
+ setFile(path).toResource("npm/prettier/filetypes/" + kind + "/" + kind + ".dirty");
+
+ mavenRunner().withArguments(installNpmMavenGoal(), "spotless:apply").runNoError();
+ assertFile(path).sameAsResource("npm/prettier/filetypes/" + kind + "/" + kind + ".clean");
+ }
+
+}
diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java
index f17feeb10a..6af4d09ce9 100644
--- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java
+++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/yaml/YamlTest.java
@@ -15,13 +15,10 @@
*/
package com.diffplug.spotless.maven.yaml;
-import static org.assertj.core.api.Assertions.assertThat;
-
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.diffplug.spotless.ProcessRunner;
import com.diffplug.spotless.maven.MavenIntegrationHarness;
public class YamlTest extends MavenIntegrationHarness {
@@ -32,10 +29,7 @@ public void testFormatYaml_WithJackson_defaultConfig_separatorComments() throws
writePomWithYamlSteps("");
setFile("yaml_test.yaml").toResource("yaml/separator_comments.yaml");
- ProcessRunner.Result runNoError = mavenRunner().withArguments("spotless:apply").runNoError();
- LOGGER.error("result: {}", runNoError);
- assertThat(runNoError.exitCode()).as("Run without error %s", runNoError).isEqualTo(0);
- LOGGER.error("GOGO");
+ mavenRunner().withArguments("spotless:apply").runNoError();
assertFile("yaml_test.yaml").sameAsResource("yaml/separator_comments.clean.yaml");
}
diff --git a/plugin-maven/src/test/resources/pom-test.xml.mustache b/plugin-maven/src/test/resources/pom-test.xml.mustache
index 122f5d9218..6db3cb0c15 100644
--- a/plugin-maven/src/test/resources/pom-test.xml.mustache
+++ b/plugin-maven/src/test/resources/pom-test.xml.mustache
@@ -21,6 +21,7 @@
+ {{{plugins}}}
com.diffplug.spotless
spotless-maven-plugin
diff --git a/settings.gradle b/settings.gradle
index 57498bf23f..fb9e7dcf0b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -12,7 +12,7 @@ pluginManagement {
// https://github.com/diffplug/goomph/blob/main/CHANGES.md
id 'com.diffplug.p2.asmaven' version '3.27.0' // DO NOT UPDATE, see https://github.com/diffplug/spotless/pull/874
// https://github.com/gradle/test-retry-gradle-plugin/releases
- id 'org.gradle.test-retry' version '1.5.0'
+ id 'org.gradle.test-retry' version '1.5.1'
// https://github.com/radarsh/gradle-test-logger-plugin/blob/develop/CHANGELOG.md
id 'com.adarshr.test-logger' version '3.2.0'
// https://github.com/davidburstrom/version-compatibility-gradle-plugin/tags
diff --git a/testlib/build.gradle b/testlib/build.gradle
index 64d4681805..639df8a2ab 100644
--- a/testlib/build.gradle
+++ b/testlib/build.gradle
@@ -22,7 +22,7 @@ dependencies {
spotbugs { reportLevel = 'high' } // low|medium|high (low = sensitive to even minor mistakes)
apply from: rootProject.file('gradle/special-tests.gradle')
-tasks.named('test') {
+tasks.withType(Test).configureEach {
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_16)) {
// for Antlr4FormatterStepTest and KtLintStepTest
def args = [
diff --git a/testlib/src/main/resources/json/sortByKeysAfter_Jackson.json b/testlib/src/main/resources/json/sortByKeysAfter_Jackson.json
new file mode 100644
index 0000000000..3af39fd0fb
--- /dev/null
+++ b/testlib/src/main/resources/json/sortByKeysAfter_Jackson.json
@@ -0,0 +1,15 @@
+{
+ "A": 1,
+ "X": 2,
+ "_arraysNotSorted": [ 3, 2, 1 ],
+ "a": 3,
+ "c": 4,
+ "x": 5,
+ "z": {
+ "A": 1,
+ "X": 2,
+ "a": 3,
+ "c": 4,
+ "x": 5
+ }
+}
diff --git a/testlib/src/main/resources/json/sortByKeysAfter_Jackson_spaceAfterKeySeparator.json b/testlib/src/main/resources/json/sortByKeysAfter_Jackson_spaceAfterKeySeparator.json
new file mode 100644
index 0000000000..3af39fd0fb
--- /dev/null
+++ b/testlib/src/main/resources/json/sortByKeysAfter_Jackson_spaceAfterKeySeparator.json
@@ -0,0 +1,15 @@
+{
+ "A": 1,
+ "X": 2,
+ "_arraysNotSorted": [ 3, 2, 1 ],
+ "a": 3,
+ "c": 4,
+ "x": 5,
+ "z": {
+ "A": 1,
+ "X": 2,
+ "a": 3,
+ "c": 4,
+ "x": 5
+ }
+}
diff --git a/testlib/src/main/resources/yaml/array_at_root.clean.yaml b/testlib/src/main/resources/yaml/array_at_root.clean.yaml
new file mode 100644
index 0000000000..f9bae04114
--- /dev/null
+++ b/testlib/src/main/resources/yaml/array_at_root.clean.yaml
@@ -0,0 +1,5 @@
+---
+- key1: "value1"
+ key2: "value2"
+- key3: "value3"
+ key4: "value4"
diff --git a/testlib/src/main/resources/yaml/array_at_root.yaml b/testlib/src/main/resources/yaml/array_at_root.yaml
new file mode 100644
index 0000000000..b26f6bb246
--- /dev/null
+++ b/testlib/src/main/resources/yaml/array_at_root.yaml
@@ -0,0 +1,7 @@
+- key1: value1
+ key2: value2
+
+
+- key3: value3
+ key4: value4
+
diff --git a/testlib/src/main/resources/yaml/array_with_bracket.clean.no_start_marker.no_quotes.yaml b/testlib/src/main/resources/yaml/array_with_bracket.clean.no_start_marker.no_quotes.yaml
new file mode 100644
index 0000000000..0e5571531a
--- /dev/null
+++ b/testlib/src/main/resources/yaml/array_with_bracket.clean.no_start_marker.no_quotes.yaml
@@ -0,0 +1,11 @@
+episodes:
+- 1
+- 2
+- 3
+- 4
+- 5
+- 6
+- 7
+best-jedi:
+ name: Obi-Wan
+ side: light
diff --git a/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml b/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml
index aa731919b4..ff56b4f77a 100644
--- a/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml
+++ b/testlib/src/main/resources/yaml/multiple_documents.clean.jackson.yaml
@@ -1,2 +1,6 @@
---
-document: "this is document 1"
+key1: "value1"
+key2: "value2"
+---
+key3: "value3"
+key4: "value4"
diff --git a/testlib/src/main/resources/yaml/multiple_documents.clean.yaml b/testlib/src/main/resources/yaml/multiple_documents.clean.yaml
index 45e0b0916b..3651473955 100644
--- a/testlib/src/main/resources/yaml/multiple_documents.clean.yaml
+++ b/testlib/src/main/resources/yaml/multiple_documents.clean.yaml
@@ -1,8 +1,8 @@
----
-document: this is document 1
-
----
-document: this is document 2
+# Document 1
+key1: value1
+key2: value2
---
-document: this is document 3
\ No newline at end of file
+# Document 2
+key3: value3
+key4: value4
diff --git a/testlib/src/main/resources/yaml/multiple_documents.yaml b/testlib/src/main/resources/yaml/multiple_documents.yaml
index 51f14a5862..3651473955 100644
--- a/testlib/src/main/resources/yaml/multiple_documents.yaml
+++ b/testlib/src/main/resources/yaml/multiple_documents.yaml
@@ -1,10 +1,8 @@
-document: this is document 1
----
-
-document: this is document 2
-
+# Document 1
+key1: value1
+key2: value2
---
-
-
-document: this is document 3
\ No newline at end of file
+# Document 2
+key3: value3
+key4: value4
diff --git a/testlib/src/test/java/com/diffplug/spotless/json/gson/GsonStepTest.java b/testlib/src/test/java/com/diffplug/spotless/json/gson/GsonStepTest.java
index 8f1d758836..3f4d2a84e0 100644
--- a/testlib/src/test/java/com/diffplug/spotless/json/gson/GsonStepTest.java
+++ b/testlib/src/test/java/com/diffplug/spotless/json/gson/GsonStepTest.java
@@ -28,7 +28,7 @@
public class GsonStepTest extends JsonFormatterStepCommonTests {
- private static final String DEFAULT_VERSION = "2.8.9";
+ private static final String DEFAULT_VERSION = "2.10.1";
@Test
void handlesComplexNestedObject() {
@@ -52,34 +52,34 @@ void handlesNotJson() {
@Test
void handlesSortingWhenSortByKeyEnabled() {
- FormatterStep step = GsonStep.create(INDENT, true, false, DEFAULT_VERSION, TestProvisioner.mavenCentral());
+ FormatterStep step = GsonStep.create(new GsonConfig(true, false, INDENT, DEFAULT_VERSION), TestProvisioner.mavenCentral());
StepHarness.forStep(step).testResource("json/sortByKeysBefore.json", "json/sortByKeysAfter.json");
}
@Test
void doesNoSortingWhenSortByKeyDisabled() {
- FormatterStep step = GsonStep.create(INDENT, false, false, DEFAULT_VERSION, TestProvisioner.mavenCentral());
+ FormatterStep step = GsonStep.create(new GsonConfig(false, false, INDENT, DEFAULT_VERSION), TestProvisioner.mavenCentral());
StepHarness.forStep(step)
.testResource("json/sortByKeysBefore.json", "json/sortByKeysAfterDisabled.json");
}
@Test
void handlesHtmlEscapeWhenEnabled() {
- FormatterStep step = GsonStep.create(INDENT, false, true, DEFAULT_VERSION, TestProvisioner.mavenCentral());
+ FormatterStep step = GsonStep.create(new GsonConfig(false, true, INDENT, DEFAULT_VERSION), TestProvisioner.mavenCentral());
StepHarness.forStep(step)
.testResource("json/escapeHtmlGsonBefore.json", "json/escapeHtmlGsonAfter.json");
}
@Test
void writesRawHtmlWhenHtmlEscapeDisabled() {
- FormatterStep step = GsonStep.create(INDENT, false, false, DEFAULT_VERSION, TestProvisioner.mavenCentral());
+ FormatterStep step = GsonStep.create(new GsonConfig(false, false, INDENT, DEFAULT_VERSION), TestProvisioner.mavenCentral());
StepHarness.forStep(step)
.testResource("json/escapeHtmlGsonBefore.json", "json/escapeHtmlGsonAfterDisabled.json");
}
@Test
void handlesVersionIncompatibility() {
- FormatterStep step = GsonStep.create(INDENT, false, false, "1.7", TestProvisioner.mavenCentral());
+ FormatterStep step = GsonStep.create(new GsonConfig(false, false, INDENT, "1.7"), TestProvisioner.mavenCentral());
Assertions.assertThatThrownBy(() -> step.format("", new File("")))
.isInstanceOf(IllegalStateException.class)
.hasMessage("There was a problem interacting with Gson; maybe you set an incompatible version?");
@@ -87,6 +87,6 @@ void handlesVersionIncompatibility() {
@Override
protected FormatterStep createFormatterStep(int indent, Provisioner provisioner) {
- return GsonStep.create(indent, false, false, DEFAULT_VERSION, provisioner);
+ return GsonStep.create(new GsonConfig(false, false, indent, DEFAULT_VERSION), provisioner);
}
}
diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/NpmFormatterStepCommonTests.java b/testlib/src/test/java/com/diffplug/spotless/npm/NpmFormatterStepCommonTests.java
index dff105108a..60dd42801a 100644
--- a/testlib/src/test/java/com/diffplug/spotless/npm/NpmFormatterStepCommonTests.java
+++ b/testlib/src/test/java/com/diffplug/spotless/npm/NpmFormatterStepCommonTests.java
@@ -17,17 +17,22 @@
import java.io.File;
import java.io.IOException;
+import java.util.Collections;
import com.diffplug.spotless.ResourceHarness;
public abstract class NpmFormatterStepCommonTests extends ResourceHarness {
protected NpmPathResolver npmPathResolver() {
- return new NpmPathResolver(npmExecutable(), npmrc());
+ return new NpmPathResolver(npmExecutable(), nodeExecutable(), npmrc(), Collections.emptyList());
}
private File npmExecutable() {
- return NpmExecutableResolver.tryFind().orElseThrow(() -> new IllegalStateException("cannot detect node binary"));
+ return NpmExecutableResolver.tryFind().orElseThrow(() -> new IllegalStateException("cannot detect npm binary"));
+ }
+
+ private File nodeExecutable() {
+ return NodeExecutableResolver.tryFindNextTo(npmExecutable()).orElseThrow(() -> new IllegalStateException("cannot detect node binary"));
}
private File npmrc() {