Skip to content

Commit

Permalink
Serializable refactor pipe/fence (#1954)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Jan 24, 2024
2 parents 353736d + 62aecad commit 0937f0f
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
* `FileSignature.Promised` and `JarState.Promised` to facilitate round-trip serialization for the Gradle configuration cache. ([#1945](https://github.com/diffplug/spotless/pull/1945))
### Removed
* **BREAKING** Remove `JarState.getMavenCoordinate(String prefix)`. ([#1945](https://github.com/diffplug/spotless/pull/1945))
* **BREAKING** Replace `PipeStepPair` with `FenceStep`. ([#1954](https://github.com/diffplug/spotless/pull/1954))
### Fixed
* Ignore system git config when running tests ([#1990](https://github.com/diffplug/spotless/issues/1990))

Expand Down
222 changes: 222 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/generic/FenceStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/*
* Copyright 2020-2024 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.spotless.generic;

import java.io.File;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.diffplug.spotless.Formatter;
import com.diffplug.spotless.FormatterFunc;
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.LineEnding;
import com.diffplug.spotless.SerializedFunction;

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

public class FenceStep {
/** Declares the name of the step. */
public static FenceStep named(String name) {
return new FenceStep(name);
}

public static String defaultToggleName() {
return "toggle";
}

public static String defaultToggleOff() {
return "spotless:off";
}

public static String defaultToggleOn() {
return "spotless:on";
}

String name;
Pattern regex;

private FenceStep(String name) {
this.name = Objects.requireNonNull(name);
}

/** Defines the opening and closing markers. */
public FenceStep openClose(String open, String close) {
return regex(Pattern.quote(open) + "([\\s\\S]*?)" + Pattern.quote(close));
}

/** Defines the pipe via regex. Must have *exactly one* capturing group. */
public FenceStep regex(String regex) {
return regex(Pattern.compile(regex));
}

/** Defines the pipe via regex. Must have *exactly one* capturing group. */
public FenceStep regex(Pattern regex) {
this.regex = Objects.requireNonNull(regex);
return this;
}

private void assertRegexSet() {
Objects.requireNonNull(regex, "must call regex() or openClose()");
}

/** Returns a step which will apply the given steps but preserve the content selected by the regex / openClose pair. */
public FormatterStep preserveWithin(List<FormatterStep> steps) {
assertRegexSet();
return FormatterStep.createLazy(name,
() -> new PreserveWithin(regex, steps),
SerializedFunction.identity(),
state -> FormatterFunc.Closeable.of(state.buildFormatter(), state));
}

/**
* Returns a step which will apply the given steps only within the blocks selected by the regex / openClose pair.
* Linting within the substeps is not supported.
*/
public FormatterStep applyWithin(List<FormatterStep> steps) {
assertRegexSet();
return FormatterStep.createLazy(name,
() -> new ApplyWithin(regex, steps),
SerializedFunction.identity(),
state -> FormatterFunc.Closeable.of(state.buildFormatter(), state));
}

static class ApplyWithin extends Apply implements FormatterFunc.Closeable.ResourceFuncNeedsFile<Formatter> {
private static final long serialVersionUID = 17061466531957339L;

ApplyWithin(Pattern regex, List<FormatterStep> steps) {
super(regex, steps);
}

@Override
public String apply(Formatter formatter, String unix, File file) throws Exception {
List<String> groups = groupsZeroed();
Matcher matcher = regex.matcher(unix);
while (matcher.find()) {
// apply the formatter to each group
groups.add(formatter.compute(matcher.group(1), file));
}
// and then assemble the result right away
return assembleGroups(unix);
}
}

static class PreserveWithin extends Apply implements FormatterFunc.Closeable.ResourceFuncNeedsFile<Formatter> {
private static final long serialVersionUID = -8676786492305178343L;

PreserveWithin(Pattern regex, List<FormatterStep> steps) {
super(regex, steps);
}

private void storeGroups(String unix) {
List<String> groups = groupsZeroed();
Matcher matcher = regex.matcher(unix);
while (matcher.find()) {
// store whatever is within the open/close tags
groups.add(matcher.group(1));
}
}

@Override
public String apply(Formatter formatter, String unix, File file) throws Exception {
storeGroups(unix);
String formatted = formatter.compute(unix, file);
return assembleGroups(formatted);
}
}

@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED")
static class Apply implements Serializable {
private static final long serialVersionUID = -2301848328356559915L;
final Pattern regex;
final List<FormatterStep> steps;

transient ArrayList<String> groups = new ArrayList<>();
transient StringBuilder builderInternal;

public Apply(Pattern regex, List<FormatterStep> steps) {
this.regex = regex;
this.steps = steps;
}

protected ArrayList<String> groupsZeroed() {
if (groups == null) {
groups = new ArrayList<>();
} else {
groups.clear();
}
return groups;
}

private StringBuilder builderZeroed() {
if (builderInternal == null) {
builderInternal = new StringBuilder();
} else {
builderInternal.setLength(0);
}
return builderInternal;
}

protected Formatter buildFormatter() {
return Formatter.builder()
.encoding(StandardCharsets.UTF_8) // can be any UTF, doesn't matter
.lineEndingsPolicy(LineEnding.UNIX.createPolicy()) // just internal, won't conflict with user
.steps(steps)
.rootDir(Path.of("")) // TODO: error messages will be suboptimal for now, but it will get fixed when we ship linting
.build();
}

protected String assembleGroups(String unix) {
if (groups.isEmpty()) {
return unix;
}
StringBuilder builder = builderZeroed();
Matcher matcher = regex.matcher(unix);
int lastEnd = 0;
int groupIdx = 0;
while (matcher.find()) {
builder.append(unix, lastEnd, matcher.start(1));
builder.append(groups.get(groupIdx));
lastEnd = matcher.end(1);
++groupIdx;
}
if (groupIdx == groups.size()) {
builder.append(unix, lastEnd, unix.length());
return builder.toString();
} else {
// these will be needed to generate Lints later on
// int startLine = 1 + (int) builder.toString().codePoints().filter(c -> c == '\n').count();
// int endLine = 1 + (int) unix.codePoints().filter(c -> c == '\n').count();

// throw an error with either the full regex, or the nicer open/close pair
Matcher openClose = Pattern.compile("\\\\Q([\\s\\S]*?)\\\\E" + "\\Q([\\s\\S]*?)\\E" + "\\\\Q([\\s\\S]*?)\\\\E")
.matcher(regex.pattern());
String pattern;
if (openClose.matches()) {
pattern = openClose.group(1) + " " + openClose.group(2);
} else {
pattern = regex.pattern();
}
throw new Error("An intermediate step removed a match of " + pattern);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2020-2021 DiffPlug
* Copyright 2020-2024 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -33,6 +33,10 @@

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

/**
* @deprecated use FenceStep instead
*/
@Deprecated
public class PipeStepPair {
/** The two steps will be named {@code <name>In} and {@code <name>Out}. */
public static Builder named(String name) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2023 DiffPlug
* Copyright 2016-2024 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -57,11 +57,11 @@
import com.diffplug.spotless.extra.EclipseBasedStepBuilder;
import com.diffplug.spotless.extra.wtp.EclipseWtpFormatterStep;
import com.diffplug.spotless.generic.EndWithNewlineStep;
import com.diffplug.spotless.generic.FenceStep;
import com.diffplug.spotless.generic.IndentStep;
import com.diffplug.spotless.generic.LicenseHeaderStep;
import com.diffplug.spotless.generic.LicenseHeaderStep.YearMode;
import com.diffplug.spotless.generic.NativeCmdStep;
import com.diffplug.spotless.generic.PipeStepPair;
import com.diffplug.spotless.generic.ReplaceRegexStep;
import com.diffplug.spotless.generic.ReplaceStep;
import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep;
Expand Down Expand Up @@ -989,7 +989,7 @@ public void withinBlocks(String name, String open, String close, Action<FormatEx
*/
public <T extends FormatExtension> void withinBlocks(String name, String open, String close, Class<T> clazz,
Action<T> configure) {
withinBlocksHelper(PipeStepPair.named(name).openClose(open, close), clazz, configure);
withinBlocksHelper(FenceStep.named(name).openClose(open, close), clazz, configure);
}

/**
Expand All @@ -1007,18 +1007,17 @@ public void withinBlocksRegex(String name, String regex, Action<FormatExtension>
*/
public <T extends FormatExtension> void withinBlocksRegex(String name, String regex, Class<T> clazz,
Action<T> configure) {
withinBlocksHelper(PipeStepPair.named(name).regex(regex), clazz, configure);
withinBlocksHelper(FenceStep.named(name).regex(regex), clazz, configure);
}

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

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

/** Disables formatting between the given tags. */
public void toggleOffOn(String off, String on) {
this.togglePair = PipeStepPair.named(PipeStepPair.defaultToggleName()).openClose(off, on).buildPair();
this.toggleFence = FenceStep.named(FenceStep.defaultToggleName()).openClose(off, on);
}

/** Disables formatting between {@code spotless:off} and {@code spotless:on}. */
public void toggleOffOn() {
toggleOffOn(PipeStepPair.defaultToggleOff(), PipeStepPair.defaultToggleOn());
toggleOffOn(FenceStep.defaultToggleOff(), FenceStep.defaultToggleOn());
}

/**
* Undoes all previous calls to {@link #toggleOffOn()} and
* {@link #toggleOffOn(String, String)}.
*/
public void toggleOffOnDisable() {
this.togglePair = null;
this.toggleFence = null;
}

private @Nullable PipeStepPair togglePair;
private @Nullable FenceStep toggleFence;

/** Sets up a format task according to the values in this extension. */
protected void setupTask(SpotlessTask task) {
Expand All @@ -1057,11 +1056,8 @@ protected void setupTask(SpotlessTask task) {
FileCollection totalTarget = targetExclude == null ? target : target.minus(targetExclude);
task.setTarget(totalTarget);
List<FormatterStep> steps;
if (togglePair != null) {
steps = new ArrayList<>(this.steps.size() + 2);
steps.add(togglePair.in());
steps.addAll(this.steps);
steps.add(togglePair.out());
if (toggleFence != null) {
steps = List.of(toggleFence.preserveWithin(this.steps));
} else {
steps = this.steps;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2023 DiffPlug
* Copyright 2016-2024 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -36,7 +36,6 @@
import com.diffplug.spotless.Formatter;
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.LineEnding;
import com.diffplug.spotless.generic.PipeStepPair;
import com.diffplug.spotless.maven.generic.EclipseWtp;
import com.diffplug.spotless.maven.generic.EndWithNewline;
import com.diffplug.spotless.maven.generic.Indent;
Expand Down Expand Up @@ -97,9 +96,8 @@ public final Formatter newFormatter(Supplier<Iterable<File>> filesToFormat, Form
.map(factory -> factory.newFormatterStep(stepConfig))
.collect(Collectors.toCollection(() -> new ArrayList<FormatterStep>()));
if (toggle != null) {
PipeStepPair pair = toggle.createPair();
formatterSteps.add(0, pair.in());
formatterSteps.add(pair.out());
List<FormatterStep> formatterStepsBeforeToggle = formatterSteps;
formatterSteps = List.of(toggle.createFence().preserveWithin(formatterStepsBeforeToggle));
}

String formatterName = this.getClass().getSimpleName();
Expand Down
Loading

0 comments on commit 0937f0f

Please sign in to comment.