diff --git a/lib/src/main/java/com/diffplug/spotless/java/TableTestFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/java/TableTestFormatterStep.java
index a813dc4e65..14ab3b1f4d 100644
--- a/lib/src/main/java/com/diffplug/spotless/java/TableTestFormatterStep.java
+++ b/lib/src/main/java/com/diffplug/spotless/java/TableTestFormatterStep.java
@@ -18,6 +18,7 @@
import java.io.Serial;
import java.io.Serializable;
import java.lang.reflect.Constructor;
+import java.util.Locale;
import java.util.Objects;
import com.diffplug.spotless.FormatterFunc;
@@ -27,34 +28,61 @@
/**
* Formats {@code @TableTest} annotation tables in Java and Kotlin source files.
- * Configuration is read from {@code .editorconfig} files.
+ *
+ * Configuration is read from {@code .editorconfig} files. When no editorconfig is found
+ * or the lookup fails, the configured fallback indent style and size are used.
+ * If no fallback is configured, defaults matching {@code Config.SPACES_4} and
+ * {@code Config.NO_INDENT} are applied.
*/
public final class TableTestFormatterStep implements Serializable {
@Serial
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 2L;
private static final String NAME = "tableTestFormatter";
private static final String MAVEN_COORDINATE = "org.tabletest:tabletest-formatter-core:";
private static final String DEFAULT_VERSION = "1.1.1";
+ /** Default fallback indent style ({@code "space"}) for Java/Kotlin files. */
+ public static final String DEFAULT_INDENT_STYLE = "space";
+ /** Default fallback indent size ({@code 4}) for Java/Kotlin files, matching {@code Config.SPACES_4}. */
+ public static final int DEFAULT_INDENT_SIZE = 4;
+
private final JarState.Promised jarState;
private final String version;
+ private final String indentStyle;
+ private final int indentSize;
- private TableTestFormatterStep(JarState.Promised jarState, String version) {
+ private TableTestFormatterStep(JarState.Promised jarState, String version, String indentStyle, int indentSize) {
this.jarState = jarState;
this.version = version;
+ this.indentStyle = validateIndentStyle(indentStyle);
+ this.indentSize = validateIndentSize(indentSize);
}
- /** Creates a step which formats {@code @TableTest} tables using the default version. */
+ /** Creates a step which formats {@code @TableTest} tables using the default version and indent config. */
public static FormatterStep create(Provisioner provisioner) {
return create(defaultVersion(), provisioner);
}
- /** Creates a step which formats {@code @TableTest} tables using the given version. */
+ /** Creates a step which formats {@code @TableTest} tables using the given version and default indent config. */
public static FormatterStep create(String version, Provisioner provisioner) {
+ return create(version, provisioner, DEFAULT_INDENT_STYLE, DEFAULT_INDENT_SIZE);
+ }
+
+ /**
+ * Creates a step which formats {@code @TableTest} tables using the given version and fallback indent config.
+ *
+ * The fallback config is used when no {@code .editorconfig} is found or the lookup fails.
+ *
+ * @param version the tabletest-formatter-core version
+ * @param provisioner the jar provisioner
+ * @param indentStyle fallback indent style: {@code "space"} or {@code "tab"} (case-insensitive)
+ * @param indentSize fallback indent size (must be >= 0)
+ */
+ public static FormatterStep create(String version, Provisioner provisioner, String indentStyle, int indentSize) {
Objects.requireNonNull(version, "version");
Objects.requireNonNull(provisioner, "provisioner");
return FormatterStep.create(NAME,
- new TableTestFormatterStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner)), version),
+ new TableTestFormatterStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner)), version, indentStyle, indentSize),
TableTestFormatterStep::equalityState,
State::createFormat);
}
@@ -65,26 +93,46 @@ public static String defaultVersion() {
}
private State equalityState() {
- return new State(jarState.get(), version);
+ return new State(jarState.get(), version, indentStyle, indentSize);
+ }
+
+ public static String validateIndentStyle(String indentStyle) {
+ Objects.requireNonNull(indentStyle, "indentStyle");
+ String lower = indentStyle.toLowerCase(Locale.ROOT);
+ if (!lower.equals("space") && !lower.equals("tab")) {
+ throw new IllegalArgumentException("indentStyle must be 'space' or 'tab', got: " + indentStyle);
+ }
+ return lower;
+ }
+
+ public static int validateIndentSize(int indentSize) {
+ if (indentSize < 0) {
+ throw new IllegalArgumentException("indentSize must be >= 0, got: " + indentSize);
+ }
+ return indentSize;
}
private static final class State implements Serializable {
@Serial
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 2L;
private final JarState jarState;
private final String version;
+ private final String indentStyle;
+ private final int indentSize;
- State(JarState jarState, String version) {
+ State(JarState jarState, String version, String indentStyle, int indentSize) {
this.jarState = jarState;
this.version = version;
+ this.indentStyle = indentStyle;
+ this.indentSize = indentSize;
}
FormatterFunc createFormat() throws Exception {
ClassLoader classLoader = jarState.getClassLoader();
Class> formatterClazz = classLoader.loadClass("com.diffplug.spotless.glue.java.TableTestFormatterFunc");
- Constructor> constructor = formatterClazz.getConstructor();
- return (FormatterFunc.NeedsFile) constructor.newInstance();
+ Constructor> constructor = formatterClazz.getConstructor(String.class, int.class);
+ return (FormatterFunc.NeedsFile) constructor.newInstance(indentStyle, indentSize);
}
}
}
diff --git a/lib/src/tableTestFormatter/java/com/diffplug/spotless/glue/java/TableTestFormatterFunc.java b/lib/src/tableTestFormatter/java/com/diffplug/spotless/glue/java/TableTestFormatterFunc.java
index 935cfe2a59..2dd183ba03 100644
--- a/lib/src/tableTestFormatter/java/com/diffplug/spotless/glue/java/TableTestFormatterFunc.java
+++ b/lib/src/tableTestFormatter/java/com/diffplug/spotless/glue/java/TableTestFormatterFunc.java
@@ -16,9 +16,11 @@
package com.diffplug.spotless.glue.java;
import java.io.File;
+import java.util.Locale;
import org.tabletest.formatter.config.Config;
import org.tabletest.formatter.config.EditorConfigProvider;
+import org.tabletest.formatter.config.IndentStyle;
import org.tabletest.formatter.core.SourceFileFormatter;
import org.tabletest.formatter.core.TableTestFormatter;
@@ -26,20 +28,45 @@
/**
* Formats {@code @TableTest} annotation tables in Java, Kotlin, and standalone {@code .table} files.
+ *
+ * For Java/Kotlin files, the indent config priority is:
+ *
+ * - Settings from {@code .editorconfig}
+ * - Configured fallback ({@code indentStyle}/{@code indentSize} constructor parameters)
+ * - Built-in default: {@code Config.SPACES_4}
+ *
+ * For {@code .table} files, {@code Config.NO_INDENT} is always used.
*/
public class TableTestFormatterFunc implements FormatterFunc.NeedsFile {
- private static final EditorConfigProvider CONFIG_PROVIDER = new EditorConfigProvider();
+ private final EditorConfigProvider CONFIG_PROVIDER = new EditorConfigProvider();
+ private final Config sourceFallbackConfig;
private final SourceFileFormatter sourceFormatter = new SourceFileFormatter();
private final TableTestFormatter tableFormatter = new TableTestFormatter();
+ /** Creates a formatter using the built-in default fallback ({@code Config.SPACES_4}). */
+ public TableTestFormatterFunc() {
+ this.sourceFallbackConfig = Config.SPACES_4;
+ }
+
+ /**
+ * Creates a formatter with a configured fallback indent style and size for Java/Kotlin files.
+ * Used when {@code .editorconfig} is absent or the lookup fails.
+ *
+ * @param indentStyle {@code "space"} or {@code "tab"} (case-insensitive)
+ * @param indentSize indent size (>= 0)
+ */
+ public TableTestFormatterFunc(String indentStyle, int indentSize) {
+ this.sourceFallbackConfig = new Config(IndentStyle.valueOf(indentStyle.toUpperCase(Locale.ROOT)), indentSize);
+ }
+
@Override
public String applyWithFile(String unix, File file) throws Exception {
String fileName = file.getName();
if (fileName.endsWith(".java") || fileName.endsWith(".kt")) {
- Config config = CONFIG_PROVIDER.lookupConfig(file.toPath(), Config.SPACES_4);
+ Config config = CONFIG_PROVIDER.lookupConfig(file.toPath(), sourceFallbackConfig);
String formatted = sourceFormatter.format(unix, config);
return formatted.equals(unix) ? unix : formatted;
}
diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md
index 69d68eb09f..692ed74491 100644
--- a/plugin-gradle/CHANGES.md
+++ b/plugin-gradle/CHANGES.md
@@ -3,6 +3,10 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).
## [Unreleased]
+### Added
+- Add `withIndentStyle` and `withIndentSize` configuration to `tableTestFormatter` for setting the fallback indent when no `.editorconfig` is found. ([#2893](https://github.com/diffplug/spotless/pull/2893))
+### Fixed
+- Fix `tableTestFormatter` editorconfig cache not honoring `.editorconfig` changes across Gradle daemon runs due to a shared static `EditorConfigProvider`. ([#2893](https://github.com/diffplug/spotless/pull/2893))
## [8.4.0] - 2026-03-18
### Added
diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TableTestExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TableTestExtension.java
index 22d2c75d60..f5cda84584 100644
--- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TableTestExtension.java
+++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TableTestExtension.java
@@ -46,14 +46,37 @@ public TableTestFormatterConfig tableTestFormatter(String version) {
public class TableTestFormatterConfig {
private final String version;
+ private String indentStyle = TableTestFormatterStep.DEFAULT_INDENT_STYLE;
+ private int indentSize = TableTestFormatterStep.DEFAULT_INDENT_SIZE;
TableTestFormatterConfig(String version) {
this.version = version;
addStep(createStep());
}
+ /**
+ * Sets the fallback indent style used when no {@code .editorconfig} is found.
+ * Must be {@code "space"} or {@code "tab"} (case-insensitive).
+ * Defaults to {@code "space"}.
+ */
+ public TableTestFormatterConfig withIndentStyle(String indentStyle) {
+ this.indentStyle = TableTestFormatterStep.validateIndentStyle(indentStyle);
+ replaceStep(createStep());
+ return this;
+ }
+
+ /**
+ * Sets the fallback indent size used when no {@code .editorconfig} is found.
+ * Must be >= 0. Defaults to {@code 4}.
+ */
+ public TableTestFormatterConfig withIndentSize(int indentSize) {
+ this.indentSize = TableTestFormatterStep.validateIndentSize(indentSize);
+ replaceStep(createStep());
+ return this;
+ }
+
private FormatterStep createStep() {
- return TableTestFormatterStep.create(version, provisioner());
+ return TableTestFormatterStep.create(version, provisioner(), indentStyle, indentSize);
}
}
}
diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/TableTestFormatter.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/TableTestFormatter.java
index 2b76fbb080..d6ea99aec5 100644
--- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/TableTestFormatter.java
+++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/TableTestFormatter.java
@@ -23,16 +23,37 @@
import com.diffplug.spotless.maven.FormatterStepFactory;
/**
- * Formats {@code @TableTest} annotation tables. Configuration is read from {@code .editorconfig} files.
+ * Formats {@code @TableTest} annotation tables. Configuration is read from {@code .editorconfig} files,
+ * falling back to the configured {@code indentStyle} and {@code indentSize} when no editorconfig is found.
*/
public class TableTestFormatter implements FormatterStepFactory {
@Parameter
private String version;
+ /**
+ * Fallback indent style when no {@code .editorconfig} is found: {@code space} or {@code tab}.
+ * Defaults to {@code space}.
+ */
+ @Parameter
+ private String indentStyle;
+
+ /**
+ * Fallback indent size when no {@code .editorconfig} is found. Must be >= 0.
+ * Defaults to {@code 4}.
+ */
+ @Parameter
+ private Integer indentSize;
+
@Override
public FormatterStep newFormatterStep(FormatterStepConfig config) {
- String version = this.version != null ? this.version : TableTestFormatterStep.defaultVersion();
- return TableTestFormatterStep.create(version, config.getProvisioner());
+ String resolvedVersion = this.version != null ? this.version : TableTestFormatterStep.defaultVersion();
+ String resolvedStyle = this.indentStyle != null
+ ? TableTestFormatterStep.validateIndentStyle(this.indentStyle)
+ : TableTestFormatterStep.DEFAULT_INDENT_STYLE;
+ int resolvedSize = this.indentSize != null
+ ? TableTestFormatterStep.validateIndentSize(this.indentSize)
+ : TableTestFormatterStep.DEFAULT_INDENT_SIZE;
+ return TableTestFormatterStep.create(resolvedVersion, config.getProvisioner(), resolvedStyle, resolvedSize);
}
}
diff --git a/testlib/src/main/resources/java/tableTestFormatter/JavaCodeFormattedWith2SpaceIndent.test b/testlib/src/main/resources/java/tableTestFormatter/JavaCodeFormattedWith2SpaceIndent.test
new file mode 100644
index 0000000000..6e53173564
--- /dev/null
+++ b/testlib/src/main/resources/java/tableTestFormatter/JavaCodeFormattedWith2SpaceIndent.test
@@ -0,0 +1,13 @@
+import org.tabletest.junit.TableTest;
+
+class CalculatorTest {
+
+ @TableTest("""
+ scenario | a | b | sum?
+ positive | 1 | 2 | 3
+ negative | -1 | -2 | -3
+ """)
+ void shouldAddNumbers(int a, int b, int sum) {
+ assert a + b == sum;
+ }
+}
diff --git a/testlib/src/test/java/com/diffplug/spotless/java/TableTestFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/TableTestFormatterStepTest.java
index ff00ef49cf..0299c1ad3b 100644
--- a/testlib/src/test/java/com/diffplug/spotless/java/TableTestFormatterStepTest.java
+++ b/testlib/src/test/java/com/diffplug/spotless/java/TableTestFormatterStepTest.java
@@ -43,24 +43,81 @@ void behaviorTableFile() {
}
}
+ @Test
+ void editorConfigChangesAreHonored() {
+ // Verifies that a new FormatterStep picks up .editorconfig changes rather than
+ // returning stale results from a shared cache.
+ //
+ // Two steps with the same version share a classloader (via SpotlessCache's
+ // serialization-based key). If EditorConfigProvider were stored as a static field,
+ // its permanent ec4j cache would survive across FormatterFunc instances, and the
+ // second step would still see the indent_size=4 result cached by the first step.
+ // Making EditorConfigProvider an instance field ensures each FormatterFunc gets a
+ // fresh provider with an empty cache.
+
+ // Step 1: .editorconfig with indent_size=4
+ setFile(".editorconfig").toContent("[*.java]\nindent_size = 4\n");
+ FormatterStep step1 = TableTestFormatterStep.create(VERSION, TestProvisioner.mavenCentral());
+ try (StepHarnessWithFile harness = StepHarnessWithFile.forStep(this, step1)) {
+ harness.testResource("CalculatorTest.java",
+ "java/tableTestFormatter/JavaCodeUnformatted.test",
+ "java/tableTestFormatter/JavaCodeFormatted.test");
+ }
+
+ // Step 2: change .editorconfig to indent_size=2, create a new step
+ setFile(".editorconfig").toContent("[*.java]\nindent_size = 2\n");
+ FormatterStep step2 = TableTestFormatterStep.create(VERSION, TestProvisioner.mavenCentral());
+ try (StepHarnessWithFile harness = StepHarnessWithFile.forStep(this, step2)) {
+ harness.testResource("CalculatorTest.java",
+ "java/tableTestFormatter/JavaCodeUnformatted.test",
+ "java/tableTestFormatter/JavaCodeFormattedWith2SpaceIndent.test");
+ }
+ }
+
+ @Test
+ void behaviorWithConfiguredFallback() {
+ // When no .editorconfig is present, the configured indentSize is used as the fallback
+ // instead of the built-in default (Config.SPACES_4 = 4 spaces).
+ // Configuring indentSize=2 should produce the same output as when editorconfig sets
+ // indent_size=2.
+ FormatterStep step = TableTestFormatterStep.create(VERSION, TestProvisioner.mavenCentral(), "space", 2);
+ try (StepHarnessWithFile harness = StepHarnessWithFile.forStep(this, step)) {
+ harness.testResource("CalculatorTest.java",
+ "java/tableTestFormatter/JavaCodeUnformatted.test",
+ "java/tableTestFormatter/JavaCodeFormattedWith2SpaceIndent.test");
+ }
+ }
+
@Test
void equality() {
new SerializableEqualityTester() {
String version = VERSION;
+ String indentStyle = TableTestFormatterStep.DEFAULT_INDENT_STYLE;
+ int indentSize = TableTestFormatterStep.DEFAULT_INDENT_SIZE;
@Override
protected void setupTest(API api) {
- // same version == same
+ // same version + defaults == same
api.areDifferentThan();
- // change the version, and it's different
+ // change the version
version = "1.0.0";
api.areDifferentThan();
+
+ // restore version, change indent size
+ version = VERSION;
+ indentSize = 2;
+ api.areDifferentThan();
+
+ // change indent style
+ indentSize = TableTestFormatterStep.DEFAULT_INDENT_SIZE;
+ indentStyle = "tab";
+ api.areDifferentThan();
}
@Override
protected FormatterStep create() {
- return TableTestFormatterStep.create(version, TestProvisioner.mavenCentral());
+ return TableTestFormatterStep.create(version, TestProvisioner.mavenCentral(), indentStyle, indentSize);
}
}.testEquals();
}