Skip to content

Commit c2fe9c6

Browse files
committed
Merge branch 'feat/prepare-for-lint-take-2' into feat/lint-take-2
2 parents ffc911e + 33444a7 commit c2fe9c6

File tree

8 files changed

+59
-94
lines changed

8 files changed

+59
-94
lines changed

CHANGES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
3434
* **BREAKING** Remove `JarState.getMavenCoordinate(String prefix)`. ([#1945](https://github.com/diffplug/spotless/pull/1945))
3535
* **BREAKING** Replace `PipeStepPair` with `FenceStep`. ([#1954](https://github.com/diffplug/spotless/pull/1954))
3636
* **BREAKING** Fully removed `Rome`, use `Biome` instead. ([#2119](https://github.com/diffplug/spotless/pull/2119))
37-
* **BREAKING** Moved `PaddedCell.DirtyState` to its own top-level class with new methods.
38-
* **BREAKING** Removed `isClean`, `applyTo`, and `applyToAndReturnResultIfDirty` from `Formatter` because users should instead use `PaddedCell.check()`.
37+
* **BREAKING** Moved `PaddedCell.DirtyState` to its own top-level class with new methods. ([#2148](https://github.com/diffplug/spotless/pull/2148))
38+
* **BREAKING** Removed `isClean`, `applyTo`, and `applyToAndReturnResultIfDirty` from `Formatter` because users should instead use `DirtyState`.
3939

4040
## [2.45.0] - 2024-01-23
4141
### Added

CONTRIBUTING.md

Lines changed: 14 additions & 16 deletions
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/src/main/java/com/diffplug/spotless/DirtyState.java

Lines changed: 38 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -69,85 +69,52 @@ public static DirtyState clean() {
6969
static final DirtyState didNotConverge = new DirtyState(null);
7070
static final DirtyState isClean = new DirtyState(null);
7171

72-
public static Calculation of(Formatter formatter, File file) throws IOException {
72+
public static DirtyState of(Formatter formatter, File file) throws IOException {
7373
return of(formatter, file, Files.readAllBytes(file.toPath()));
7474
}
7575

76-
public static Calculation of(Formatter formatter, File file, byte[] rawBytes) {
77-
return new Calculation(formatter, file, rawBytes);
78-
}
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+
}
7996

80-
public static class Calculation {
81-
private final Formatter formatter;
82-
private final File file;
83-
private final byte[] rawBytes;
84-
final String raw;
85-
86-
private Calculation(Formatter formatter, File file, byte[] rawBytes) {
87-
this.formatter = formatter;
88-
this.file = file;
89-
this.rawBytes = rawBytes;
90-
this.raw = new String(rawBytes, formatter.getEncoding());
91-
// check that all characters were encodable
92-
String encodingError = EncodingErrorMsg.msg(raw, rawBytes, formatter.getEncoding());
93-
if (encodingError != null) {
94-
throw new IllegalArgumentException(encodingError);
95-
}
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);
96102
}
97103

98-
/**
99-
* Calculates whether the given file is dirty according to a PaddedCell invocation of the given formatter.
100-
* DirtyState includes the clean state of the file, as well as a warning if we were not able to apply the formatter
101-
* due to diverging idempotence.
102-
*/
103-
public DirtyState calculateDirtyState() {
104-
ValuePerStep<Throwable> exceptionPerStep = new ValuePerStep<>(formatter);
105-
DirtyState result = calculateDirtyState(exceptionPerStep);
106-
LintPolicy.legacyBehavior(formatter, file, exceptionPerStep);
107-
return result;
104+
PaddedCell cell = PaddedCell.check(formatter, file, rawUnix);
105+
if (!cell.isResolvable()) {
106+
return didNotConverge;
108107
}
109108

110-
/**
111-
* Calculates whether the given file is dirty according to a PaddedCell invocation of the given formatter.
112-
* DirtyState includes the clean state of the file, as well as a warning if we were not able to apply the formatter
113-
* due to diverging idempotence.
114-
*/
115-
DirtyState calculateDirtyState(ValuePerStep<Throwable> exceptionPerStep) {
116-
String rawUnix = LineEnding.toUnix(raw);
117-
118-
// enforce the format
119-
String formattedUnix = formatter.computeWithLint(rawUnix, file, exceptionPerStep);
120-
// convert the line endings if necessary
121-
String formatted = formatter.computeLineEndings(formattedUnix, file);
122-
123-
// if F(input) == input, then the formatter is well-behaving and the input is clean
124-
byte[] formattedBytes = formatted.getBytes(formatter.getEncoding());
125-
if (Arrays.equals(rawBytes, formattedBytes)) {
126-
return isClean;
127-
}
128-
129-
// F(input) != input, so we'll do a padded check
130-
String doubleFormattedUnix = formatter.computeWithLint(formattedUnix, file, exceptionPerStep);
131-
if (doubleFormattedUnix.equals(formattedUnix)) {
132-
// most dirty files are idempotent-dirty, so this is a quick-short circuit for that common case
133-
return new DirtyState(formattedBytes);
134-
}
135-
136-
PaddedCell cell = PaddedCell.check(formatter, file, rawUnix, exceptionPerStep);
137-
if (!cell.isResolvable()) {
138-
return didNotConverge;
139-
}
140-
141-
// get the canonical bytes
142-
String canonicalUnix = cell.canonical();
143-
String canonical = formatter.computeLineEndings(canonicalUnix, file);
144-
byte[] canonicalBytes = canonical.getBytes(formatter.getEncoding());
145-
if (!Arrays.equals(rawBytes, canonicalBytes)) {
146-
// and write them to disk if needed
147-
return new DirtyState(canonicalBytes);
148-
} else {
149-
return isClean;
150-
}
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;
151118
}
152119
}
153120
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ static void performHook(SpotlessTaskImpl spotlessTask) {
5555
} else {
5656
bytes = Files.readAllBytes(file.toPath());
5757
}
58-
DirtyState dirty = DirtyState.of(formatter, file, bytes).calculateDirtyState();
58+
DirtyState dirty = DirtyState.of(formatter, file, bytes);
5959
if (dirty.isClean()) {
6060
dumpIsClean();
6161
} else if (dirty.didNotConverge()) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ void processInputFile(@Nullable GitRatchet ratchet, Formatter formatter, File in
102102
dirtyState = DirtyState.clean();
103103
} else {
104104
try {
105-
dirtyState = DirtyState.of(formatter, input).calculateDirtyState();
105+
dirtyState = DirtyState.of(formatter, input);
106106
} catch (IOException e) {
107107
throw new IOException("Issue processing file: " + input, e);
108108
} catch (RuntimeException e) {

plugin-maven/src/main/java/com/diffplug/spotless/maven/IdeHook.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ static void performHook(Iterable<File> projectFiles, Formatter formatter, String
4949
} else {
5050
bytes = Files.readAllBytes(file.toPath());
5151
}
52-
DirtyState dirty = DirtyState.of(formatter, file, bytes).calculateDirtyState();
52+
DirtyState dirty = DirtyState.of(formatter, file, bytes);
5353
if (dirty.isClean()) {
5454
dumpIsClean();
5555
} else if (dirty.didNotConverge()) {

plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ protected void process(String name, Iterable<File> files, Formatter formatter, U
6060
}
6161

6262
try {
63-
DirtyState dirtyState = DirtyState.of(formatter, file).calculateDirtyState();
63+
DirtyState dirtyState = DirtyState.of(formatter, file);
6464
if (!dirtyState.isClean() && !dirtyState.didNotConverge()) {
6565
getLog().info(String.format("Writing clean file: %s", file));
6666
dirtyState.writeCanonicalTo(file);

plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ protected void process(String name, Iterable<File> files, Formatter formatter, U
7878
}
7979
buildContext.removeMessages(file);
8080
try {
81-
DirtyState dirtyState = DirtyState.of(formatter, file).calculateDirtyState();
81+
DirtyState dirtyState = DirtyState.of(formatter, file);
8282
if (!dirtyState.isClean() && !dirtyState.didNotConverge()) {
8383
problemFiles.add(file);
8484
if (buildContext.isIncremental()) {

0 commit comments

Comments
 (0)