Skip to content

Commit c188741

Browse files
committed
Add FenceStep as it is after we do the lint refactor.
1 parent 0257d0b commit c188741

File tree

2 files changed

+333
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)