diff --git a/lib/src/main/java/com/diffplug/spotless/FileSignature.java b/lib/src/main/java/com/diffplug/spotless/FileSignature.java index 10bba7a716..e5d495b9bd 100644 --- a/lib/src/main/java/com/diffplug/spotless/FileSignature.java +++ b/lib/src/main/java/com/diffplug/spotless/FileSignature.java @@ -125,6 +125,10 @@ public static Promised promise(Iterable files) { return new Promised(MoreIterables.toNullHostileList(files), null); } + public static Promised promise(File file) { + return new Promised(MoreIterables.toNullHostileList(Collections.singletonList(file)), null); + } + /** Returns all of the files in this signature, throwing an exception if there are more or less than 1 file. */ public Collection files() { return Collections.unmodifiableList(files); diff --git a/lib/src/main/java/com/diffplug/spotless/Formatter.java b/lib/src/main/java/com/diffplug/spotless/Formatter.java index 9400989ddf..b5cd5f81fc 100644 --- a/lib/src/main/java/com/diffplug/spotless/Formatter.java +++ b/lib/src/main/java/com/diffplug/spotless/Formatter.java @@ -312,4 +312,10 @@ public void close() { /** This Sentinel reference may be used to pass string content to a Formatter or FormatterStep when there is no actual File to format */ public static final File NO_FILE_SENTINEL = new File("NO_FILE_SENTINEL"); + + static void checkNotSentinel(File file) { + if (file == Formatter.NO_FILE_SENTINEL) { + throw new IllegalArgumentException("This step requires the underlying file. If this is a test, use StepHarnessWithFile"); + } + } } diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java index 7aee828b0d..800a553225 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java @@ -115,7 +115,7 @@ public void close() { @Override public String apply(String unix, File file) throws Exception { - FormatterStepImpl.checkNotSentinel(file); + Formatter.checkNotSentinel(file); return function.apply(resource, unix, file); } @@ -144,7 +144,7 @@ interface NeedsFile extends FormatterFunc { @Override default String apply(String unix, File file) throws Exception { - FormatterStepImpl.checkNotSentinel(file); + Formatter.checkNotSentinel(file); return applyWithFile(unix, file); } diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStep.java b/lib/src/main/java/com/diffplug/spotless/FormatterStep.java index 070e502a0c..0fc98c0686 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStep.java @@ -70,28 +70,6 @@ default FormatterStep filterByFile(SerializableFileFilter filter) { return new FilterByFileFormatterStep(this, filter); } - /** - * Implements a FormatterStep in a strict way which guarantees correct and lazy implementation - * of up-to-date checks. This maximizes performance for cases where the FormatterStep is not - * actually needed (e.g. don't load eclipse setting file unless this step is actually running) - * while also ensuring that Gradle can detect changes in a step's settings to determine that - * it needs to rerun a format. - */ - abstract class Strict extends LazyForwardingEquality implements FormatterStep { - private static final long serialVersionUID = 1L; - - /** - * Implements the formatting function strictly in terms - * of the input data and the result of {@link #calculateState()}. - */ - protected abstract String format(State state, String rawUnix, File file) throws Exception; - - @Override - public final String format(String rawUnix, File file) throws Exception { - return format(state(), rawUnix, file); - } - } - /** * @param name * The name of the formatter step. @@ -151,8 +129,8 @@ static static FormatterStep createLazy( String name, ThrowingEx.Supplier stateSupplier, - ThrowingEx.Function stateToFormatter) { - return new FormatterStepImpl.Standard<>(name, stateSupplier, stateToFormatter); + SerializedFunction stateToFormatter) { + return createLazy(name, stateSupplier, SerializedFunction.identity(), stateToFormatter); } /** @@ -168,7 +146,7 @@ static FormatterStep createLazy( static FormatterStep create( String name, State state, - ThrowingEx.Function stateToFormatter) { + SerializedFunction stateToFormatter) { Objects.requireNonNull(state, "state"); return createLazy(name, () -> state, stateToFormatter); } @@ -185,7 +163,7 @@ static FormatterStep create( static FormatterStep createNeverUpToDateLazy( String name, ThrowingEx.Supplier functionSupplier) { - return new FormatterStepImpl.NeverUpToDate(name, functionSupplier); + return new NeverUpToDateStep(name, functionSupplier); } /** diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java b/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java deleted file mode 100644 index 8e2982d6db..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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. - * 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; - -import java.io.File; -import java.io.Serializable; -import java.util.Objects; -import java.util.Random; - -import com.diffplug.spotless.FormatterStep.Strict; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -/** - * Standard implementation of FormatExtension which cleanly enforces - * separation of serializable configuration and a pure format function. - *

- * Not an inner-class of FormatterStep so that it can stay entirely private - * from the API. - */ -@SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") -abstract class FormatterStepImpl extends Strict { - private static final long serialVersionUID = 1L; - - /** Transient because only the state matters. */ - final transient String name; - - /** Transient because only the state matters. */ - transient ThrowingEx.Supplier stateSupplier; - - FormatterStepImpl(String name, ThrowingEx.Supplier stateSupplier) { - this.name = Objects.requireNonNull(name); - this.stateSupplier = Objects.requireNonNull(stateSupplier); - } - - @Override - public String getName() { - return name; - } - - @Override - protected State calculateState() throws Exception { - // LazyForwardingEquality guarantees that this will only be called once, and keeping toFormat - // causes a memory leak, see https://github.com/diffplug/spotless/issues/1194 - State state = stateSupplier.get(); - stateSupplier = null; - return state; - } - - static final class Standard extends FormatterStepImpl { - private static final long serialVersionUID = 1L; - - final transient ThrowingEx.Function stateToFormatter; - transient FormatterFunc formatter; // initialized lazily - - Standard(String name, ThrowingEx.Supplier stateSupplier, ThrowingEx.Function stateToFormatter) { - super(name, stateSupplier); - this.stateToFormatter = Objects.requireNonNull(stateToFormatter); - } - - @Override - protected String format(State state, String rawUnix, File file) throws Exception { - Objects.requireNonNull(state, "state"); - Objects.requireNonNull(rawUnix, "rawUnix"); - Objects.requireNonNull(file, "file"); - if (formatter == null) { - formatter = stateToFormatter.apply(state()); - } - return formatter.apply(rawUnix, file); - } - - @Override - public void close() throws Exception { - if (formatter instanceof FormatterFunc.Closeable) { - ((FormatterFunc.Closeable) formatter).close(); - formatter = null; - } - } - } - - /** Formatter which is equal to itself, but not to any other Formatter. */ - static class NeverUpToDate extends FormatterStepImpl { - private static final long serialVersionUID = 1L; - - private static final Random RANDOM = new Random(); - - final transient ThrowingEx.Supplier formatterSupplier; - transient FormatterFunc formatter; // initialized lazily - - NeverUpToDate(String name, ThrowingEx.Supplier formatterSupplier) { - super(name, RANDOM::nextInt); - this.formatterSupplier = Objects.requireNonNull(formatterSupplier, "formatterSupplier"); - } - - @Override - protected String format(Integer state, String rawUnix, File file) throws Exception { - if (formatter == null) { - formatter = formatterSupplier.get(); - if (formatter instanceof FormatterFunc.Closeable) { - throw new AssertionError("NeverUpToDate does not support FormatterFunc.Closeable. See https://github.com/diffplug/spotless/pull/284"); - } - } - return formatter.apply(rawUnix, file); - } - - @Override - public void close() throws Exception { - if (formatter instanceof FormatterFunc.Closeable) { - ((FormatterFunc.Closeable) formatter).close(); - formatter = null; - } - } - } - - static void checkNotSentinel(File file) { - if (file == Formatter.NO_FILE_SENTINEL) { - throw new IllegalArgumentException("This step requires the underlying file. If this is a test, use StepHarnessWithFile"); - } - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/NeverUpToDateStep.java b/lib/src/main/java/com/diffplug/spotless/NeverUpToDateStep.java new file mode 100644 index 0000000000..f3bd76e2d6 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/NeverUpToDateStep.java @@ -0,0 +1,59 @@ +/* + * Copyright 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; + +import java.io.File; +import java.util.Objects; + +/** + * Formatter which is equal to itself, but not to any other Formatter. + */ +class NeverUpToDateStep implements FormatterStep { + private static final long serialVersionUID = 1L; + + private final String name; + private final ThrowingEx.Supplier formatterSupplier; + private transient FormatterFunc formatter; // initialized lazily + + NeverUpToDateStep(String name, ThrowingEx.Supplier formatterSupplier) { + this.name = name; + this.formatterSupplier = Objects.requireNonNull(formatterSupplier, "formatterSupplier"); + } + + @Override + public String getName() { + return name; + } + + @Override + public String format(String rawUnix, File file) throws Exception { + if (formatter == null) { + formatter = formatterSupplier.get(); + if (formatter instanceof FormatterFunc.Closeable) { + throw new AssertionError("NeverUpToDate does not support FormatterFunc.Closeable. See https://github.com/diffplug/spotless/pull/284"); + } + } + return formatter.apply(rawUnix, file); + } + + @Override + public void close() throws Exception { + if (formatter instanceof FormatterFunc.Closeable) { + ((FormatterFunc.Closeable) formatter).close(); + formatter = null; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java b/lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java index 31b30fe56f..3e58c9dbae 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/NativeCmdStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2021 DiffPlug + * Copyright 2021-2024 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,24 +35,37 @@ private NativeCmdStep() {} public static FormatterStep create(String name, File pathToExe, List arguments) { Objects.requireNonNull(name, "name"); Objects.requireNonNull(pathToExe, "pathToExe"); - return FormatterStep.createLazy(name, () -> new State(FileSignature.signAsList(pathToExe), arguments), State::toFunc); + return FormatterStep.createLazy(name, () -> new State(FileSignature.promise(pathToExe), arguments), State::toRuntime, Runtime::toFunc); } static class State implements Serializable { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; + final FileSignature.Promised pathToExe; + final List arguments; - final FileSignature pathToExe; + State(FileSignature.Promised pathToExe, List arguments) { + this.pathToExe = pathToExe; + this.arguments = arguments; + } + + Runtime toRuntime() { + return new Runtime(pathToExe.get().getOnlyFile(), arguments); + } + } + static class Runtime implements Serializable { + private static final long serialVersionUID = 2L; + final File pathToExe; final List arguments; - State(FileSignature pathToExe, List arguments) { + Runtime(File pathToExe, List arguments) { this.pathToExe = pathToExe; this.arguments = arguments; } String format(ProcessRunner runner, String input) throws IOException, InterruptedException { List argumentsWithPathToExe = new ArrayList<>(); - argumentsWithPathToExe.add(pathToExe.getOnlyFile().getAbsolutePath()); + argumentsWithPathToExe.add(pathToExe.getAbsolutePath()); if (arguments != null) { argumentsWithPathToExe.addAll(arguments); } diff --git a/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java index fece2ea0fd..e9c7a59e54 100644 --- a/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 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. @@ -16,7 +16,6 @@ package com.diffplug.spotless.sql; import java.io.File; -import java.io.Serializable; import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterFunc; @@ -32,24 +31,14 @@ public class DBeaverSQLFormatterStep { private DBeaverSQLFormatterStep() {} public static FormatterStep create(Iterable files) { - return FormatterStep.createLazy(NAME, - () -> new State(files), - State::createFormat); + return FormatterStep.create(NAME, FileSignature.promise(files), + FileSignature.Promised::get, + DBeaverSQLFormatterStep::createFormat); } - static final class State implements Serializable { - private static final long serialVersionUID = 1L; - - final FileSignature settingsSignature; - - State(final Iterable settingsFiles) throws Exception { - this.settingsSignature = FileSignature.signAsList(settingsFiles); - } - - FormatterFunc createFormat() throws Exception { - FormatterProperties preferences = FormatterProperties.from(settingsSignature.files()); - DBeaverSQLFormatter dbeaverSqlFormatter = new DBeaverSQLFormatter(preferences.getProperties()); - return dbeaverSqlFormatter::format; - } + private static FormatterFunc createFormat(FileSignature settings) { + FormatterProperties preferences = FormatterProperties.from(settings.files()); + DBeaverSQLFormatter dbeaverSqlFormatter = new DBeaverSQLFormatter(preferences.getProperties()); + return dbeaverSqlFormatter::format; } } diff --git a/testlib/src/main/java/com/diffplug/spotless/StepHarnessBase.java b/testlib/src/main/java/com/diffplug/spotless/StepHarnessBase.java index 656057cd4e..d86021ef99 100644 --- a/testlib/src/main/java/com/diffplug/spotless/StepHarnessBase.java +++ b/testlib/src/main/java/com/diffplug/spotless/StepHarnessBase.java @@ -27,14 +27,15 @@ enum RoundTrip { private final Formatter formatter; protected StepHarnessBase(Formatter formatter, RoundTrip roundTrip) { - this.formatter = Objects.requireNonNull(formatter); if (roundTrip == RoundTrip.DONT_ROUNDTRIP) { + this.formatter = Objects.requireNonNull(formatter); return; } Formatter roundTripped = SerializableEqualityTester.reserialize(formatter); if (roundTrip == RoundTrip.ASSERT_EQUAL) { Assertions.assertThat(roundTripped).isEqualTo(formatter); } + this.formatter = roundTripped; } protected Formatter formatter() { diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/FenceStepTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/FenceStepTest.java index 682855755e..697a199fd2 100644 --- a/testlib/src/test/java/com/diffplug/spotless/generic/FenceStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/generic/FenceStepTest.java @@ -16,16 +16,11 @@ package com.diffplug.spotless.generic; import java.io.File; -import java.io.Serializable; import java.util.Arrays; -import java.util.Objects; - -import javax.annotation.Nullable; import org.junit.jupiter.api.Test; import com.diffplug.common.base.StringPrinter; -import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.StepHarness; @@ -34,8 +29,8 @@ class FenceStepTest extends ResourceHarness { @Test void single() { FormatterStep fence = FenceStep.named("fence").openClose("spotless:off", "spotless:on") - .preserveWithin(Arrays.asList(createNeverUpToDateSerializable("lowercase", String::toLowerCase))); - StepHarness harness = StepHarness.forStepNoRoundtrip(fence); + .preserveWithin(Arrays.asList(ToCaseStep.lower())); + StepHarness harness = StepHarness.forStep(fence); harness.test( StringPrinter.buildStringFromLines( "A B C", @@ -54,8 +49,8 @@ void single() { @Test void multiple() { FormatterStep fence = FenceStep.named("fence").openClose("spotless:off", "spotless:on") - .preserveWithin(Arrays.asList(createNeverUpToDateSerializable("lowercase", String::toLowerCase))); - StepHarness harness = StepHarness.forStepNoRoundtrip(fence); + .preserveWithin(Arrays.asList(ToCaseStep.lower())); + StepHarness harness = StepHarness.forStep(fence); harness.test( StringPrinter.buildStringFromLines( "A B C", @@ -88,7 +83,7 @@ void multiple() { @Test void broken() { FormatterStep fence = FenceStep.named("fence").openClose("spotless:off", "spotless:on") - .preserveWithin(Arrays.asList(createNeverUpToDateSerializable("uppercase", String::toUpperCase))); + .preserveWithin(Arrays.asList(ToCaseStep.upper())); // this fails because uppercase turns spotless:off into SPOTLESS:OFF, etc StepHarness.forStepNoRoundtrip(fence).testExceptionMsg(StringPrinter.buildStringFromLines("A B C", "spotless:off", @@ -100,8 +95,8 @@ void broken() { @Test void andApply() { FormatterStep fence = FenceStep.named("fence").openClose("", "") - .applyWithin(Arrays.asList(createNeverUpToDateSerializable("lowercase", String::toLowerCase))); - StepHarness.forStepNoRoundtrip(fence).test( + .applyWithin(Arrays.asList(ToCaseStep.lower())); + StepHarness.forStep(fence).test( StringPrinter.buildStringFromLines( "A B C", "", @@ -116,46 +111,45 @@ void andApply() { "G H I")); } - /** - * @param name - * The name of the formatter step - * @param function - * The function used by the formatter step - * @return A FormatterStep which will never report that it is up-to-date, because - * it is not equal to the serialized representation of itself. - */ - static FormatterStep createNeverUpToDateSerializable( - String name, - T function) { - Objects.requireNonNull(function, "function"); - return new NeverUpToDateSerializable(name, function); - } + static class ToCaseStep implements FormatterStep { + static ToCaseStep upper() { + return new ToCaseStep(true); + } - static class NeverUpToDateSerializable implements FormatterStep, Serializable { - private final String name; - private final T formatterFunc; + static ToCaseStep lower() { + return new ToCaseStep(false); + } + + private final boolean uppercase; - private NeverUpToDateSerializable(String name, T formatterFunc) { - this.name = name; - this.formatterFunc = formatterFunc; + ToCaseStep(boolean uppercase) { + this.uppercase = uppercase; } @Override public String getName() { - return name; + return uppercase ? "uppercase" : "lowercase"; } - @Nullable + @org.jetbrains.annotations.Nullable @Override public String format(String rawUnix, File file) throws Exception { - return formatterFunc.apply(rawUnix, file); + return uppercase ? rawUnix.toUpperCase() : rawUnix.toLowerCase(); } @Override public void close() throws Exception { - if (formatterFunc instanceof FormatterFunc.Closeable) { - ((FormatterFunc.Closeable) formatterFunc).close(); - } + + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ToCaseStep && getName().equals(((ToCaseStep) obj).getName()); } } } diff --git a/testlib/src/test/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStepTest.java index 58b0115b9c..648ddc6669 100644 --- a/testlib/src/test/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 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. @@ -29,7 +29,7 @@ class DBeaverSQLFormatterStepTest extends ResourceHarness { @Test - void behavior() throws Exception { + void behavior() { FormatterStep step = DBeaverSQLFormatterStep.create(Collections.emptySet()); StepHarness.forStep(step) .testResource("sql/dbeaver/full.dirty", "sql/dbeaver/full.clean") @@ -40,21 +40,21 @@ void behavior() throws Exception { } @Test - void behaviorWithConfigFile() throws Exception { + void behaviorWithConfigFile() { FormatterStep step = DBeaverSQLFormatterStep.create(createTestFiles("sql/dbeaver/sqlConfig.properties")); StepHarness.forStep(step) .testResource("sql/dbeaver/create.dirty", "sql/dbeaver/create.clean"); } @Test - void behaviorWithAlternativeConfigFile() throws Exception { + void behaviorWithAlternativeConfigFile() { FormatterStep step = DBeaverSQLFormatterStep.create(createTestFiles("sql/dbeaver/sqlConfig2.properties")); StepHarness.forStep(step) .testResource("sql/dbeaver/create.dirty", "sql/dbeaver/create.clean.alternative"); } @Test - void equality() throws Exception { + void equality() { List sqlConfig1 = createTestFiles("sql/dbeaver/sqlConfig.properties"); List sqlConfig2 = createTestFiles("sql/dbeaver/sqlConfig2.properties"); new SerializableEqualityTester() {