diff --git a/CHANGES.md b/CHANGES.md index 9929ccf7a6..76f7a0e6f7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,10 @@ 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] +### Added +- Add a `cleanup` option to `GoogleJavaFormatStep` to disable google-java-format's non-whitespace cleanups. ([#3157](https://github.com/diffplug/spotless/pull/3157)) +### Changes +- Bump default `google-java-format` version `1.36.1` -> `2.0.0`. ([#3157](https://github.com/diffplug/spotless/pull/3157)) ## [4.10.4] - 2026-10-08 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index acb1161c04..840d24c3e1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ flexmark-all = "com.vladsch.flexmark:flexmark-all:0.64.8" freshmark = "com.diffplug.freshmark:freshmark:1.3.1" cucumber-gherkin = "io.cucumber:gherkin:42.0.1" gherkin-utils = "io.cucumber:gherkin-utils:12.0.2" -google-java-format = "com.google.googlejavaformat:google-java-format:1.36.1" +google-java-format = "com.google.googlejavaformat:google-java-format:2.0.0" gson = "com.google.code.gson:gson:2.14.0" json-simple = "org.json:json:20210307" ktfmt = "org.jetbrains.kotlinx:ktfmt:0.65" diff --git a/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java index d13f8d9d40..d07800fbc8 100644 --- a/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java +++ b/lib/src/googleJavaFormat/java/com/diffplug/spotless/glue/java/GoogleJavaFormatFormatterFunc.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 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. @@ -15,6 +15,7 @@ */ package com.diffplug.spotless.glue.java; +import java.lang.reflect.Method; import java.util.Objects; import javax.annotation.Nonnull; @@ -46,7 +47,7 @@ public class GoogleJavaFormatFormatterFunc implements FormatterFunc { private final boolean reorderImports; - public GoogleJavaFormatFormatterFunc(@Nonnull String version, @Nonnull String style, boolean reflowStrings, boolean reorderImports, boolean formatJavadoc) { + public GoogleJavaFormatFormatterFunc(@Nonnull String version, @Nonnull String style, boolean reflowStrings, boolean reorderImports, boolean formatJavadoc, boolean cleanup) { this.version = Objects.requireNonNull(version); this.formatterStyle = Style.valueOf(Objects.requireNonNull(style)); this.reflowStrings = reflowStrings; @@ -56,9 +57,29 @@ public GoogleJavaFormatFormatterFunc(@Nonnull String version, @Nonnull String st if (!formatJavadoc) { builder = builder.formatJavadoc(false); } + if (!cleanup) { + builder = disableCleanup(builder); + } this.formatter = new Formatter(builder.build()); } + // JavaFormatterOptions.Builder.cleanup(boolean) was added in google-java-format 2.0.0, and this + // glue is compiled against an older version. + private static JavaFormatterOptions.Builder disableCleanup(JavaFormatterOptions.Builder builder) { + Method cleanup; + try { + cleanup = JavaFormatterOptions.Builder.class.getMethod("cleanup", boolean.class); + } catch (NoSuchMethodException e) { + // google-java-format versions before 2.0.0 don't have cleanups + return builder; + } + try { + return (JavaFormatterOptions.Builder) cleanup.invoke(builder, false); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + } + @Override @Nonnull public String apply(@Nonnull String input) throws Exception { diff --git a/lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java b/lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java index 079955a731..13b6dde628 100644 --- a/lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java +++ b/lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java @@ -34,6 +34,7 @@ public final class GoogleJavaFormatStep implements Serializable { private static final boolean DEFAULT_REFLOW_LONG_STRINGS = false; private static final boolean DEFAULT_REORDER_IMPORTS = false; private static final boolean DEFAULT_FORMAT_JAVADOC = true; + private static final boolean DEFAULT_CLEANUP = true; private static final String NAME = "google-java-format"; private static final String MAVEN_COORDINATE = "com.google.googlejavaformat:google-java-format"; @@ -44,19 +45,22 @@ public final class GoogleJavaFormatStep implements Serializable { private final boolean reflowLongStrings; private final boolean reorderImports; private final boolean formatJavadoc; + private final boolean cleanup; private GoogleJavaFormatStep(JarState.Promised jarState, String version, String style, boolean reflowLongStrings, boolean reorderImports, - boolean formatJavadoc) { + boolean formatJavadoc, + boolean cleanup) { this.jarState = jarState; this.version = version; this.style = style; this.reflowLongStrings = reflowLongStrings; this.reorderImports = reorderImports; this.formatJavadoc = formatJavadoc; + this.cleanup = cleanup; } /** Creates a step which formats everything - code, import order, and unused imports. */ @@ -89,14 +93,22 @@ public static FormatterStep create(String groupArtifact, String version, String /** Creates a step which formats everything - groupArtifact, code, import order, and unused imports - and optionally reflows long strings. */ public static FormatterStep create(String groupArtifact, String version, String style, Provisioner provisioner, boolean reflowLongStrings, boolean reorderImports, boolean formatJavadoc) { - return createInternally(NAME, groupArtifact, version, style, provisioner, reflowLongStrings, reorderImports, formatJavadoc, false); + return create(groupArtifact, version, style, provisioner, reflowLongStrings, reorderImports, formatJavadoc, DEFAULT_CLEANUP); + } + + /** + * Creates a step which formats everything - groupArtifact, code, import order, and unused imports - and optionally reflows long strings. + * If {@code cleanup} is false, google-java-format 2.0.0 and later skip their non-whitespace cleanups (older versions don't have any). + */ + public static FormatterStep create(String groupArtifact, String version, String style, Provisioner provisioner, boolean reflowLongStrings, boolean reorderImports, boolean formatJavadoc, boolean cleanup) { + return createInternally(NAME, groupArtifact, version, style, provisioner, reflowLongStrings, reorderImports, formatJavadoc, cleanup, false); } static FormatterStep createRemoveUnusedImportsOnly(Provisioner provisioner) { - return createInternally(RemoveUnusedImportsStep.NAME, MAVEN_COORDINATE, defaultVersion(), defaultStyle(), provisioner, defaultReflowLongStrings(), defaultReorderImports(), defaultFormatJavadoc(), true); + return createInternally(RemoveUnusedImportsStep.NAME, MAVEN_COORDINATE, defaultVersion(), defaultStyle(), provisioner, defaultReflowLongStrings(), defaultReorderImports(), defaultFormatJavadoc(), defaultCleanup(), true); } - private static FormatterStep createInternally(String name, String groupArtifact, String version, String style, Provisioner provisioner, boolean reflowLongStrings, boolean reorderImports, boolean formatJavadoc, boolean removeImports) { + private static FormatterStep createInternally(String name, String groupArtifact, String version, String style, Provisioner provisioner, boolean reflowLongStrings, boolean reorderImports, boolean formatJavadoc, boolean cleanup, boolean removeImports) { Objects.requireNonNull(groupArtifact, "groupArtifact"); if (groupArtifact.chars().filter(ch -> ch == ':').count() != 1) { throw new IllegalArgumentException("groupArtifact must be in the form 'groupId:artifactId'"); @@ -105,7 +117,7 @@ private static FormatterStep createInternally(String name, String groupArtifact, Objects.requireNonNull(style, "style"); Objects.requireNonNull(provisioner, "provisioner"); - GoogleJavaFormatStep step = new GoogleJavaFormatStep(JarState.promise(() -> JarState.from(groupArtifact + ":" + version, provisioner)), version, style, reflowLongStrings, reorderImports, formatJavadoc); + GoogleJavaFormatStep step = new GoogleJavaFormatStep(JarState.promise(() -> JarState.from(groupArtifact + ":" + version, provisioner)), version, style, reflowLongStrings, reorderImports, formatJavadoc, cleanup); if (removeImports) { return FormatterStep.create(name, step, @@ -150,8 +162,12 @@ public static boolean defaultFormatJavadoc() { return DEFAULT_FORMAT_JAVADOC; } + public static boolean defaultCleanup() { + return DEFAULT_CLEANUP; + } + private State equalityState() { - return new State(version, style, jarState.get(), reflowLongStrings, reorderImports, formatJavadoc); + return new State(version, style, jarState.get(), reflowLongStrings, reorderImports, formatJavadoc, cleanup); } private static final class State implements Serializable { @@ -164,13 +180,15 @@ private static final class State implements Serializable { private final boolean reflowLongStrings; private final boolean reorderImports; private final boolean formatJavadoc; + private final boolean cleanup; State(String version, String style, JarState jarState, boolean reflowLongStrings, boolean reorderImports, - boolean formatJavadoc) { + boolean formatJavadoc, + boolean cleanup) { JVM_SUPPORT.assertFormatterSupported(version); ModuleHelper.doOpenInternalPackagesIfRequired(); this.jarState = jarState; @@ -179,13 +197,14 @@ private static final class State implements Serializable { this.reflowLongStrings = reflowLongStrings; this.reorderImports = reorderImports; this.formatJavadoc = formatJavadoc; + this.cleanup = cleanup; } FormatterFunc createFormat() throws Exception { final ClassLoader classLoader = jarState.getClassLoader(); Class formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.java.GoogleJavaFormatFormatterFunc"); - Constructor constructor = formatterFunc.getConstructor(String.class, String.class, boolean.class, boolean.class, boolean.class); - FormatterFunc googleJavaFormatFormatterFunc = (FormatterFunc) constructor.newInstance(version, style, reflowLongStrings, reorderImports, formatJavadoc); + Constructor constructor = formatterFunc.getConstructor(String.class, String.class, boolean.class, boolean.class, boolean.class, boolean.class); + FormatterFunc googleJavaFormatFormatterFunc = (FormatterFunc) constructor.newInstance(version, style, reflowLongStrings, reorderImports, formatJavadoc, cleanup); return JVM_SUPPORT.suggestLaterVersionOnError(version, googleJavaFormatFormatterFunc); } diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index d3249bf262..ea9fa8cd41 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,11 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +- Add `cleanup(false)` to `googleJavaFormat()` to disable google-java-format's non-whitespace cleanups. ([#3157](https://github.com/diffplug/spotless/pull/3157)) +### Changes +- Bump default `google-java-format` version `1.36.1` -> `2.0.0`. ([#3157](https://github.com/diffplug/spotless/pull/3157)) + This version applies some small non-whitespace cleanups by default (removing redundant semicolons, empty annotation parentheses, and parentheses around single lambda parameters), which can be disabled with `cleanup(false)`. ## [8.10.4] - 2026-10-08 diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index f6e9570853..023f78fb2a 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -380,8 +380,9 @@ spotless { googleJavaFormat() // optional: you can specify a specific version (>= 1.8) and/or switch to AOSP style // and/or reflow long strings + // and/or disable non-whitespace cleanups (google-java-format >= 2.0.0) // and/or use custom group artifact (you probably don't need this) - googleJavaFormat('1.8').aosp().reflowLongStrings().formatJavadoc(false).reorderImports(false).groupArtifact('com.google.googlejavaformat:google-java-format') + googleJavaFormat('1.8').aosp().reflowLongStrings().formatJavadoc(false).reorderImports(false).cleanup(false).groupArtifact('com.google.googlejavaformat:google-java-format') ``` ### palantir-java-format diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java index 9411811322..33d9dbac3a 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java @@ -211,6 +211,7 @@ public class GoogleJavaFormatConfig { boolean reflowLongStrings; boolean reorderImports; boolean formatJavadoc = true; + boolean cleanup = true; GoogleJavaFormatConfig(String version) { this.version = Objects.requireNonNull(version); @@ -261,6 +262,13 @@ public GoogleJavaFormatConfig formatJavadoc(boolean formatJavadoc) { return this; } + /** Enables or disables google-java-format's non-whitespace cleanups (google-java-format 2.0.0 and later), which are enabled by default. */ + public GoogleJavaFormatConfig cleanup(boolean cleanup) { + this.cleanup = cleanup; + replaceStep(createStep()); + return this; + } + private FormatterStep createStep() { return GoogleJavaFormatStep.create( groupArtifact, @@ -269,7 +277,8 @@ private FormatterStep createStep() { provisioner(), reflowLongStrings, reorderImports, - formatJavadoc); + formatJavadoc, + cleanup); } } diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 46c259e6e9..59b4ceadbf 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,6 +3,11 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +- Add `false` to `` to disable google-java-format's non-whitespace cleanups. ([#3157](https://github.com/diffplug/spotless/pull/3157)) +### Changes +- Bump default `google-java-format` version `1.36.1` -> `2.0.0`. ([#3157](https://github.com/diffplug/spotless/pull/3157)) + This version applies some small non-whitespace cleanups by default (removing redundant semicolons, empty annotation parentheses, and parentheses around single lambda parameters), which can be disabled with `false`. ## [3.10.4] - 2026-10-08 diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 5c57f5eca9..ea7c4c72cf 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -345,6 +345,7 @@ Types in `java.lang` and in the file's own package are shortened without adding true false false. + false com.google.googlejavaformat:google-java-format diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java index d0a2b24954..e86393f529 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/GoogleJavaFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 DiffPlug + * Copyright 2016-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,9 @@ public class GoogleJavaFormat implements FormatterStepFactory { @Parameter private Boolean formatJavadoc; + @Parameter + private Boolean cleanup; + @Override public FormatterStep newFormatterStep(FormatterStepConfig config) { String groupArtifact = this.groupArtifact != null ? this.groupArtifact : GoogleJavaFormatStep.defaultGroupArtifact(); @@ -49,6 +52,7 @@ public FormatterStep newFormatterStep(FormatterStepConfig config) { boolean reflowLongStrings = this.reflowLongStrings != null ? this.reflowLongStrings : GoogleJavaFormatStep.defaultReflowLongStrings(); boolean reorderImports = this.reorderImports != null ? this.reorderImports : GoogleJavaFormatStep.defaultReorderImports(); boolean formatJavadoc = this.formatJavadoc != null ? this.formatJavadoc : GoogleJavaFormatStep.defaultFormatJavadoc(); - return GoogleJavaFormatStep.create(groupArtifact, version, style, config.getProvisioner(), reflowLongStrings, reorderImports, formatJavadoc); + boolean cleanup = this.cleanup != null ? this.cleanup : GoogleJavaFormatStep.defaultCleanup(); + return GoogleJavaFormatStep.create(groupArtifact, version, style, config.getProvisioner(), reflowLongStrings, reorderImports, formatJavadoc, cleanup); } } diff --git a/testlib/src/main/resources/java/googlejavaformat/TextBlockGjf2.clean b/testlib/src/main/resources/java/googlejavaformat/TextBlockGjf2.clean new file mode 100644 index 0000000000..cf0dc428e0 --- /dev/null +++ b/testlib/src/main/resources/java/googlejavaformat/TextBlockGjf2.clean @@ -0,0 +1,15 @@ +import mylib.UsedA; +import mylib.UsedB; + +public class Java { + public static void main(String[] args) { + var a = + """ + Howdy + Partner! + """; + System.out.println(a); + UsedB.someMethod(); + UsedA.someMethod(); + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java index cae19f4bca..5d42ea0d4b 100644 --- a/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/java/GoogleJavaFormatStepTest.java @@ -34,7 +34,8 @@ class GoogleJavaFormatStepTest extends ResourceHarness { @Test void jvm13Features() throws Exception { try (StepHarness step = StepHarness.forStep(GoogleJavaFormatStep.create(TestProvisioner.mavenCentral()))) { - step.testResource("java/googlejavaformat/TextBlock.dirty", "java/googlejavaformat/TextBlock.clean"); + // google-java-format 2.0.0 indents text block contents even when long strings aren't reflowed + step.testResource("java/googlejavaformat/TextBlock.dirty", defaultVersionIsAtLeast2() ? "java/googlejavaformat/TextBlockGjf2.clean" : "java/googlejavaformat/TextBlock.clean"); } } @@ -117,6 +118,29 @@ void behaviorWithSkipFormatJavadoc() throws Exception { } } + @Test + void behaviorWithCleanup() throws Exception { + String unformatted = "class T {\n @Deprecated() void f() { g((x) -> x);; }\n}\n"; + String withoutCleanups = "class T {\n @Deprecated()\n void f() {\n g((x) -> x);\n ;\n }\n}\n"; + // google-java-format versions before 2.0.0 don't have cleanups + String withCleanups = defaultVersionIsAtLeast2() ? "class T {\n @Deprecated\n void f() {\n g(x -> x);\n }\n}\n" : withoutCleanups; + try (StepHarness step = StepHarness.forStep(createWithCleanup(true))) { + step.test(unformatted, withCleanups); + } + try (StepHarness step = StepHarness.forStep(createWithCleanup(false))) { + step.test(unformatted, withoutCleanups); + } + } + + /** The default version depends on the JVM, e.g. google-java-format 2.0.0 requires Java 21. */ + private static boolean defaultVersionIsAtLeast2() { + return !GoogleJavaFormatStep.defaultVersion().startsWith("1."); + } + + private static FormatterStep createWithCleanup(boolean cleanup) { + return GoogleJavaFormatStep.create(GoogleJavaFormatStep.defaultGroupArtifact(), GoogleJavaFormatStep.defaultVersion(), GoogleJavaFormatStep.defaultStyle(), TestProvisioner.mavenCentral(), GoogleJavaFormatStep.defaultReflowLongStrings(), GoogleJavaFormatStep.defaultReorderImports(), GoogleJavaFormatStep.defaultFormatJavadoc(), cleanup); + } + @Test void behaviorWithCustomGroupArtifact() throws Exception { FormatterStep step = GoogleJavaFormatStep.create(GoogleJavaFormatStep.defaultGroupArtifact(), GoogleJavaFormatStep.defaultVersion(), GoogleJavaFormatStep.defaultStyle(), TestProvisioner.mavenCentral(), false);