Skip to content

Commit 26fbd03

Browse files
authored
Start making each FormatterStep roundtrip serializable. (#1945)
2 parents b3c4893 + 93eb099 commit 26fbd03

18 files changed

+498
-125
lines changed

CHANGES.md

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ This document is intended for Spotless developers.
1010
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
1111

1212
## [Unreleased]
13+
### Added
14+
* `FileSignature.Promised` and `JarState.Promised` to facilitate round-trip serialization for the Gradle configuration cache. ([#1945](https://github.com/diffplug/spotless/pull/1945))
15+
### Removed
16+
* **BREAKING** Remove `JarState.getMavenCoordinate(String prefix)`. ([#1945](https://github.com/diffplug/spotless/pull/1945))
1317
### Fixed
1418
* Ignore system git config when running tests ([#1990](https://github.com/diffplug/spotless/issues/1990))
1519

CONTRIBUTING.md

+22-1
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,31 @@ Here's a checklist for creating a new step for Spotless:
9494

9595
- [ ] Class name ends in Step, `SomeNewStep`.
9696
- [ ] Class has a public static method named `create` that returns a `FormatterStep`.
97-
- [ ] Has a test class named `SomeNewStepTest`.
97+
- [ ] Has a test class named `SomeNewStepTest` that uses `StepHarness` or `StepHarnessWithFile` to test the step.
98+
- [ ] Start with `StepHarness.forStep(myStep).supportsRoundTrip(false)`, and then add round trip support as described in the next section.
9899
- [ ] Test class has test methods to verify behavior.
99100
- [ ] Test class has a test method `equality()` which tests equality using `StepEqualityTester` (see existing methods for examples).
100101

102+
### Serialization roundtrip
103+
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:
105+
106+
- `RoundtripState` which must be roundtrip serializable but has no equality constraints
107+
- `FileSignature.Promised` for settings files and `JarState.Promised` for the classpath
108+
- `EqualityState` which will never be reserialized and its serialized form is used for equality / hashCode checks
109+
- `FileSignature` for settings files and `JarState` for the classpath
110+
111+
```java
112+
FormatterStep create(String name,
113+
RoundtripState roundTrip,
114+
SerializedFunction<RoundtripState, EqualityState> equalityFunc,
115+
SerializedFunction<EqualityState, FormatterFunc> formatterFunc)
116+
FormatterStep createLazy(String name,
117+
Supplier<RoundtripState> roundTrip,
118+
SerializedFunction<RoundtripState, EqualityState> equalityFunc,
119+
SerializedFunction<EqualityState, FormatterFunc> formatterFunc)
120+
```
121+
101122
### Third-party dependencies via reflection or compile-only source sets
102123

103124
Most formatters are going to use some kind of third-party jar. Spotless integrates with many formatters, some of which have incompatible transitive dependencies. To address this, we resolve third-party dependencies using [`JarState`](https://github.com/diffplug/spotless/blob/b26f0972b185995d7c6a7aefa726c146d24d9a82/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java#L118). To call methods on the classes in that `JarState`, you can either use reflection or a compile-only source set. See [#524](https://github.com/diffplug/spotless/issues/524) for examples of both approaches.

lib-extra/src/main/java/com/diffplug/spotless/extra/EclipseBasedStepBuilder.java

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2021 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.
@@ -24,7 +24,6 @@
2424
import java.util.ArrayList;
2525
import java.util.List;
2626
import java.util.Objects;
27-
import java.util.Optional;
2827
import java.util.Properties;
2928

3029
import com.diffplug.common.base.Errors;
@@ -187,12 +186,6 @@ public Properties getPreferences() {
187186
return preferences.getProperties();
188187
}
189188

190-
/** Returns first coordinate from sorted set that starts with a given prefix.*/
191-
public Optional<String> getMavenCoordinate(String prefix) {
192-
return jarState.getMavenCoordinates().stream()
193-
.filter(coordinate -> coordinate.startsWith(prefix)).findFirst();
194-
}
195-
196189
/**
197190
* Load class based on the given configuration of JAR provider and Maven coordinates.
198191
* Different class loader instances are provided in the following scenarios:

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

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2021 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.
@@ -33,6 +33,7 @@
3333
import java.util.Locale;
3434
import java.util.Map;
3535

36+
import edu.umd.cs.findbugs.annotations.Nullable;
3637
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3738

3839
/** Computes a signature for any needed files. */
@@ -43,6 +44,8 @@ public final class FileSignature implements Serializable {
4344
* Transient because not needed to uniquely identify a FileSignature instance, and also because
4445
* Gradle only needs this class to be Serializable so it can compare FileSignature instances for
4546
* incremental builds.
47+
*
48+
* We don't want these absolute paths to screw up buildcache keys.
4649
*/
4750
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
4851
private final transient List<File> files;
@@ -93,6 +96,31 @@ private FileSignature(final List<File> files) throws IOException {
9396
}
9497
}
9598

99+
/** A view of `FileSignature` which can be safely roundtripped. */
100+
public static class Promised implements Serializable {
101+
private static final long serialVersionUID = 1L;
102+
private final List<File> files;
103+
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
104+
private transient @Nullable FileSignature cached;
105+
106+
private Promised(List<File> files, @Nullable FileSignature cached) {
107+
this.files = files;
108+
this.cached = cached;
109+
}
110+
111+
public FileSignature get() {
112+
if (cached == null) {
113+
// null when restored via serialization
114+
cached = ThrowingEx.get(() -> new FileSignature(files));
115+
}
116+
return cached;
117+
}
118+
}
119+
120+
public Promised asPromise() {
121+
return new Promised(files, this);
122+
}
123+
96124
/** Returns all of the files in this signature, throwing an exception if there are more or less than 1 file. */
97125
public Collection<File> files() {
98126
return Collections.unmodifiableList(files);

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

+3-1
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.
@@ -304,6 +304,8 @@ public void close() {
304304
for (FormatterStep step : steps) {
305305
if (step instanceof FormatterStepImpl.Standard) {
306306
((FormatterStepImpl.Standard) step).cleanupFormatterFunc();
307+
} else if (step instanceof FormatterStepEqualityOnStateSerialization) {
308+
((FormatterStepEqualityOnStateSerialization) step).cleanupFormatterFunc();
307309
}
308310
}
309311
}

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

+56-10
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.
@@ -29,7 +29,7 @@
2929
*/
3030
public interface FormatterStep extends Serializable {
3131
/** The name of the step, for debugging purposes. */
32-
public String getName();
32+
String getName();
3333

3434
/**
3535
* Returns a formatted version of the given content.
@@ -43,7 +43,8 @@ public interface FormatterStep extends Serializable {
4343
* if the formatter step doesn't have any changes to make
4444
* @throws Exception if the formatter step experiences a problem
4545
*/
46-
public @Nullable String format(String rawUnix, File file) throws Exception;
46+
@Nullable
47+
String format(String rawUnix, File file) throws Exception;
4748

4849
/**
4950
* Returns a new FormatterStep which will only apply its changes
@@ -54,7 +55,7 @@ public interface FormatterStep extends Serializable {
5455
* @return FormatterStep
5556
*/
5657
@Deprecated
57-
public default FormatterStep filterByContentPattern(String contentPattern) {
58+
default FormatterStep filterByContentPattern(String contentPattern) {
5859
return filterByContent(OnMatch.INCLUDE, contentPattern);
5960
}
6061

@@ -68,7 +69,7 @@ public default FormatterStep filterByContentPattern(String contentPattern) {
6869
* java regular expression used to filter in or out files which content contain pattern
6970
* @return FormatterStep
7071
*/
71-
public default FormatterStep filterByContent(OnMatch onMatch, String contentPattern) {
72+
default FormatterStep filterByContent(OnMatch onMatch, String contentPattern) {
7273
return new FilterByContentPatternFormatterStep(this, onMatch, contentPattern);
7374
}
7475

@@ -78,7 +79,7 @@ public default FormatterStep filterByContent(OnMatch onMatch, String contentPatt
7879
* <p>
7980
* The provided filter must be serializable.
8081
*/
81-
public default FormatterStep filterByFile(SerializableFileFilter filter) {
82+
default FormatterStep filterByFile(SerializableFileFilter filter) {
8283
return new FilterByFileFormatterStep(this, filter);
8384
}
8485

@@ -104,6 +105,51 @@ public final String format(String rawUnix, File file) throws Exception {
104105
}
105106
}
106107

108+
/**
109+
* @param name
110+
* The name of the formatter step.
111+
* @param roundtripInit
112+
* If the step has any state, this supplier will calculate it lazily. The supplier doesn't
113+
* have to be serializable, but the result it calculates needs to be serializable.
114+
* @param equalityFunc
115+
* A pure serializable function (method reference recommended) which takes the result of `roundtripInit`,
116+
* and returns a serializable object whose serialized representation will be used for `.equals` and
117+
* `.hashCode` of the FormatterStep.
118+
* @param formatterFunc
119+
* A pure serializable function (method reference recommended) which takes the result of `equalityFunc`,
120+
* and returns a `FormatterFunc` which will be used for the actual formatting.
121+
* @return A FormatterStep which can be losslessly roundtripped through the java serialization machinery.
122+
*/
123+
static <RoundtripState extends Serializable, EqualityState extends Serializable> FormatterStep createLazy(
124+
String name,
125+
ThrowingEx.Supplier<RoundtripState> roundtripInit,
126+
SerializedFunction<RoundtripState, EqualityState> equalityFunc,
127+
SerializedFunction<EqualityState, FormatterFunc> formatterFunc) {
128+
return new FormatterStepSerializationRoundtrip<>(name, roundtripInit, equalityFunc, formatterFunc);
129+
}
130+
131+
/**
132+
* @param name
133+
* The name of the formatter step.
134+
* @param roundTrip
135+
* The roundtrip serializable state of the step.
136+
* @param equalityFunc
137+
* A pure serializable function (method reference recommended) which takes the result of `roundTrip`,
138+
* and returns a serializable object whose serialized representation will be used for `.equals` and
139+
* `.hashCode` of the FormatterStep.
140+
* @param formatterFunc
141+
* A pure serializable function (method reference recommended) which takes the result of `equalityFunc`,
142+
* and returns a `FormatterFunc` which will be used for the actual formatting.
143+
* @return A FormatterStep which can be losslessly roundtripped through the java serialization machinery.
144+
*/
145+
static <RoundtripState extends Serializable, EqualityState extends Serializable> FormatterStep create(
146+
String name,
147+
RoundtripState roundTrip,
148+
SerializedFunction<RoundtripState, EqualityState> equalityFunc,
149+
SerializedFunction<EqualityState, FormatterFunc> formatterFunc) {
150+
return createLazy(name, () -> roundTrip, equalityFunc, formatterFunc);
151+
}
152+
107153
/**
108154
* @param name
109155
* The name of the formatter step
@@ -115,7 +161,7 @@ public final String format(String rawUnix, File file) throws Exception {
115161
* only the state supplied by state and nowhere else.
116162
* @return A FormatterStep
117163
*/
118-
public static <State extends Serializable> FormatterStep createLazy(
164+
static <State extends Serializable> FormatterStep createLazy(
119165
String name,
120166
ThrowingEx.Supplier<State> stateSupplier,
121167
ThrowingEx.Function<State, FormatterFunc> stateToFormatter) {
@@ -132,7 +178,7 @@ public static <State extends Serializable> FormatterStep createLazy(
132178
* only the state supplied by state and nowhere else.
133179
* @return A FormatterStep
134180
*/
135-
public static <State extends Serializable> FormatterStep create(
181+
static <State extends Serializable> FormatterStep create(
136182
String name,
137183
State state,
138184
ThrowingEx.Function<State, FormatterFunc> stateToFormatter) {
@@ -149,7 +195,7 @@ public static <State extends Serializable> FormatterStep create(
149195
* @return A FormatterStep which will never report that it is up-to-date, because
150196
* it is not equal to the serialized representation of itself.
151197
*/
152-
public static FormatterStep createNeverUpToDateLazy(
198+
static FormatterStep createNeverUpToDateLazy(
153199
String name,
154200
ThrowingEx.Supplier<FormatterFunc> functionSupplier) {
155201
return new FormatterStepImpl.NeverUpToDate(name, functionSupplier);
@@ -163,7 +209,7 @@ public static FormatterStep createNeverUpToDateLazy(
163209
* @return A FormatterStep which will never report that it is up-to-date, because
164210
* it is not equal to the serialized representation of itself.
165211
*/
166-
public static FormatterStep createNeverUpToDate(
212+
static FormatterStep createNeverUpToDate(
167213
String name,
168214
FormatterFunc function) {
169215
Objects.requireNonNull(function, "function");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2016-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.Serializable;
20+
import java.util.Arrays;
21+
22+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
23+
24+
/**
25+
* Standard implementation of FormatterStep which cleanly enforces
26+
* separation of a lazily computed "state" object whose serialized form
27+
* is used as the basis for equality and hashCode, which is separate
28+
* from the serialized form of the step itself, which can include absolute paths
29+
* and such without interfering with buildcache keys.
30+
*/
31+
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
32+
abstract class FormatterStepEqualityOnStateSerialization<State extends Serializable> implements FormatterStep, Serializable {
33+
private static final long serialVersionUID = 1L;
34+
35+
protected abstract State stateSupplier() throws Exception;
36+
37+
protected abstract FormatterFunc stateToFormatter(State state) throws Exception;
38+
39+
private transient FormatterFunc formatter;
40+
private transient State stateInternal;
41+
private transient byte[] serializedStateInternal;
42+
43+
@Override
44+
public String format(String rawUnix, File file) throws Exception {
45+
if (formatter == null) {
46+
formatter = stateToFormatter(state());
47+
}
48+
return formatter.apply(rawUnix, file);
49+
}
50+
51+
@Override
52+
public boolean equals(Object o) {
53+
if (o == null) {
54+
return false;
55+
} else if (getClass() != o.getClass()) {
56+
return false;
57+
} else {
58+
return Arrays.equals(serializedState(), ((FormatterStepEqualityOnStateSerialization<?>) o).serializedState());
59+
}
60+
}
61+
62+
@Override
63+
public int hashCode() {
64+
return Arrays.hashCode(serializedState());
65+
}
66+
67+
void cleanupFormatterFunc() {
68+
if (formatter instanceof FormatterFunc.Closeable) {
69+
((FormatterFunc.Closeable) formatter).close();
70+
formatter = null;
71+
}
72+
}
73+
74+
private State state() throws Exception {
75+
if (stateInternal == null) {
76+
stateInternal = stateSupplier();
77+
}
78+
return stateInternal;
79+
}
80+
81+
private byte[] serializedState() {
82+
if (serializedStateInternal == null) {
83+
serializedStateInternal = ThrowingEx.get(() -> LazyForwardingEquality.toBytes(state()));
84+
}
85+
return serializedStateInternal;
86+
}
87+
}

0 commit comments

Comments
 (0)