Skip to content

Commit 755c22f

Browse files
authored
Linting - breaking changes to internal API to prepare (#2148)
2 parents 2abd54a + 5822660 commit 755c22f

File tree

28 files changed

+284
-475
lines changed

28 files changed

+284
-475
lines changed

CHANGES.md

+2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
7373
* **BREAKING** Remove `JarState.getMavenCoordinate(String prefix)`. ([#1945](https://github.com/diffplug/spotless/pull/1945))
7474
* **BREAKING** Replace `PipeStepPair` with `FenceStep`. ([#1954](https://github.com/diffplug/spotless/pull/1954))
7575
* **BREAKING** Fully removed `Rome`, use `Biome` instead. ([#2119](https://github.com/diffplug/spotless/pull/2119))
76+
* **BREAKING** Moved `PaddedCell.DirtyState` to its own top-level class with new methods. ([#2148](https://github.com/diffplug/spotless/pull/2148))
77+
* **BREAKING** Removed `isClean`, `applyTo`, and `applyToAndReturnResultIfDirty` from `Formatter` because users should instead use `DirtyState`.
7678

7779
## [2.45.0] - 2024-01-23
7880
### Added

CONTRIBUTING.md

+14-16
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,20 @@ In order to use and combine `FormatterStep`, you first create a `Formatter`, whi
1010

1111
- an encoding
1212
- a list of `FormatterStep`
13-
- a line endings policy (`LineEnding.GIT_ATTRIBUTES` is almost always the best choice)
13+
- a line endings policy (`LineEnding.GIT_ATTRIBUTES_FAST_ALLSAME` is almost always the best choice)
1414

15-
Once you have an instance of `Formatter`, you can call `boolean isClean(File)`, or `void applyTo(File)` to either check or apply formatting to a file. Spotless will then:
15+
Once you have an instance of `Formatter`, you can call `DirtyState.of(Formatter, File)`. Under the hood, Spotless will:
1616

1717
- parse the raw bytes into a String according to the encoding
1818
- normalize its line endings to `\n`
1919
- pass the unix string to each `FormatterStep` one after the other
20+
- check for idempotence problems, and repeatedly apply the steps until the [result is stable](PADDEDCELL.md).
2021
- apply line endings according to the policy
2122

2223
You can also use lower-level methods like `String compute(String unix, File file)` if you'd like to do lower-level processing.
2324

2425
All `FormatterStep` implement `Serializable`, `equals`, and `hashCode`, so build systems that support up-to-date checks can easily and correctly determine if any actions need to be taken.
2526

26-
Spotless also provides `PaddedCell`, which makes it easy to diagnose and correct idempotence problems.
27-
2827
## Project layout
2928

3029
For the folders below in monospace text, they are published on MavenCentral at the coordinate `com.diffplug.spotless:spotless-${FOLDER_NAME}`. The other folders are dev infrastructure.
@@ -39,15 +38,16 @@ For the folders below in monospace text, they are published on MavenCentral at t
3938

4039
## How to add a new FormatterStep
4140

42-
The easiest way to create a FormatterStep is `FormatterStep createNeverUpToDate(String name, FormatterFunc function)`, which you can use like this:
41+
The easiest way to create a FormatterStep is to just create `class FooStep implements FormatterStep`. It has one abstract method which is the formatting function, and you're ready to tinker. To work with the build plugins, this class will need to
4342

44-
```java
45-
FormatterStep identityStep = FormatterStep.createNeverUpToDate("identity", unixStr -> unixStr)
46-
```
43+
- implement equality and hashcode
44+
- support lossless roundtrip serialization
45+
46+
You can use `StepHarness` (if you don't care about the `File` argument) or `StepHarnessWithFile` to test. The harness will roundtrip serialize your step, check that it's equal to itself, and then perform all tests on the roundtripped step.
4747

48-
This creates a step which will fail up-to-date checks (it is equal only to itself), and will use the function you passed in to do the formatting pass.
48+
## Implementing equality in terms of serialization
4949

50-
To create a step which can handle up-to-date checks properly, use the method `<State extends Serializable> FormatterStep create(String name, State state, Function<State, FormatterFunc> stateToFormatter)`. Here's an example:
50+
Spotless has infrastructure which uses the serialized form of your step to implement equality for you. Here is an example:
5151

5252
```java
5353
public final class ReplaceStep {
@@ -62,10 +62,10 @@ public final class ReplaceStep {
6262
private static final class State implements Serializable {
6363
private static final long serialVersionUID = 1L;
6464

65-
private final CharSequence target;
66-
private final CharSequence replacement;
65+
private final String target;
66+
private final String replacement;
6767

68-
State(CharSequence target, CharSequence replacement) {
68+
State(String target, String replacement) {
6969
this.target = target;
7070
this.replacement = replacement;
7171
}
@@ -82,8 +82,6 @@ The `FormatterStep` created above implements `equals` and `hashCode` based on th
8282
Oftentimes, a rule's state will be expensive to compute. `EclipseFormatterStep`, for example, depends on a formatting file. Ideally, we would like to only pay the cost of the I/O needed to load that file if we have to - we'd like to create the FormatterStep now but load its state lazily at the last possible moment. For this purpose, each of the `FormatterStep.create` methods has a lazy counterpart. Here are their signatures:
8383

8484
```java
85-
FormatterStep createNeverUpToDate (String name, FormatterFunc function )
86-
FormatterStep createNeverUpToDateLazy(String name, Supplier<FormatterFunc> functionSupplier)
8785
FormatterStep create (String name, State state , Function<State, FormatterFunc> stateToFormatter)
8886
FormatterStep createLazy(String name, Supplier<State> stateSupplier, Function<State, FormatterFunc> stateToFormatter)
8987
```
@@ -101,7 +99,7 @@ Here's a checklist for creating a new step for Spotless:
10199

102100
### Serialization roundtrip
103101

104-
In order to support Gradle's configuration cache, all `FormatterStep` must be round-trip serializable. This is a bit tricky because step equality is based on the serialized form of the state, and `transient` is used to take absolute paths out of the equality check. To make this work, roundtrip compatible steps actually have *two* states:
102+
In order to support Gradle's configuration cache, all `FormatterStep` must be round-trip serializable. This is a bit tricky because step equality is based on the serialized form of the state, and `transient` can be used to take absolute paths out of the equality check. To make this work, roundtrip compatible steps can actually have *two* states:
105103

106104
- `RoundtripState` which must be roundtrip serializable but has no equality constraints
107105
- `FileSignature.Promised` for settings files and `JarState.Promised` for the classpath

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

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2023 DiffPlug
2+
* Copyright 2016-2024 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,15 +58,17 @@ interface CleanProvider {
5858
}
5959

6060
private static class CleanProviderFormatter implements CleanProvider {
61+
private final Path rootDir;
6162
private final Formatter formatter;
6263

63-
CleanProviderFormatter(Formatter formatter) {
64+
CleanProviderFormatter(Path rootDir, Formatter formatter) {
65+
this.rootDir = Objects.requireNonNull(rootDir);
6466
this.formatter = Objects.requireNonNull(formatter);
6567
}
6668

6769
@Override
6870
public Path getRootDir() {
69-
return formatter.getRootDir();
71+
return rootDir;
7072
}
7173

7274
@Override
@@ -123,8 +125,8 @@ public Builder runToFix(String runToFix) {
123125
return this;
124126
}
125127

126-
public Builder formatter(Formatter formatter) {
127-
this.formatter = new CleanProviderFormatter(formatter);
128+
public Builder formatter(Path rootDir, Formatter formatter) {
129+
this.formatter = new CleanProviderFormatter(rootDir, formatter);
128130
return this;
129131
}
130132

@@ -244,8 +246,8 @@ private String diff(File file) throws IOException {
244246
* look like if formatted using the given formatter. Does not end with any newline
245247
* sequence (\n, \r, \r\n). The key of the map entry is the 0-based line where the first difference occurred.
246248
*/
247-
public static Map.Entry<Integer, String> diff(Formatter formatter, File file) throws IOException {
248-
return diff(new CleanProviderFormatter(formatter), file);
249+
public static Map.Entry<Integer, String> diff(Path rootDir, Formatter formatter, File file) throws IOException {
250+
return diff(new CleanProviderFormatter(rootDir, formatter), file);
249251
}
250252

251253
private static Map.Entry<Integer, String> diff(CleanProvider formatter, File file) throws IOException {

lib-extra/src/test/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStepSpecialCaseTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 DiffPlug
2+
* Copyright 2023-2024 DiffPlug
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2022-2024 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
import java.nio.file.Files;
22+
import java.util.Arrays;
23+
24+
import javax.annotation.Nullable;
25+
26+
/**
27+
* The clean/dirty state of a single file. Intended use:
28+
* - {@link #isClean()} means that the file is is clean, and there's nothing else to say
29+
* - {@link #didNotConverge()} means that we were unable to determine a clean state
30+
* - once you've tested the above conditions and you know that it's a dirty file with a converged state,
31+
* then you can call {@link #writeCanonicalTo(OutputStream)} to get the canonical form of the given file.
32+
*/
33+
public class DirtyState {
34+
@Nullable
35+
private final byte[] canonicalBytes;
36+
37+
DirtyState(@Nullable byte[] canonicalBytes) {
38+
this.canonicalBytes = canonicalBytes;
39+
}
40+
41+
public boolean isClean() {
42+
return this == isClean;
43+
}
44+
45+
public boolean didNotConverge() {
46+
return this == didNotConverge;
47+
}
48+
49+
private byte[] canonicalBytes() {
50+
if (canonicalBytes == null) {
51+
throw new IllegalStateException("First make sure that {@code !isClean()} and {@code !didNotConverge()}");
52+
}
53+
return canonicalBytes;
54+
}
55+
56+
public void writeCanonicalTo(File file) throws IOException {
57+
Files.write(file.toPath(), canonicalBytes());
58+
}
59+
60+
public void writeCanonicalTo(OutputStream out) throws IOException {
61+
out.write(canonicalBytes());
62+
}
63+
64+
/** Returns the DirtyState which corresponds to {@code isClean()}. */
65+
public static DirtyState clean() {
66+
return isClean;
67+
}
68+
69+
static final DirtyState didNotConverge = new DirtyState(null);
70+
static final DirtyState isClean = new DirtyState(null);
71+
72+
public static DirtyState of(Formatter formatter, File file) throws IOException {
73+
return of(formatter, file, Files.readAllBytes(file.toPath()));
74+
}
75+
76+
public static DirtyState of(Formatter formatter, File file, byte[] rawBytes) {
77+
String raw = new String(rawBytes, formatter.getEncoding());
78+
// check that all characters were encodable
79+
String encodingError = EncodingErrorMsg.msg(raw, rawBytes, formatter.getEncoding());
80+
if (encodingError != null) {
81+
throw new IllegalArgumentException(encodingError);
82+
}
83+
84+
String rawUnix = LineEnding.toUnix(raw);
85+
86+
// enforce the format
87+
String formattedUnix = formatter.compute(rawUnix, file);
88+
// convert the line endings if necessary
89+
String formatted = formatter.computeLineEndings(formattedUnix, file);
90+
91+
// if F(input) == input, then the formatter is well-behaving and the input is clean
92+
byte[] formattedBytes = formatted.getBytes(formatter.getEncoding());
93+
if (Arrays.equals(rawBytes, formattedBytes)) {
94+
return isClean;
95+
}
96+
97+
// F(input) != input, so we'll do a padded check
98+
String doubleFormattedUnix = formatter.compute(formattedUnix, file);
99+
if (doubleFormattedUnix.equals(formattedUnix)) {
100+
// most dirty files are idempotent-dirty, so this is a quick-short circuit for that common case
101+
return new DirtyState(formattedBytes);
102+
}
103+
104+
PaddedCell cell = PaddedCell.check(formatter, file, rawUnix);
105+
if (!cell.isResolvable()) {
106+
return didNotConverge;
107+
}
108+
109+
// get the canonical bytes
110+
String canonicalUnix = cell.canonical();
111+
String canonical = formatter.computeLineEndings(canonicalUnix, file);
112+
byte[] canonicalBytes = canonical.getBytes(formatter.getEncoding());
113+
if (!Arrays.equals(rawBytes, canonicalBytes)) {
114+
// and write them to disk if needed
115+
return new DirtyState(canonicalBytes);
116+
} else {
117+
return isClean;
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)