Skip to content

Commit 0937f0f

Browse files
authored
Serializable refactor pipe/fence (#1954)
2 parents 353736d + 62aecad commit 0937f0f

File tree

9 files changed

+421
-41
lines changed

9 files changed

+421
-41
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
1414
* `FileSignature.Promised` and `JarState.Promised` to facilitate round-trip serialization for the Gradle configuration cache. ([#1945](https://github.com/diffplug/spotless/pull/1945))
1515
### Removed
1616
* **BREAKING** Remove `JarState.getMavenCoordinate(String prefix)`. ([#1945](https://github.com/diffplug/spotless/pull/1945))
17+
* **BREAKING** Replace `PipeStepPair` with `FenceStep`. ([#1954](https://github.com/diffplug/spotless/pull/1954))
1718
### Fixed
1819
* Ignore system git config when running tests ([#1990](https://github.com/diffplug/spotless/issues/1990))
1920

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
* Copyright 2020-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.generic;
17+
18+
import java.io.File;
19+
import java.io.Serializable;
20+
import java.nio.charset.StandardCharsets;
21+
import java.nio.file.Path;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
import java.util.Objects;
25+
import java.util.regex.Matcher;
26+
import java.util.regex.Pattern;
27+
28+
import com.diffplug.spotless.Formatter;
29+
import com.diffplug.spotless.FormatterFunc;
30+
import com.diffplug.spotless.FormatterStep;
31+
import com.diffplug.spotless.LineEnding;
32+
import com.diffplug.spotless.SerializedFunction;
33+
34+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
35+
36+
public class FenceStep {
37+
/** Declares the name of the step. */
38+
public static FenceStep named(String name) {
39+
return new FenceStep(name);
40+
}
41+
42+
public static String defaultToggleName() {
43+
return "toggle";
44+
}
45+
46+
public static String defaultToggleOff() {
47+
return "spotless:off";
48+
}
49+
50+
public static String defaultToggleOn() {
51+
return "spotless:on";
52+
}
53+
54+
String name;
55+
Pattern regex;
56+
57+
private FenceStep(String name) {
58+
this.name = Objects.requireNonNull(name);
59+
}
60+
61+
/** Defines the opening and closing markers. */
62+
public FenceStep openClose(String open, String close) {
63+
return regex(Pattern.quote(open) + "([\\s\\S]*?)" + Pattern.quote(close));
64+
}
65+
66+
/** Defines the pipe via regex. Must have *exactly one* capturing group. */
67+
public FenceStep regex(String regex) {
68+
return regex(Pattern.compile(regex));
69+
}
70+
71+
/** Defines the pipe via regex. Must have *exactly one* capturing group. */
72+
public FenceStep regex(Pattern regex) {
73+
this.regex = Objects.requireNonNull(regex);
74+
return this;
75+
}
76+
77+
private void assertRegexSet() {
78+
Objects.requireNonNull(regex, "must call regex() or openClose()");
79+
}
80+
81+
/** Returns a step which will apply the given steps but preserve the content selected by the regex / openClose pair. */
82+
public FormatterStep preserveWithin(List<FormatterStep> steps) {
83+
assertRegexSet();
84+
return FormatterStep.createLazy(name,
85+
() -> new PreserveWithin(regex, steps),
86+
SerializedFunction.identity(),
87+
state -> FormatterFunc.Closeable.of(state.buildFormatter(), state));
88+
}
89+
90+
/**
91+
* Returns a step which will apply the given steps only within the blocks selected by the regex / openClose pair.
92+
* Linting within the substeps is not supported.
93+
*/
94+
public FormatterStep applyWithin(List<FormatterStep> steps) {
95+
assertRegexSet();
96+
return FormatterStep.createLazy(name,
97+
() -> new ApplyWithin(regex, steps),
98+
SerializedFunction.identity(),
99+
state -> FormatterFunc.Closeable.of(state.buildFormatter(), state));
100+
}
101+
102+
static class ApplyWithin extends Apply implements FormatterFunc.Closeable.ResourceFuncNeedsFile<Formatter> {
103+
private static final long serialVersionUID = 17061466531957339L;
104+
105+
ApplyWithin(Pattern regex, List<FormatterStep> steps) {
106+
super(regex, steps);
107+
}
108+
109+
@Override
110+
public String apply(Formatter formatter, String unix, File file) throws Exception {
111+
List<String> groups = groupsZeroed();
112+
Matcher matcher = regex.matcher(unix);
113+
while (matcher.find()) {
114+
// apply the formatter to each group
115+
groups.add(formatter.compute(matcher.group(1), file));
116+
}
117+
// and then assemble the result right away
118+
return assembleGroups(unix);
119+
}
120+
}
121+
122+
static class PreserveWithin extends Apply implements FormatterFunc.Closeable.ResourceFuncNeedsFile<Formatter> {
123+
private static final long serialVersionUID = -8676786492305178343L;
124+
125+
PreserveWithin(Pattern regex, List<FormatterStep> steps) {
126+
super(regex, steps);
127+
}
128+
129+
private void storeGroups(String unix) {
130+
List<String> groups = groupsZeroed();
131+
Matcher matcher = regex.matcher(unix);
132+
while (matcher.find()) {
133+
// store whatever is within the open/close tags
134+
groups.add(matcher.group(1));
135+
}
136+
}
137+
138+
@Override
139+
public String apply(Formatter formatter, String unix, File file) throws Exception {
140+
storeGroups(unix);
141+
String formatted = formatter.compute(unix, file);
142+
return assembleGroups(formatted);
143+
}
144+
}
145+
146+
@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
147+
static class Apply implements Serializable {
148+
private static final long serialVersionUID = -2301848328356559915L;
149+
final Pattern regex;
150+
final List<FormatterStep> steps;
151+
152+
transient ArrayList<String> groups = new ArrayList<>();
153+
transient StringBuilder builderInternal;
154+
155+
public Apply(Pattern regex, List<FormatterStep> steps) {
156+
this.regex = regex;
157+
this.steps = steps;
158+
}
159+
160+
protected ArrayList<String> groupsZeroed() {
161+
if (groups == null) {
162+
groups = new ArrayList<>();
163+
} else {
164+
groups.clear();
165+
}
166+
return groups;
167+
}
168+
169+
private StringBuilder builderZeroed() {
170+
if (builderInternal == null) {
171+
builderInternal = new StringBuilder();
172+
} else {
173+
builderInternal.setLength(0);
174+
}
175+
return builderInternal;
176+
}
177+
178+
protected Formatter buildFormatter() {
179+
return Formatter.builder()
180+
.encoding(StandardCharsets.UTF_8) // can be any UTF, doesn't matter
181+
.lineEndingsPolicy(LineEnding.UNIX.createPolicy()) // just internal, won't conflict with user
182+
.steps(steps)
183+
.rootDir(Path.of("")) // TODO: error messages will be suboptimal for now, but it will get fixed when we ship linting
184+
.build();
185+
}
186+
187+
protected String assembleGroups(String unix) {
188+
if (groups.isEmpty()) {
189+
return unix;
190+
}
191+
StringBuilder builder = builderZeroed();
192+
Matcher matcher = regex.matcher(unix);
193+
int lastEnd = 0;
194+
int groupIdx = 0;
195+
while (matcher.find()) {
196+
builder.append(unix, lastEnd, matcher.start(1));
197+
builder.append(groups.get(groupIdx));
198+
lastEnd = matcher.end(1);
199+
++groupIdx;
200+
}
201+
if (groupIdx == groups.size()) {
202+
builder.append(unix, lastEnd, unix.length());
203+
return builder.toString();
204+
} else {
205+
// these will be needed to generate Lints later on
206+
// int startLine = 1 + (int) builder.toString().codePoints().filter(c -> c == '\n').count();
207+
// int endLine = 1 + (int) unix.codePoints().filter(c -> c == '\n').count();
208+
209+
// throw an error with either the full regex, or the nicer open/close pair
210+
Matcher openClose = Pattern.compile("\\\\Q([\\s\\S]*?)\\\\E" + "\\Q([\\s\\S]*?)\\E" + "\\\\Q([\\s\\S]*?)\\\\E")
211+
.matcher(regex.pattern());
212+
String pattern;
213+
if (openClose.matches()) {
214+
pattern = openClose.group(1) + " " + openClose.group(2);
215+
} else {
216+
pattern = regex.pattern();
217+
}
218+
throw new Error("An intermediate step removed a match of " + pattern);
219+
}
220+
}
221+
}
222+
}

lib/src/main/java/com/diffplug/spotless/generic/PipeStepPair.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2021 DiffPlug
2+
* Copyright 2020-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,10 @@
3333

3434
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
3535

36+
/**
37+
* @deprecated use FenceStep instead
38+
*/
39+
@Deprecated
3640
public class PipeStepPair {
3741
/** The two steps will be named {@code <name>In} and {@code <name>Out}. */
3842
public static Builder named(String name) {

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

Lines changed: 13 additions & 17 deletions
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.
@@ -57,11 +57,11 @@
5757
import com.diffplug.spotless.extra.EclipseBasedStepBuilder;
5858
import com.diffplug.spotless.extra.wtp.EclipseWtpFormatterStep;
5959
import com.diffplug.spotless.generic.EndWithNewlineStep;
60+
import com.diffplug.spotless.generic.FenceStep;
6061
import com.diffplug.spotless.generic.IndentStep;
6162
import com.diffplug.spotless.generic.LicenseHeaderStep;
6263
import com.diffplug.spotless.generic.LicenseHeaderStep.YearMode;
6364
import com.diffplug.spotless.generic.NativeCmdStep;
64-
import com.diffplug.spotless.generic.PipeStepPair;
6565
import com.diffplug.spotless.generic.ReplaceRegexStep;
6666
import com.diffplug.spotless.generic.ReplaceStep;
6767
import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep;
@@ -989,7 +989,7 @@ public void withinBlocks(String name, String open, String close, Action<FormatEx
989989
*/
990990
public <T extends FormatExtension> void withinBlocks(String name, String open, String close, Class<T> clazz,
991991
Action<T> configure) {
992-
withinBlocksHelper(PipeStepPair.named(name).openClose(open, close), clazz, configure);
992+
withinBlocksHelper(FenceStep.named(name).openClose(open, close), clazz, configure);
993993
}
994994

995995
/**
@@ -1007,18 +1007,17 @@ public void withinBlocksRegex(String name, String regex, Action<FormatExtension>
10071007
*/
10081008
public <T extends FormatExtension> void withinBlocksRegex(String name, String regex, Class<T> clazz,
10091009
Action<T> configure) {
1010-
withinBlocksHelper(PipeStepPair.named(name).regex(regex), clazz, configure);
1010+
withinBlocksHelper(FenceStep.named(name).regex(regex), clazz, configure);
10111011
}
10121012

1013-
private <T extends FormatExtension> void withinBlocksHelper(PipeStepPair.Builder builder, Class<T> clazz,
1013+
private <T extends FormatExtension> void withinBlocksHelper(FenceStep fence, Class<T> clazz,
10141014
Action<T> configure) {
10151015
// create the sub-extension
10161016
T formatExtension = spotless.instantiateFormatExtension(clazz);
10171017
// configure it
10181018
configure.execute(formatExtension);
10191019
// create a step which applies all of those steps as sub-steps
1020-
FormatterStep step = builder.buildStepWhichAppliesSubSteps(spotless.project.getRootDir().toPath(),
1021-
formatExtension.steps);
1020+
FormatterStep step = fence.applyWithin(formatExtension.steps);
10221021
addStep(step);
10231022
}
10241023

@@ -1027,28 +1026,28 @@ private <T extends FormatExtension> void withinBlocksHelper(PipeStepPair.Builder
10271026
* that captured group.
10281027
*/
10291028
public void toggleOffOnRegex(String regex) {
1030-
this.togglePair = PipeStepPair.named(PipeStepPair.defaultToggleName()).regex(regex).buildPair();
1029+
this.toggleFence = FenceStep.named(FenceStep.defaultToggleName()).regex(regex);
10311030
}
10321031

10331032
/** Disables formatting between the given tags. */
10341033
public void toggleOffOn(String off, String on) {
1035-
this.togglePair = PipeStepPair.named(PipeStepPair.defaultToggleName()).openClose(off, on).buildPair();
1034+
this.toggleFence = FenceStep.named(FenceStep.defaultToggleName()).openClose(off, on);
10361035
}
10371036

10381037
/** Disables formatting between {@code spotless:off} and {@code spotless:on}. */
10391038
public void toggleOffOn() {
1040-
toggleOffOn(PipeStepPair.defaultToggleOff(), PipeStepPair.defaultToggleOn());
1039+
toggleOffOn(FenceStep.defaultToggleOff(), FenceStep.defaultToggleOn());
10411040
}
10421041

10431042
/**
10441043
* Undoes all previous calls to {@link #toggleOffOn()} and
10451044
* {@link #toggleOffOn(String, String)}.
10461045
*/
10471046
public void toggleOffOnDisable() {
1048-
this.togglePair = null;
1047+
this.toggleFence = null;
10491048
}
10501049

1051-
private @Nullable PipeStepPair togglePair;
1050+
private @Nullable FenceStep toggleFence;
10521051

10531052
/** Sets up a format task according to the values in this extension. */
10541053
protected void setupTask(SpotlessTask task) {
@@ -1057,11 +1056,8 @@ protected void setupTask(SpotlessTask task) {
10571056
FileCollection totalTarget = targetExclude == null ? target : target.minus(targetExclude);
10581057
task.setTarget(totalTarget);
10591058
List<FormatterStep> steps;
1060-
if (togglePair != null) {
1061-
steps = new ArrayList<>(this.steps.size() + 2);
1062-
steps.add(togglePair.in());
1063-
steps.addAll(this.steps);
1064-
steps.add(togglePair.out());
1059+
if (toggleFence != null) {
1060+
steps = List.of(toggleFence.preserveWithin(this.steps));
10651061
} else {
10661062
steps = this.steps;
10671063
}

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

Lines changed: 3 additions & 5 deletions
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.
@@ -36,7 +36,6 @@
3636
import com.diffplug.spotless.Formatter;
3737
import com.diffplug.spotless.FormatterStep;
3838
import com.diffplug.spotless.LineEnding;
39-
import com.diffplug.spotless.generic.PipeStepPair;
4039
import com.diffplug.spotless.maven.generic.EclipseWtp;
4140
import com.diffplug.spotless.maven.generic.EndWithNewline;
4241
import com.diffplug.spotless.maven.generic.Indent;
@@ -97,9 +96,8 @@ public final Formatter newFormatter(Supplier<Iterable<File>> filesToFormat, Form
9796
.map(factory -> factory.newFormatterStep(stepConfig))
9897
.collect(Collectors.toCollection(() -> new ArrayList<FormatterStep>()));
9998
if (toggle != null) {
100-
PipeStepPair pair = toggle.createPair();
101-
formatterSteps.add(0, pair.in());
102-
formatterSteps.add(pair.out());
99+
List<FormatterStep> formatterStepsBeforeToggle = formatterSteps;
100+
formatterSteps = List.of(toggle.createFence().preserveWithin(formatterStepsBeforeToggle));
103101
}
104102

105103
String formatterName = this.getClass().getSimpleName();

0 commit comments

Comments
 (0)