Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)));
}
}
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading