Skip to content

Replace the unmaintained Autostyle formatter with Spotless #3292

Description

@juherr

Context

Correction: this issue originally claimed Autostyle was unmaintained since 2021. That is wrong — 4.0 was released in January 2024 and 4.0.1 in July 2025, and @vlsi has said he plans to move the Java part to OpenRewrite. So "the formatter is abandoned" is not a reason to migrate. The rest of the issue is kept for the measurements, which are still useful to whoever revisits this.

The build formats Java sources with Autostyle 4.0 (build-logic/code-quality/src/main/kotlin/testng.style.gradle.kts). Swapping it for Spotless was attempted as part of #3291 and backed out, because Spotless is flaky in this build.

With the "unmaintained" premise gone, the remaining options are: stay on Autostyle, follow @vlsi to OpenRewrite, or revisit Spotless once the flakiness below has a fix.

Why the first attempt was reverted

With org.gradle.parallel=true (set in gradle.properties), spotlessJavaCheck fails intermittently — roughly 1 run in 6 of ./gradlew clean build:

Execution failed for task ':testng:spotlessJavaCheck' (registered by plugin 'testng.style').
> There were 1 lint error(s), they must be fixed or suppressed.
  src/test/java/org/testng/test/VerifyShadedJarWorksTest.java:LINE_UNDEFINED
      removeUnusedImports(java.lang.NoClassDefFoundError)
      com/google/googlejavaformat/java/RemoveUnusedImports (...)

The class reported missing differs on every occurrence — com/google/common/collect/Iterators$MergingIterator, ImmutableList$SubList, ImmutableEnumMap, FluentIterable$3, com/google/common/base/Predicate, and google-java-format's own RemoveUnusedImports. That points at a formatter classloader being closed while still in use, rather than at any single misbehaving step.

What was measured

12 runs of ./gradlew clean build -x test per configuration, on Gradle 9.6.1 / JDK 25 / Spotless 8.8.0:

Configuration Failures
Spotless, --no-parallel 0 / 6
Spotless, removeUnusedImports("cleanthat-javaparser-unnecessaryimport") ~2 / 12
Spotless, default removeUnusedImports() ~2 / 12
Spotless, without the root-project apply false declaration 12 / 12
Spotless, tasks serialized through a maxParallelUsages = 1 build service 6 / 12
Autostyle (current state) 0 / 10

Two results are worth keeping in mind:

  • Removing the root id("com.diffplug.spotless") apply false makes it fail every time. The plugin also reaches the build via api("com.diffplug.spotless:spotless-plugin-gradle") in build-logic/code-quality, so the two entry points appear to matter. Switching that api to compileOnly does not work — the precompiled script plugin then cannot resolve com.diffplug.spotless at apply time.
  • Serializing the tasks with a shared build service made things worse, not better, which argues against a simple task-level race.

Spotless 8.8.0 was the latest release at the time, so there was no upstream fix to pick up.

Possible directions

  1. Apply Spotless from the root project only, targeting **/src/**/*.java, so there is a single set of formatter tasks and no cross-project concurrency. Untested. Costs per-project incrementality and changes how check is wired.
  2. Report upstream to diffplug/spotless with the reproducer above — a classloader closed mid-use across parallel projects looks like a genuine bug.
  3. Wait for a newer Spotless and re-measure with the table above.
  4. Stay on Autostyle. It works, and the usual argument against it does not currently bite: Gradle 9 keeps the configuration cache off by default, and Spotless' removeUnusedImports is not configuration-cache compatible either, so the swap would not unlock it.
  5. Follow the OpenRewrite direction @vlsi is taking for Autostyle's Java support (Add OpenRewrite code cleanup with an incremental in-house runner apache/jmeter#6217), which would handle import management and code cleanup in one pass.

Separate finding: Autostyle 4.0.1 does not run on JDK 25

The build is pinned to Autostyle 4.0 on purpose. 4.0.1 fails every module on JDK 25:

Execution failed for task ':testng-reflection-utils:autostyleJavaProcess'
> Error while formatting .../ReflectionHelper.java: class
  com.google.googlejavaformat.java.RemoveUnusedImports (in unnamed module @0x2f5f0157)
  cannot access class com.sun.tools.javac.file.JavacFileManager (in module jdk.compiler)
  because module jdk.compiler does not export com.sun.tools.javac.file to unnamed module

google-java-format needs --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED on JDK 16+, and 4.0.1 does not appear to pass it. 4.0 is unaffected. Worth reporting upstream before anyone bumps the version.

Notes for whoever picks this up

  • The migration itself is small: swap the plugin id and dependency, rename the skipAutostyle build parameter to skipSpotless, and update .github/CONTRIBUTING.md, .github/PULL_REQUEST_TEMPLATE.md, docs/BUILD_SYSTEM.md and docs/README.md from autostyleApply/autostyleCheck to spotlessApply/spotlessCheck.
  • Expect a second, separate commit of pure reformatting: the google-java-format version bundled with Spotless reflows javadoc and method chains differently from the one Autostyle used. Last time that was 86 files, +829/-281, with no semantic change. Keeping it apart from the configuration change makes review possible.
  • Whatever the fix, please validate it by running ./gradlew clean build -x test at least a dozen times in a row. A single green run proves nothing here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type: Feature requestRequest for a new feature or enhancementarchitectureInternal architecture / refactoring

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions