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: + *

    + *
  1. Settings from {@code .editorconfig}
  2. + *
  3. Configured fallback ({@code indentStyle}/{@code indentSize} constructor parameters)
  4. + *
  5. Built-in default: {@code Config.SPACES_4}
  6. + *
+ * 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(); }