diff --git a/CHANGES.md b/CHANGES.md index bc19d79cf5..867e6cfee0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* `PipeStepPair` which allows extracting blocks of text in one step, then injecting those blocks back in later. Currently only used for `spotless:off` `spotless:on`, but could also be used to [apply different steps in different places](https://github.com/diffplug/spotless/issues/412) ([#691](https://github.com/diffplug/spotless/pull/691)). ### Changed * When applying license headers for the first time, we are now more lenient about parsing existing years from the header ([#690](https://github.com/diffplug/spotless/pull/690)). diff --git a/README.md b/README.md index c043a88c77..42d5bdcf32 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ output = [ '| --------------------------------------------- | ------------- | ------------ | ------------ | --------|', '| Automatic [idempotency safeguard](PADDEDCELL.md) | {{yes}} | {{yes}} | {{yes}} | {{no}} |', '| Misconfigured [encoding safeguard](https://github.com/diffplug/spotless/blob/08340a11566cdf56ecf50dbd4d557ed84a70a502/testlib/src/test/java/com/diffplug/spotless/EncodingErrorMsgTest.java#L34-L38) | {{yes}} | {{yes}} | {{yes}} | {{no}} |', +'| Toggle with [`spotless:off` and `spotless:on`](plugin-gradle/#spotlessoff-and-spotlesson) | {{yes}} | {{yes}} | {{no}} | {{no}} |', '| [Ratchet from](https://github.com/diffplug/spotless/tree/main/plugin-gradle#ratchet) `origin/main` or other git ref | {{yes}} | {{yes}} | {{no}} | {{no}} |', '| Define [line endings using git](https://github.com/diffplug/spotless/tree/main/plugin-gradle#line-endings-and-encodings-invisible-stuff) | {{yes}} | {{yes}} | {{yes}} | {{no}} |', '| Fast incremental format and up-to-date check | {{yes}} | {{no}} | {{no}} | {{no}} |', @@ -73,6 +74,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | --------------------------------------------- | ------------- | ------------ | ------------ | --------| | Automatic [idempotency safeguard](PADDEDCELL.md) | :+1: | :+1: | :+1: | :white_large_square: | | Misconfigured [encoding safeguard](https://github.com/diffplug/spotless/blob/08340a11566cdf56ecf50dbd4d557ed84a70a502/testlib/src/test/java/com/diffplug/spotless/EncodingErrorMsgTest.java#L34-L38) | :+1: | :+1: | :+1: | :white_large_square: | +| Toggle with [`spotless:off` and `spotless:on`](plugin-gradle/#spotlessoff-and-spotlesson) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [Ratchet from](https://github.com/diffplug/spotless/tree/main/plugin-gradle#ratchet) `origin/main` or other git ref | :+1: | :+1: | :white_large_square: | :white_large_square: | | Define [line endings using git](https://github.com/diffplug/spotless/tree/main/plugin-gradle#line-endings-and-encodings-invisible-stuff) | :+1: | :+1: | :+1: | :white_large_square: | | Fast incremental format and up-to-date check | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | diff --git a/lib/src/main/java/com/diffplug/spotless/generic/PipeStepPair.java b/lib/src/main/java/com/diffplug/spotless/generic/PipeStepPair.java new file mode 100644 index 0000000000..907bd1da0f --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/generic/PipeStepPair.java @@ -0,0 +1,157 @@ +/* + * Copyright 2020 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.generic; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.diffplug.spotless.FormatterStep; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class PipeStepPair { + /** The two steps will be named `In` and `Out`. */ + public static Builder named(String name) { + return new Builder(name); + } + + public static String defaultToggleName() { + return "toggle"; + } + + public static String defaultToggleOff() { + return "spotless:off"; + } + + public static String defaultToggleOn() { + return "spotless:on"; + } + + public static class Builder { + String name; + Pattern regex; + + private Builder(String name) { + this.name = Objects.requireNonNull(name); + } + + /** Defines the opening and closing markers. */ + public Builder openClose(String open, String close) { + return regex(Pattern.quote(open) + "([\\s\\S]*?)" + Pattern.quote(close)); + } + + /** Defines the pipe via regex. Must have *exactly one* capturing group. */ + public Builder regex(String regex) { + return regex(Pattern.compile(regex)); + } + + /** Defines the pipe via regex. Must have *exactly one* capturing group. */ + public Builder regex(Pattern regex) { + this.regex = regex; + return this; + } + + public PipeStepPair buildPair() { + return new PipeStepPair(name, regex); + } + } + + final FormatterStep in, out; + + private PipeStepPair(String name, Pattern pattern) { + StateIn stateIn = new StateIn(pattern); + StateOut stateOut = new StateOut(stateIn); + in = FormatterStep.create(name + "In", stateIn, state -> state::format); + out = FormatterStep.create(name + "Out", stateOut, state -> state::format); + } + + public FormatterStep in() { + return in; + } + + public FormatterStep out() { + return out; + } + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + static class StateIn implements Serializable { + private static final long serialVersionUID = -844178006407733370L; + + final Pattern regex; + + public StateIn(Pattern regex) { + this.regex = regex; + } + + final transient ArrayList groups = new ArrayList<>(); + + private String format(String unix) { + groups.clear(); + Matcher matcher = regex.matcher(unix); + while (matcher.find()) { + groups.add(matcher.group(1)); + } + return unix; + } + } + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + static class StateOut implements Serializable { + private static final long serialVersionUID = -1195263184715054229L; + + final StateIn in; + + StateOut(StateIn in) { + this.in = in; + } + + final transient StringBuilder builder = new StringBuilder(); + + private String format(String unix) { + if (in.groups.isEmpty()) { + return unix; + } + builder.setLength(0); + Matcher matcher = in.regex.matcher(unix); + int lastEnd = 0; + int groupIdx = 0; + while (matcher.find()) { + builder.append(unix, lastEnd, matcher.start(1)); + builder.append(in.groups.get(groupIdx)); + lastEnd = matcher.end(1); + ++groupIdx; + } + if (groupIdx == in.groups.size()) { + builder.append(unix, lastEnd, unix.length()); + return builder.toString(); + } else { + // throw an error with either the full regex, or the nicer open/close pair + Matcher openClose = Pattern.compile("\\\\Q([\\s\\S]*?)\\\\E" + "\\Q([\\s\\S]*?)\\E" + "\\\\Q([\\s\\S]*?)\\\\E") + .matcher(in.regex.pattern()); + String pattern; + if (openClose.matches()) { + pattern = openClose.group(1) + " " + openClose.group(2); + } else { + pattern = in.regex.pattern(); + } + throw new Error("An intermediate step removed a match of " + pattern); + } + } + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 7d8f339840..e0878afdfb 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +* New option [`toggleOffOn()`](README.md#spotlessoff-and-spotlesson) which allows the tags `spotless:off` and `spotless:on` to protect sections of code from the rest of the formatters ([#691](https://github.com/diffplug/spotless/pull/691)). ### Changed * When applying license headers for the first time, we are now more lenient about parsing existing years from the header ([#690](https://github.com/diffplug/spotless/pull/690)). diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 6086bcc3f1..0360ee474e 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -33,7 +33,7 @@ output = prefixDelimiterReplace(input, 'https://javadoc.io/static/com.diffplug.s Spotless is a general-purpose formatting plugin used by [4,000 projects on GitHub (August 2020)](https://github.com/search?l=gradle&q=spotless&type=Code). It is completely à la carte, but also includes powerful "batteries-included" if you opt-in. -To people who use your build, it looks like this ([IDE support also available]()): +To people who use your build, it looks like this ([IDE support also available](IDE_HOOK.md)): ```console user@machine repo % ./gradlew build @@ -78,6 +78,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - **Language independent** - [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history)) - [How can I enforce formatting gradually? (aka "ratchet")](#ratchet) + - [`spotless:off` and `spotless:on`](#spotlessoff-and-spotlesson) - [Line endings and encodings (invisible stuff)](#line-endings-and-encodings-invisible-stuff) - [Custom steps](#custom-steps) - [Multiple (or custom) language-specific blocks](#multiple-or-custom-language-specific-blocks) @@ -694,11 +695,23 @@ However, we strongly recommend that you use a non-local branch, such as a tag or This is especially helpful for injecting accurate copyright dates using the [license step](#license-header). +## `spotless:off` and `spotless:on` + +Sometimes there is a chunk of code which you have carefully handcrafted, and you would like to exclude just this one little part from getting clobbered by the autoformat. Some formatters have a way to do this, many don't, but who cares. If you setup your spotless like this: + +```gradle +spotless { + java { // or kotlin, or c, or python, or whatever + toggleOffOn() +``` + +Then whenever Spotless encounters a pair of `spotless:off` / `spotless:on`, it will exclude the code between them from formatting, regardless of all other rules. If you want, you can change the tags to be whatever you want, e.g. `toggleOffOn('fmt:off', 'fmt:on')`. If you decide to change the default, be sure to [read this](https://github.com/diffplug/spotless/pull/691) for some gotchas. + ## Line endings and encodings (invisible stuff) -Spotless uses UTF-8 by default, but you can use [any encoding which Java supports](https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html). You can set it globally, and you can also set it per-format. +Spotless uses UTF-8 by default, but you can use [any encoding which the JVM supports](https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html). You can set it globally, and you can also set it per-format. ```gradle spotless { 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 be55495888..efa056d421 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 @@ -53,6 +53,7 @@ import com.diffplug.spotless.generic.IndentStep; import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.generic.LicenseHeaderStep.YearMode; +import com.diffplug.spotless.generic.PipeStepPair; import com.diffplug.spotless.generic.ReplaceRegexStep; import com.diffplug.spotless.generic.ReplaceStep; import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep; @@ -623,12 +624,46 @@ public EclipseWtpConfig eclipseWtp(EclipseWtpFormatterStep type, String version) return new EclipseWtpConfig(type, version); } + /** + * Given a regex with *exactly one capturing group*, disables formatting + * inside that captured group. + */ + public void toggleOffOnRegex(String regex) { + this.togglePair = PipeStepPair.named(PipeStepPair.defaultToggleName()).regex(regex).buildPair(); + } + + /** Disables formatting between the given tags. */ + public void toggleOffOn(String off, String on) { + this.togglePair = PipeStepPair.named(PipeStepPair.defaultToggleName()).openClose(off, on).buildPair(); + } + + /** Disables formatting between `spotless:off` and `spotless:on`. */ + public void toggleOffOn() { + toggleOffOn(PipeStepPair.defaultToggleOff(), PipeStepPair.defaultToggleOn()); + } + + /** Undoes all previous calls to {@link #toggleOffOn()} and {@link #toggleOffOn(String, String)}. */ + public void toggleOffOnDisable() { + this.togglePair = null; + } + + private @Nullable PipeStepPair togglePair; + /** Sets up a format task according to the values in this extension. */ protected void setupTask(SpotlessTask task) { task.setEncoding(getEncoding().name()); task.setExceptionPolicy(exceptionPolicy); FileCollection totalTarget = targetExclude == null ? target : target.minus(targetExclude); task.setTarget(totalTarget); + List steps; + if (togglePair != null) { + steps = new ArrayList<>(this.steps.size() + 2); + steps.add(togglePair.in()); + steps.addAll(this.steps); + steps.add(togglePair.out()); + } else { + steps = this.steps; + } task.setSteps(steps); task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> totalTarget)); if (spotless.project != spotless.project.getRootProject()) { diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ToggleOffOnTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ToggleOffOnTest.java new file mode 100644 index 0000000000..bf1e404b81 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/ToggleOffOnTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2020 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.gradle.spotless; + +import java.io.IOException; + +import org.junit.Test; + +public class ToggleOffOnTest extends GradleIntegrationHarness { + @Test + public void toggleOffOn() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " custom 'lowercase', { str -> str.toLowerCase() }", + " toggleOffOn()", + " }", + "}"); + setFile("test.md").toLines( + "A B C", + "spotless:off", + "D E F", + "spotless:on", + "G H I"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("test.md").hasLines( + "a b c", + "spotless:off", + "D E F", + "spotless:on", + "g h i"); + } +} diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index f435bb8911..b5707ed3b4 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* New option [``](README.md#spotlessoff-and-spotlesson) which allows the tags `spotless:off` and `spotless:on` to protect sections of code from the rest of the formatters ([#691](https://github.com/diffplug/spotless/pull/691)). ### Changed * When applying license headers for the first time, we are now more lenient about parsing existing years from the header ([#690](https://github.com/diffplug/spotless/pull/690)). diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 882041f049..8c7162104c 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -59,6 +59,7 @@ user@machine repo % mvn spotless:check - [Generic steps](#generic-steps) - [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history)) - [How can I enforce formatting gradually? (aka "ratchet")](#ratchet) + - [`spotless:off` and `spotless:on`](#spotlessoff-and-spotlesson) - [Line endings and encodings (invisible stuff)](#line-endings-and-encodings-invisible-stuff) - [Disabling warnings and error messages](#disabling-warnings-and-error-messages) - [How do I preview what `mvn spotless:apply` will do?](#how-do-i-preview-what-mvn-spotlessapply-will-do) @@ -654,6 +655,18 @@ However, we strongly recommend that you use a non-local branch, such as a tag or This is especially helpful for injecting accurate copyright dates using the [license step](#license-header). +## `spotless:off` and `spotless:on` + +Sometimes there is a chunk of code which you have carefully handcrafted, and you would like to exclude just this one little part from getting clobbered by the autoformat. Some formatters have a way to do this, many don't, but who cares. If you setup your spotless like this: + +```xml + + + + ... +``` + +Then whenever Spotless encounters a pair of `spotless:off` / `spotless:on`, it will exclude that subsection of code from formatting. If you want, you can change the tags to be whatever you want, e.g. `fmt:offfmt:on')`. If you change the default, [read this](https://github.com/diffplug/spotless/pull/691) for some gotchas. ## Line endings and encodings (invisible stuff) diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java index 29edc0acaa..90fad16e9d 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterFactory.java @@ -16,7 +16,6 @@ package com.diffplug.spotless.maven; import static java.util.Collections.emptySet; -import static java.util.stream.Collectors.toList; import java.io.File; import java.nio.charset.Charset; @@ -25,6 +24,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.apache.maven.plugins.annotations.Parameter; @@ -33,6 +33,7 @@ import com.diffplug.spotless.Formatter; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.generic.PipeStepPair; import com.diffplug.spotless.maven.generic.*; public abstract class FormatterFactory { @@ -56,6 +57,8 @@ public abstract class FormatterFactory { private final List stepFactories = new ArrayList<>(); + private ToggleOffOn toggle; + public abstract Set defaultIncludes(); public abstract String licenseHeaderDelimiter(); @@ -79,7 +82,12 @@ public final Formatter newFormatter(List filesToFormat, FormatterConfig co List formatterSteps = factories.stream() .filter(Objects::nonNull) // all unrecognized steps from XML config appear as nulls in the list .map(factory -> factory.newFormatterStep(stepConfig)) - .collect(toList()); + .collect(Collectors.toCollection(() -> new ArrayList())); + if (toggle != null) { + PipeStepPair pair = toggle.createPair(); + formatterSteps.add(0, pair.in()); + formatterSteps.add(pair.out()); + } return Formatter.builder() .encoding(formatterEncoding) @@ -122,6 +130,10 @@ public final void addPrettier(Prettier prettier) { addStepFactory(prettier); } + public final void addToggleOffOn(ToggleOffOn toggle) { + this.toggle = toggle; + } + protected final void addStepFactory(FormatterStepFactory stepFactory) { Objects.requireNonNull(stepFactory); stepFactories.add(stepFactory); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ToggleOffOn.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ToggleOffOn.java new file mode 100644 index 0000000000..fe1676aec9 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/ToggleOffOn.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.maven.generic; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.generic.PipeStepPair; + +public class ToggleOffOn { + @Parameter + public String off = PipeStepPair.defaultToggleOff(); + + @Parameter + public String on = PipeStepPair.defaultToggleOn(); + + @Parameter + public String regex; + + public PipeStepPair createPair() { + if (regex != null) { + return PipeStepPair.named(PipeStepPair.defaultToggleName()).regex(regex).buildPair(); + } else { + return PipeStepPair.named(PipeStepPair.defaultToggleName()).openClose(off, on).buildPair(); + } + } +} diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/ToggleOffOnTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/ToggleOffOnTest.java new file mode 100644 index 0000000000..9b876e4ad7 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/generic/ToggleOffOnTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016-2020 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.maven.generic; + +import org.junit.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +public class ToggleOffOnTest extends MavenIntegrationHarness { + @Test + public void toggleOffOn() throws Exception { + writePomWithJavaSteps( + "", + " true", + " 1", + "", + ""); + setFile("src/main/java/Main.java").toLines( + " Here is some stuff", + " No matter", + "//spotless:off", + " This won't get tabbed", + "//spotless:on", + " But this will get tabbed."); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("src/main/java/Main.java").hasLines( + " Here is some stuff", + " No matter", + "//spotless:off", + " This won't get tabbed", + "//spotless:on", + " But this will get tabbed."); + } + + @Test + public void toggleOffOnCustom() throws Exception { + writePomWithJavaSteps( + "", + " true", + " 1", + "", + "", + " //off", + " //on", + ""); + setFile("src/main/java/Main.java").toLines( + " Here is some stuff", + " No matter", + "//off", + " This won't get tabbed", + "//on", + " But this will get tabbed."); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("src/main/java/Main.java").hasLines( + " Here is some stuff", + " No matter", + "//off", + " This won't get tabbed", + "//on", + " But this will get tabbed."); + } +} diff --git a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java index 38cf0fc332..e8bf6fdb1a 100644 --- a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java +++ b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java @@ -16,6 +16,9 @@ package com.diffplug.spotless; import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Arrays; import java.util.Objects; import java.util.function.Consumer; @@ -45,6 +48,16 @@ public static StepHarness forStep(FormatterStep step) { input -> LineEnding.toUnix(step.format(input, new File(""))))); } + /** Creates a harness for testing steps which don't depend on the file. */ + public static StepHarness forSteps(FormatterStep... steps) { + return forFormatter(Formatter.builder() + .steps(Arrays.asList(steps)) + .lineEndingsPolicy(LineEnding.UNIX.createPolicy()) + .encoding(StandardCharsets.UTF_8) + .rootDir(Paths.get("")) + .build()); + } + /** Creates a harness for testing a formatter whose steps don't depend on the file. */ public static StepHarness forFormatter(Formatter formatter) { return new StepHarness(FormatterFunc.Closeable.ofDangerous( @@ -80,8 +93,12 @@ public StepHarness testResourceUnaffected(String resourceIdempotent) throws Exce } /** Asserts that the given elements in the resources directory are transformed as expected. */ - public StepHarness testException(String resourceBefore, Consumer> exceptionAssertion) throws Exception { - String before = ResourceHarness.getTestResource(resourceBefore); + public StepHarness testResourceException(String resourceBefore, Consumer> exceptionAssertion) throws Exception { + return testException(ResourceHarness.getTestResource(resourceBefore), exceptionAssertion); + } + + /** Asserts that the given elements in the resources directory are transformed as expected. */ + public StepHarness testException(String before, Consumer> exceptionAssertion) throws Exception { try { formatter.apply(before); Assert.fail(); diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/PipeStepPairTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/PipeStepPairTest.java new file mode 100644 index 0000000000..1a32168e4d --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/generic/PipeStepPairTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.generic; + +import java.util.Locale; + +import org.junit.Test; + +import com.diffplug.common.base.StringPrinter; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.StepHarness; + +public class PipeStepPairTest { + @Test + public void single() throws Exception { + PipeStepPair pair = PipeStepPair.named("underTest").openClose("spotless:off", "spotless:on").buildPair(); + FormatterStep lowercase = FormatterStep.createNeverUpToDate("lowercase", str -> str.toLowerCase(Locale.ROOT)); + StepHarness harness = StepHarness.forSteps(pair.in(), lowercase, pair.out()); + harness.test( + StringPrinter.buildStringFromLines( + "A B C", + "spotless:off", + "D E F", + "spotless:on", + "G H I"), + StringPrinter.buildStringFromLines( + "a b c", + "spotless:off", + "D E F", + "spotless:on", + "g h i")); + } + + @Test + public void multiple() throws Exception { + PipeStepPair pair = PipeStepPair.named("underTest").openClose("spotless:off", "spotless:on").buildPair(); + FormatterStep lowercase = FormatterStep.createNeverUpToDate("lowercase", str -> str.toLowerCase(Locale.ROOT)); + StepHarness harness = StepHarness.forSteps(pair.in(), lowercase, pair.out()); + harness.test( + StringPrinter.buildStringFromLines( + "A B C", + "spotless:off", + "D E F", + "spotless:on", + "G H I", + "spotless:off J K L spotless:on", + "M N O", + "P Q R", + "S T U spotless:off V W", + " X ", + " Y spotless:on Z", + "1 2 3"), + StringPrinter.buildStringFromLines( + "a b c", + "spotless:off", + "D E F", + "spotless:on", + "g h i", + "spotless:off J K L spotless:on", + "m n o", + "p q r", + "s t u spotless:off V W", + " X ", + " Y spotless:on z", + "1 2 3")); + } + + @Test + public void broken() throws Exception { + PipeStepPair pair = PipeStepPair.named("underTest").openClose("spotless:off", "spotless:on").buildPair(); + FormatterStep uppercase = FormatterStep.createNeverUpToDate("uppercase", str -> str.toUpperCase(Locale.ROOT)); + StepHarness harness = StepHarness.forSteps(pair.in(), uppercase, pair.out()); + // this fails because uppercase turns spotless:off into SPOTLESS:OFF, etc + harness.testException(StringPrinter.buildStringFromLines( + "A B C", + "spotless:off", + "D E F", + "spotless:on", + "G H I"), exception -> { + exception.hasMessage("An intermediate step removed a match of spotless:off spotless:on"); + }); + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java index dc4738a074..4c77801d9d 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java @@ -33,13 +33,13 @@ public class GoogleJavaFormatStepTest extends ResourceHarness { public void suggestJre11() throws Exception { try (StepHarness step = StepHarness.forStep(GoogleJavaFormatStep.create(TestProvisioner.mavenCentral()))) { if (JreVersion.thisVm() < 11) { - step.testException("java/googlejavaformat/TextBlock.dirty", throwable -> { + step.testResourceException("java/googlejavaformat/TextBlock.dirty", throwable -> { throwable.hasMessageStartingWith("You are running Spotless on JRE 8") .hasMessageEndingWith(", which limits you to google-java-format 1.7\n" + "If you upgrade your build JVM to 11+, then you can use google-java-format 1.9, which may have fixed this problem."); }); } else if (JreVersion.thisVm() < 13) { - step.testException("java/googlejavaformat/TextBlock.dirty", throwable -> { + step.testResourceException("java/googlejavaformat/TextBlock.dirty", throwable -> { throwable.isInstanceOf(InvocationTargetException.class) .extracting(exception -> exception.getCause().getMessage()).asString().contains("7:18: error: unclosed string literal"); }); diff --git a/testlib/src/test/java/com/diffplug/spotless/kotlin/KtLintStepTest.java b/testlib/src/test/java/com/diffplug/spotless/kotlin/KtLintStepTest.java index dbdc5898a2..bfa2f77fc1 100644 --- a/testlib/src/test/java/com/diffplug/spotless/kotlin/KtLintStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/kotlin/KtLintStepTest.java @@ -38,7 +38,7 @@ public void behavior() throws Exception { FormatterStep step = KtLintStep.create(TestProvisioner.jcenter()); StepHarness.forStep(step) .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") - .testException("kotlin/ktlint/unsolvable.dirty", assertion -> { + .testResourceException("kotlin/ktlint/unsolvable.dirty", assertion -> { assertion.isInstanceOf(AssertionError.class); assertion.hasMessage("Error on line: 1, column: 1\n" + "Wildcard import"); @@ -52,7 +52,7 @@ public void worksShyiko() throws Exception { FormatterStep step = KtLintStep.create("0.31.0", TestProvisioner.jcenter()); StepHarness.forStep(step) .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") - .testException("kotlin/ktlint/unsolvable.dirty", assertion -> { + .testResourceException("kotlin/ktlint/unsolvable.dirty", assertion -> { assertion.isInstanceOf(AssertionError.class); assertion.hasMessage("Error on line: 1, column: 1\n" + "Wildcard import"); @@ -69,7 +69,7 @@ public void worksPinterestAndPre034() throws Exception { FormatterStep step = KtLintStep.create("0.32.0", TestProvisioner.jcenter()); StepHarness.forStep(step) .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") - .testException("kotlin/ktlint/unsolvable.dirty", assertion -> { + .testResourceException("kotlin/ktlint/unsolvable.dirty", assertion -> { assertion.isInstanceOf(AssertionError.class); assertion.hasMessage("Error on line: 1, column: 1\n" + "Wildcard import"); diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java index 10b960d418..1ac24e8422 100644 --- a/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java @@ -116,7 +116,7 @@ public void verifyPrettierErrorMessageIsRelayed() throws Exception { npmExecutable(), new PrettierConfig(null, ImmutableMap.of("parser", "postcss"))); try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) { - stepHarness.testException("npm/prettier/filetypes/scss/scss.dirty", exception -> { + stepHarness.testResourceException("npm/prettier/filetypes/scss/scss.dirty", exception -> { exception.hasMessageContaining("HTTP 501"); exception.hasMessageContaining("Couldn't resolve parser \"postcss\""); });