diff --git a/CHANGES.md b/CHANGES.md index ee531ac510..3f8f699b84 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Fixed +- `ConfigurationCacheHackList` no longer throws `IllegalStateException` ("If the initializer was null, then one of roundtripStateInternal or equalityStateInternal should be non-null") during Gradle input fingerprinting when a step's equality state is `null` (e.g. a step wrapped in `toggleOffOn()`/a fence whose state is not yet provisioned). ([#2950](https://github.com/diffplug/spotless/issues/2950)) ## [4.6.2] - 2026-05-27 ### Fixed diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java b/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java index 15febb27ff..1343a11529 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStepSerializationRoundtrip.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2025 DiffPlug + * Copyright 2023-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,10 +69,11 @@ protected FormatterFunc stateToFormatter(EqualityState equalityState) throws Exc private void writeObject(ObjectOutputStream out) throws IOException { if (initializer == null) { - // then this instance was created by Gradle's ConfigurationCacheHackList and the following will hold true - if (roundtripStateInternal == null && equalityStateInternal == null) { - throw new IllegalStateException("If the initializer was null, then one of roundtripStateInternal or equalityStateInternal should be non-null, and neither was"); - } + // then this instance was created by Gradle's ConfigurationCacheHackList. HackClone populated + // exactly one of roundtripStateInternal / equalityStateInternal; it is legitimate for that value + // to be null (e.g. a step whose equality state is null), which still serializes deterministically. + // An equality-optimized clone is only ever serialized for its cache-key bytes, never rehydrated for + // use, so a both-null clone is safe and must not blow up Gradle's input fingerprinting (see #2950). } else { // this was a normal instance, which means we need to encode to roundtripStateInternal (since the initializer might not be serializable) // and there's no reason to keep equalityStateInternal since we can always recompute it diff --git a/lib/src/test/java/com/diffplug/spotless/ConfigurationCacheHackListTest.java b/lib/src/test/java/com/diffplug/spotless/ConfigurationCacheHackListTest.java new file mode 100644 index 0000000000..1ebd4e2834 --- /dev/null +++ b/lib/src/test/java/com/diffplug/spotless/ConfigurationCacheHackListTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2024-2026 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.Serializable; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.generic.FenceStep; + +class ConfigurationCacheHackListTest { + private static FormatterStep roundtripStep(String name, Serializable equalityState) { + // mirrors FormatterStep.createLazy(name, roundtripInit, equalityFunc, formatterFunc) + return FormatterStep.createLazy(name, + () -> "roundtrip-" + name, + rt -> equalityState, + eq -> (FormatterFunc) (s -> s)); + } + + private static FormatterStep toggleOffOn(FormatterStep... within) { + return FenceStep.named(FenceStep.defaultToggleName()) + .openClose(FenceStep.defaultToggleOff(), FenceStep.defaultToggleOn()) + .preserveWithin(List.of(within)); + } + + /** Gradle fingerprints the task's input (a {@link ConfigurationCacheHackList}) by calling hashCode(). */ + private static int fingerprint(FormatterStep... steps) { + ConfigurationCacheHackList list = ConfigurationCacheHackList.forEquality(); + list.addAll(List.of(steps)); + return list.hashCode(); + } + + @Test + void plainStepCanBeFingerprinted() { + fingerprint(roundtripStep("plain", "eq-state")); + } + + @Test + void fenceWrappingStepCanBeFingerprinted() { + fingerprint(toggleOffOn(roundtripStep("inner", "eq-state"))); + } + + /** + * A sub-step whose equality state is null (e.g. a not-yet-provisioned promised state, as with + * greclipse in a clean CI environment) must not break Gradle's input fingerprinting. See #2950. + */ + @Test + void fenceWrappingStepWithNullEqualityStateCanBeFingerprinted() { + fingerprint(toggleOffOn(roundtripStep("inner-null-eq", null))); + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index d0fde86d5f..e191087703 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Fixed +- Fixed `spotlessGroovyGradle`/`greclipse()` (and any `toggleOffOn()`-wrapped step) failing during Gradle's input fingerprinting with "If the initializer was null, then one of roundtripStateInternal or equalityStateInternal should be non-null" — a regression introduced in 8.6.0 that surfaces in clean CI environments. ([#2950](https://github.com/diffplug/spotless/issues/2950)) ## [8.6.0] - 2026-05-27 ### Added