Skip to content

Commit 9c6b725

Browse files
authored
Merge pull request #561 from diffplug/feat/paddedcell-mandatory
Make padded cell mandatory
2 parents de2d9b6 + 36a8f97 commit 9c6b725

File tree

13 files changed

+253
-302
lines changed

13 files changed

+253
-302
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1212
## [Unreleased]
1313
### Added
1414
* Support for google-java-format 1.8 (including test infrastructure for Java 11). ([#562](https://github.com/diffplug/spotless/issues/562))
15+
* Improved PaddedCell such that it is as performant as non-padded cell - no reason not to have it always enabled. Deprecated all of `PaddedCellBulk`. ([#561](https://github.com/diffplug/spotless/pull/561))
1516

1617
## [1.28.1] - 2020-04-02
1718
### Fixed

PADDEDCELL.md

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,4 @@
1-
# You have a misbehaving rule that needs a `paddedCell()`
2-
3-
`spotlessCheck` has detected that one of your rules is misbehaving. This will cause `spotlessCheck` to fail even after you have called `spotlessApply`. To bandaid over this problem, add `paddedCell()` to your `build.gradle`, as such:
4-
5-
```gradle
6-
spotless {
7-
java {
8-
...
9-
paddedCell()
10-
}
11-
}
12-
```
13-
14-
This is not a bug in Spotless itself, but in one of the third-party formatters, such as the [eclipse formatter](https://bugs.eclipse.org/bugs/show_bug.cgi?id=310642), [google-java-format](https://github.com/google/google-java-format/issues), or some custom rule.
15-
16-
`paddedCell()` will ensure that your code continues to be formatted, although it will be a little slower. Now when you run `spotlessCheck`, it will generate helpful bug report files in the `build/spotless-diagnose-<FORMAT_NAME>` folder which will contain the states that your rules are fighting over. These files are very helpful to the developers of the code formatter you are using.
1+
# PaddedCell
172

183
## How spotless works
194

@@ -46,26 +31,7 @@ The rule we wrote above is obviously a bad idea. But complex code formatters ca
4631

4732
Formally, a correct formatter `F` must satisfy `F(F(input)) == F(input)` for all values of input. Any formatter which doesn't meet this rule is misbehaving.
4833

49-
## How does `paddedCell()` work?
50-
51-
Spotless now has a special `paddedCell()` mode. If you add it to your format as such:
52-
53-
```gradle
54-
spotless {
55-
format 'cpp', {
56-
...
57-
paddedCell()
58-
}
59-
}
60-
```
61-
62-
then it will run in the following way:
63-
64-
- When you call `spotlessApply`, it will automatically check for a ping-pong condition.
65-
- If there is a ping-pong condition, it will resolve the ambiguity arbitrarily, but consistently
66-
- It will also warn that `filename such-and-such cycles between 2 steps`.
67-
68-
## How is the ambiguity resolved?
34+
## How spotless fixes this automatically
6935

7036
This is easiest to show in an example:
7137

lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ public static class Builder {
4848
private Builder() {}
4949

5050
private String runToFix;
51-
private boolean isPaddedCell;
5251
private Formatter formatter;
5352
private List<File> problemFiles;
5453

@@ -58,8 +57,9 @@ public Builder runToFix(String runToFix) {
5857
return this;
5958
}
6059

60+
@Deprecated
6161
public Builder isPaddedCell(boolean isPaddedCell) {
62-
this.isPaddedCell = isPaddedCell;
62+
System.err.println("PaddedCell is now always on, and cannot be turned off.");
6363
return this;
6464
}
6565

@@ -171,11 +171,7 @@ private static String diff(Builder builder, File file) throws IOException {
171171
String raw = new String(Files.readAllBytes(file.toPath()), builder.formatter.getEncoding());
172172
String rawUnix = LineEnding.toUnix(raw);
173173
String formattedUnix;
174-
if (builder.isPaddedCell) {
175-
formattedUnix = PaddedCell.check(builder.formatter, file, rawUnix).canonical();
176-
} else {
177-
formattedUnix = builder.formatter.compute(rawUnix, file);
178-
}
174+
formattedUnix = PaddedCell.check(builder.formatter, file, rawUnix).canonical();
179175

180176
if (rawUnix.equals(formattedUnix)) {
181177
// the formatting is fine, so it's a line-ending issue

lib/src/main/java/com/diffplug/spotless/PaddedCell.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818
import static com.diffplug.spotless.LibPreconditions.requireElementsNonNull;
1919

2020
import java.io.File;
21+
import java.io.IOException;
22+
import java.io.OutputStream;
2123
import java.nio.file.Files;
24+
import java.nio.file.StandardOpenOption;
2225
import java.util.ArrayList;
26+
import java.util.Arrays;
2327
import java.util.Collections;
2428
import java.util.Comparator;
2529
import java.util.List;
@@ -171,4 +175,93 @@ public String userMessage() {
171175
}
172176
// @formatter:on
173177
}
178+
179+
/**
180+
* Calculates whether the given file is dirty according to a PaddedCell invocation of the given formatter.
181+
* DirtyState includes the clean state of the file, as well as a warning if we were not able to apply the formatter
182+
* due to diverging idempotence.
183+
*/
184+
public static DirtyState calculateDirtyState(Formatter formatter, File file) throws IOException {
185+
Objects.requireNonNull(formatter, "formatter");
186+
Objects.requireNonNull(file, "file");
187+
188+
byte[] rawBytes = Files.readAllBytes(file.toPath());
189+
String raw = new String(rawBytes, formatter.getEncoding());
190+
String rawUnix = LineEnding.toUnix(raw);
191+
192+
// enforce the format
193+
String formattedUnix = formatter.compute(rawUnix, file);
194+
// convert the line endings if necessary
195+
String formatted = formatter.computeLineEndings(formattedUnix, file);
196+
197+
// if F(input) == input, then the formatter is well-behaving and the input is clean
198+
byte[] formattedBytes = formatted.getBytes(formatter.getEncoding());
199+
if (Arrays.equals(rawBytes, formattedBytes)) {
200+
return isClean;
201+
}
202+
203+
// F(input) != input, so we'll do a padded check
204+
String doubleFormattedUnix = formatter.compute(formattedUnix, file);
205+
if (doubleFormattedUnix.equals(formattedUnix)) {
206+
// most dirty files are idempotent-dirty, so this is a quick-short circuit for that common case
207+
return new DirtyState(formattedBytes);
208+
}
209+
210+
PaddedCell cell = PaddedCell.check(formatter, file, rawUnix);
211+
if (!cell.isResolvable()) {
212+
return didNotConverge;
213+
}
214+
215+
// get the canonical bytes
216+
String canonicalUnix = cell.canonical();
217+
String canonical = formatter.computeLineEndings(canonicalUnix, file);
218+
byte[] canonicalBytes = canonical.getBytes(formatter.getEncoding());
219+
if (!Arrays.equals(rawBytes, canonicalBytes)) {
220+
// and write them to disk if needed
221+
return new DirtyState(canonicalBytes);
222+
} else {
223+
return isClean;
224+
}
225+
}
226+
227+
/**
228+
* The clean/dirty state of a single file. Intended use:
229+
* - {@link #isClean()} means that the file is is clean, and there's nothing else to say
230+
* - {@link #isConverged()} means that we were able to determine a clean state
231+
* - once you've tested the above conditions and you know that it's a dirty file with a converged state,
232+
* then you can call {@link #writeCanonicalTo()} to get the canonical form of the given file.
233+
*/
234+
public static class DirtyState {
235+
private final byte[] canonicalBytes;
236+
237+
private DirtyState(byte[] canonicalBytes) {
238+
this.canonicalBytes = canonicalBytes;
239+
}
240+
241+
public boolean isClean() {
242+
return this == isClean;
243+
}
244+
245+
public boolean didNotConverge() {
246+
return this == didNotConverge;
247+
}
248+
249+
private byte[] canonicalBytes() {
250+
if (canonicalBytes == null) {
251+
throw new IllegalStateException("First make sure that `!isClean()` and `!didNotConverge()`");
252+
}
253+
return canonicalBytes;
254+
}
255+
256+
public void writeCanonicalTo(File file) throws IOException {
257+
Files.write(file.toPath(), canonicalBytes(), StandardOpenOption.TRUNCATE_EXISTING);
258+
}
259+
260+
public void writeCanonicalTo(OutputStream out) throws IOException {
261+
out.write(canonicalBytes());
262+
}
263+
}
264+
265+
private static final DirtyState didNotConverge = new DirtyState(null);
266+
private static final DirtyState isClean = new DirtyState(null);
174267
}

lib/src/main/java/com/diffplug/spotless/PaddedCellBulk.java

Lines changed: 11 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@
2222
import java.nio.file.Path;
2323
import java.nio.file.Paths;
2424
import java.nio.file.SimpleFileVisitor;
25-
import java.nio.file.StandardOpenOption;
2625
import java.nio.file.attribute.BasicFileAttributes;
2726
import java.util.ArrayList;
28-
import java.util.Arrays;
2927
import java.util.Collections;
3028
import java.util.List;
3129
import java.util.Locale;
@@ -35,27 +33,8 @@
3533

3634
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3735

38-
/**
39-
* Incorporates the PaddedCell machinery into broader apply / check usage.
40-
*
41-
* Here's the general workflow:
42-
*
43-
* ### Identify that paddedCell is needed
44-
*
45-
* Initially, paddedCell is off. That's the default, and there's no need for users to know about it.
46-
*
47-
* If they encounter a scenario where `spotlessCheck` fails after calling `spotlessApply`, then they would
48-
* justifiably be frustrated. Luckily, every time `spotlessCheck` fails, it passes the failed files to
49-
* {@link #anyMisbehave(Formatter, List)}, which checks to see if any of the rules are causing a cycle
50-
* or some other kind of mischief. If they are, it can give the user a special error message instructing
51-
* them to turn on paddedCell.
52-
*
53-
* ### spotlessCheck with paddedCell on
54-
*
55-
* Spotless check behaves as normal, finding a list of problem files, but then passes that list
56-
* to {@link #check(File, File, Formatter, List)}. If there were no problem files, then `paddedCell`
57-
* is no longer necessary, so users might as well turn it off, so we give that info as a warning.
58-
*/
36+
/** COMPLETELY DEPRECATED, use {@link PaddedCell#canonicalIfDirty(Formatter, File)} instead. */
37+
@Deprecated
5938
public final class PaddedCellBulk {
6039
private static final Logger logger = Logger.getLogger(PaddedCellBulk.class.getName());
6140

@@ -72,11 +51,13 @@ public final class PaddedCellBulk {
7251
* tell the user about a misbehaving rule and alert her to how to enable
7352
* paddedCell mode, with minimal effort.
7453
*/
54+
@Deprecated
7555
public static boolean anyMisbehave(Formatter formatter, List<File> problemFiles) {
7656
return anyMisbehave(formatter, problemFiles, 500);
7757
}
7858

7959
/** Same as {@link #anyMisbehave(Formatter, List)} but with a customizable timeout. */
60+
@Deprecated
8061
public static boolean anyMisbehave(Formatter formatter, List<File> problemFiles, long timeoutMs) {
8162
Objects.requireNonNull(formatter, "formatter");
8263
Objects.requireNonNull(problemFiles, "problemFiles");
@@ -105,6 +86,7 @@ public static boolean anyMisbehave(Formatter formatter, List<File> problemFiles,
10586
* @return A list of files which are failing because of paddedCell problems, but could be fixed. (specifically, the files for which spotlessApply would be effective)
10687
*/
10788
@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
89+
@Deprecated
10890
public static List<File> check(File rootDir, File diagnoseDir, Formatter formatter, List<File> problemFiles) throws IOException {
10991
Objects.requireNonNull(rootDir, "rootDir");
11092
Objects.requireNonNull(diagnoseDir, "diagnoseDir");
@@ -191,47 +173,20 @@ public String getName() {
191173
}
192174

193175
/** Performs the typical spotlessApply, but with PaddedCell handling of misbehaving FormatterSteps. */
176+
@Deprecated
194177
public static void apply(Formatter formatter, File file) throws IOException {
195178
applyAnyChanged(formatter, file);
196179
}
197180

198181
/** Performs the typical spotlessApply, but with PaddedCell handling of misbehaving FormatterSteps. */
182+
@Deprecated
199183
public static boolean applyAnyChanged(Formatter formatter, File file) throws IOException {
200-
Objects.requireNonNull(formatter, "formatter");
201-
Objects.requireNonNull(file, "file");
202-
203-
byte[] rawBytes = Files.readAllBytes(file.toPath());
204-
String raw = new String(rawBytes, formatter.getEncoding());
205-
String rawUnix = LineEnding.toUnix(raw);
206-
207-
// enforce the format
208-
String formattedUnix = formatter.compute(rawUnix, file);
209-
// convert the line endings if necessary
210-
String formatted = formatter.computeLineEndings(formattedUnix, file);
211-
212-
// if F(input) == input, then the formatter is well-behaving and the input is clean
213-
byte[] formattedBytes = formatted.getBytes(formatter.getEncoding());
214-
if (Arrays.equals(rawBytes, formattedBytes)) {
184+
PaddedCell.DirtyState dirtyState = PaddedCell.calculateDirtyState(formatter, file);
185+
if (dirtyState.isClean() || dirtyState.didNotConverge()) {
215186
return false;
216-
}
217-
218-
// F(input) != input, so we'll do a padded check
219-
PaddedCell cell = PaddedCell.check(formatter, file, rawUnix);
220-
if (!cell.isResolvable()) {
221-
// nothing we can do, but check will warn and dump out the divergence path
222-
return false;
223-
}
224-
225-
// get the canonical bytes
226-
String canonicalUnix = cell.canonical();
227-
String canonical = formatter.computeLineEndings(canonicalUnix, file);
228-
byte[] canonicalBytes = canonical.getBytes(formatter.getEncoding());
229-
if (!Arrays.equals(rawBytes, canonicalBytes)) {
230-
// and write them to disk if needed
231-
Files.write(file.toPath(), canonicalBytes, StandardOpenOption.TRUNCATE_EXISTING);
232-
return true;
233187
} else {
234-
return false;
188+
dirtyState.writeCanonicalTo(file);
189+
return true;
235190
}
236191
}
237192

plugin-gradle/CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
44

55
## [Unreleased]
6+
### Changed
7+
* PaddedCell is now always enabled. It is strictly better than non-padded cell, and there is no performance penalty. [See here](https://github.com/diffplug/spotless/pull/560#issuecomment-621752798) for detailed explanation. ([#561](https://github.com/diffplug/spotless/pull/561))
68

79
## [3.28.1] - 2020-04-02
810
### Added

plugin-gradle/README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,6 @@ Configuration for Groovy is similar to [Java](#java). Most java steps, like `li
168168

169169
The groovy formatter's default behavior is to format all `.groovy` and `.java` files found in the Groovy source directories. If you would like to exclude the `.java` files, set the parameter `excludeJava`, or you can set the `target` parameter as described in the [Custom rules](#custom) section.
170170

171-
Due to cyclic ambiguities of groovy formatter results, e.g. for nested closures, the use of [paddedCell()](../PADDEDCELL.md) and/or [Custom rules](#custom) is recommended to bandaid over this third-party formatter problem.
172-
173171
```gradle
174172
apply plugin: 'groovy'
175173
...
@@ -182,7 +180,6 @@ spotless {
182180
groovy {
183181
licenseHeaderFile 'spotless.license.java'
184182
excludeJava() // excludes all Java sources within the Groovy source dirs from formatting
185-
paddedCell() // Avoid cyclic ambiguities
186183
// the Groovy Eclipse formatter extends the Java Eclipse formatter,
187184
// so it formats Java files by default (unless `excludeJava` is used).
188185
greclipse().configFile('greclipse.properties')

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,16 @@ private String formatName() {
6767
throw new IllegalStateException("This format is not contained by any SpotlessExtension.");
6868
}
6969

70-
boolean paddedCell = false;
71-
7270
/** Enables paddedCell mode. @see <a href="https://github.com/diffplug/spotless/blob/master/PADDEDCELL.md">Padded cell</a> */
71+
@Deprecated
7372
public void paddedCell() {
7473
paddedCell(true);
7574
}
7675

7776
/** Enables paddedCell mode. @see <a href="https://github.com/diffplug/spotless/blob/master/PADDEDCELL.md">Padded cell</a> */
77+
@Deprecated
7878
public void paddedCell(boolean paddedCell) {
79-
this.paddedCell = paddedCell;
79+
root.project.getLogger().warn("PaddedCell is now always on, and cannot be turned off.");
8080
}
8181

8282
LineEnding lineEndings;
@@ -593,7 +593,6 @@ public EclipseWtpConfig eclipseWtp(EclipseWtpFormatterStep type, String version)
593593

594594
/** Sets up a format task according to the values in this extension. */
595595
protected void setupTask(SpotlessTask task) {
596-
task.setPaddedCell(paddedCell);
597596
task.setEncoding(getEncoding().name());
598597
task.setExceptionPolicy(exceptionPolicy);
599598
if (targetExclude == null) {

0 commit comments

Comments
 (0)