diff --git a/.github/scripts/update_renovate_changelogs.py b/.github/scripts/update_renovate_changelogs.py index 61c64f425c..c397a757a5 100755 --- a/.github/scripts/update_renovate_changelogs.py +++ b/.github/scripts/update_renovate_changelogs.py @@ -23,6 +23,7 @@ ("libraries", "gson"): "gson", ("libraries", "json-simple"): "json-simple", ("libraries", "ktfmt"): "ktfmt", + ("libraries", "open-java-format"): "open-java-format", ("libraries", "prince-of-space-core"): "prince-of-space", ("libraries", "scalafmt-core"): "scalafmt", ("libraries", "sortpom-sorter"): "sortpom", diff --git a/CHANGES.md b/CHANGES.md index 75ae39ee0e..18ed590ecf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] +### Added +- `OpenJavaFormatStep` for [open-java-format](https://github.com/openjavaformat/open-java-format), a fork of palantir-java-format. Requires JDK 21+. + ### Changes - Generate formatter defaults from version catalog. ([#3045](https://github.com/diffplug/spotless/pull/3045)) - Bump default `gson` version `2.13.2` -> `2.14.0`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) diff --git a/README.md b/README.md index 921c50d2c5..d566491f61 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ lib('gherkin.GherkinUtilsStep') +'{{yes}} | {{yes}} extra('groovy.GrEclipseFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('java.GoogleJavaFormatStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('java.ImportOrderStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', +lib('java.OpenJavaFormatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('java.PalantirJavaFormatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('java.PrinceOfSpaceStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', @@ -144,6 +145,7 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}} | [`groovy.GrEclipseFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/groovy/GrEclipseFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.GoogleJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/GoogleJavaFormatStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.ImportOrderStep`](lib/src/main/java/com/diffplug/spotless/java/ImportOrderStep.java) | :+1: | :+1: | :+1: | :white_large_square: | +| [`java.OpenJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/OpenJavaFormatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`java.PalantirJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`java.PrinceOfSpaceStep`](lib/src/main/java/com/diffplug/spotless/java/PrinceOfSpaceStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`java.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: | diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2da62c3667..4fa8f2602e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,6 +62,7 @@ google-java-format = "com.google.googlejavaformat:google-java-format:1.30.0" gson = "com.google.code.gson:gson:2.14.0" json-simple = "org.json:json:20210307" ktfmt = "com.facebook:ktfmt:0.64" +open-java-format = "dev.openjavaformat:open-java-format:2.98.0.1" palantir-java-format = "com.palantir.javaformat:palantir-java-format:1.1.0" prince-of-space-core = "io.github.agustafson.princeofspace:prince-of-space-core:2.2.0" scalafmt-core = "org.scalameta:scalafmt-core_2.13:3.11.5" diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index f5b6e93ef2..26f1a206a7 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -22,6 +22,7 @@ buildConfig { buildConfigField("VERSION_CLEANTHAT", libs.cleanthat.java.version) buildConfigField("VERSION_GJF", libs.google.java.format.version) buildConfigField("VERSION_JAVAPARSER", libs.javaparser.symbol.solver.core.version) + buildConfigField("VERSION_OPEN_JAVA_FORMAT", libs.open.java.format.version) buildConfigField("VERSION_PALANTIR_JAVA_FORMAT", libs.palantir.java.format.default.version) buildConfigField("VERSION_PRINCE_OF_SPACE", libs.prince.of.space.core.version) buildConfigField("VERSION_TABLETEST_FORMATTER", libs.tabletest.formatter.core.version) diff --git a/lib/src/main/java/com/diffplug/spotless/java/OpenJavaFormatStep.java b/lib/src/main/java/com/diffplug/spotless/java/OpenJavaFormatStep.java new file mode 100644 index 0000000000..2878b19b15 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/java/OpenJavaFormatStep.java @@ -0,0 +1,139 @@ +/* + * Copyright 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.java; + +import java.io.Serial; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.util.Objects; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.JarState; +import com.diffplug.spotless.Jvm; +import com.diffplug.spotless.Provisioner; + +/** Wraps up open-java-format, a fork of + * palantir-java-format, as a FormatterStep. */ +public final class OpenJavaFormatStep implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + private static final boolean DEFAULT_FORMAT_JAVADOC = false; + private static final String DEFAULT_STYLE = "OJF"; + private static final String NAME = "open-java-format"; + public static final String MAVEN_COORDINATE = "dev.openjavaformat:open-java-format:"; + public static final String DEFAULT_VERSION = BuildConfig.VERSION_OPEN_JAVA_FORMAT; // requires Java 21+ + + /** The jar that contains the formatter. */ + private final JarState.Promised jarState; + /** Version of the formatter jar. */ + private final String formatterVersion; + private final String style; + /** Whether to format Java docs. */ + private final boolean formatJavadoc; + + private OpenJavaFormatStep(JarState.Promised jarState, + String formatterVersion, + String style, + boolean formatJavadoc) { + this.jarState = jarState; + this.formatterVersion = formatterVersion; + this.style = style; + this.formatJavadoc = formatJavadoc; + } + + /** Creates a step which formats everything - code, import order, and unused imports. */ + public static FormatterStep create(Provisioner provisioner) { + return create(defaultVersion(), provisioner); + } + + /** Creates a step which formats everything - code, import order, and unused imports. */ + public static FormatterStep create(String version, Provisioner provisioner) { + return create(version, defaultStyle(), provisioner); + } + + /** + * Creates a step which formats code, import order, and unused imports, but not Java docs. And with the given format + * style. + */ + public static FormatterStep create(String version, String style, Provisioner provisioner) { + return create(version, style, DEFAULT_FORMAT_JAVADOC, provisioner); + } + + /** + * Creates a step which formats everything - code, import order, unused imports, and Java docs. And with the given + * format style. + */ + public static FormatterStep create(String version, String style, boolean formatJavadoc, Provisioner provisioner) { + Objects.requireNonNull(version, "version"); + Objects.requireNonNull(style, "style"); + Objects.requireNonNull(provisioner, "provisioner"); + + return FormatterStep.create(NAME, + new OpenJavaFormatStep(JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner)), version, style, formatJavadoc), + OpenJavaFormatStep::equalityState, + State::createFormat); + } + + /** Get default formatter version */ + public static String defaultVersion() { + return DEFAULT_VERSION; + } + + /** Get default style */ + public static String defaultStyle() { + return DEFAULT_STYLE; + } + + /** Get default for whether Java docs should be formatted */ + public static boolean defaultFormatJavadoc() { + return DEFAULT_FORMAT_JAVADOC; + } + + private State equalityState() { + return new State(jarState.get(), formatterVersion, style, formatJavadoc); + } + + private static final class State implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + + private final JarState jarState; + private final String formatterVersion; + private final String style; + private final boolean formatJavadoc; + + State(JarState jarState, String formatterVersion, String style, boolean formatJavadoc) { + if (Jvm.version() < 21) { + throw new IllegalStateException("open-java-format requires a JDK 21+ host runtime, this JVM is " + Jvm.version()); + } + ModuleHelper.doOpenInternalPackagesIfRequired(); + this.jarState = jarState; + this.formatterVersion = formatterVersion; + this.style = style; + this.formatJavadoc = formatJavadoc; + } + + FormatterFunc createFormat() throws Exception { + final ClassLoader classLoader = jarState.getClassLoader(); + // open-java-format keeps the palantir-java-format API, so the palantir glue drives it + final Class> formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.pjf.PalantirJavaFormatFormatterFunc"); + // 1st arg is "style", 2nd arg is "formatJavadoc" + final Constructor> constructor = formatterFunc.getConstructor(String.class, boolean.class); + return (FormatterFunc) constructor.newInstance(style, formatJavadoc); + } + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 66f5fb2d0b..348dc79303 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +- `openJavaFormat()` for [open-java-format](https://github.com/openjavaformat/open-java-format), a fork of palantir-java-format. Requires JDK 21+. + ### Changes - Generate formatter defaults from version catalog. ([#3045](https://github.com/diffplug/spotless/pull/3045)) - Bump default `gson` version `2.13.2` -> `2.14.0`. ([#3045](https://github.com/diffplug/spotless/pull/3045)) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index db4db7c376..4333064604 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -56,7 +56,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [Git hook (optional)](#git-hook) - [Linting](#linting) - **Languages** - - [Java](#java) ([removeUnusedImports](#removeunusedimports), [forbidWildcardImports](#forbidwildcardimports), [expandWildcardImports](#expandwildcardimports), [forbidModuleImports](#forbidmoduleimports), [shortenFullyQualifiedTypes](#shortenfullyqualifiedtypes), [google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [prince-of-space](#prince-of-space), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [tabletest-formatter](#tabletest-formatter), [IntelliJ IDEA](#intellij-idea)) + - [Java](#java) ([removeUnusedImports](#removeunusedimports), [forbidWildcardImports](#forbidwildcardimports), [expandWildcardImports](#expandwildcardimports), [forbidModuleImports](#forbidmoduleimports), [shortenFullyQualifiedTypes](#shortenfullyqualifiedtypes), [google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [open-java-format](#open-java-format), [palantir-java-format](#palantir-java-format), [prince-of-space](#prince-of-space), [formatAnnotations](#formatAnnotations), [cleanthat](#cleanthat), [tabletest-formatter](#tabletest-formatter), [IntelliJ IDEA](#intellij-idea)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [tabletest-formatter](#tabletest-formatter-1), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) @@ -361,6 +361,19 @@ spotless { googleJavaFormat('1.8').aosp().reflowLongStrings().formatJavadoc(false).reorderImports(false).groupArtifact('com.google.googlejavaformat:google-java-format') ``` +### open-java-format + +[homepage](https://github.com/openjavaformat/open-java-format). [changelog](https://github.com/openjavaformat/open-java-format/releases). Requires a JDK 21+ host runtime. +```gradle +spotless { + java { + openJavaFormat() + // optional: you can specify a specific version and/or switch to AOSP/GOOGLE style + openJavaFormat('2.98.0.1').style("GOOGLE") + // optional: you can also format Javadocs + openJavaFormat('2.98.0.1').formatJavadoc(true) +``` + ### palantir-java-format [homepage](https://github.com/palantir/palantir-java-format). [changelog](https://github.com/palantir/palantir-java-format/releases). 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..6dee78c365 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 @@ -44,6 +44,7 @@ import com.diffplug.spotless.java.FormatAnnotationsStep; import com.diffplug.spotless.java.GoogleJavaFormatStep; import com.diffplug.spotless.java.ImportOrderStep; +import com.diffplug.spotless.java.OpenJavaFormatStep; import com.diffplug.spotless.java.PalantirJavaFormatStep; import com.diffplug.spotless.java.PrinceOfSpaceStep; import com.diffplug.spotless.java.RemoveUnusedImportsStep; @@ -273,6 +274,50 @@ private FormatterStep createStep() { } } + /** Uses the open-java-format jar to format source code. */ + public OpenJavaFormatConfig openJavaFormat() { + return openJavaFormat(OpenJavaFormatStep.defaultVersion()); + } + + /** + * Uses the given version of open-java-format to format source code. + *
+ * Limited to published versions. See issue #33
+ * for a workaround for using snapshot versions.
+ */
+ public OpenJavaFormatConfig openJavaFormat(String version) {
+ Objects.requireNonNull(version);
+ return new OpenJavaFormatConfig(version);
+ }
+
+ public class OpenJavaFormatConfig {
+ final String version;
+ String style;
+ boolean formatJavadoc;
+
+ OpenJavaFormatConfig(String version) {
+ this.version = Objects.requireNonNull(version);
+ this.style = OpenJavaFormatStep.defaultStyle();
+ addStep(createStep());
+ }
+
+ public OpenJavaFormatConfig style(String style) {
+ this.style = Objects.requireNonNull(style);
+ replaceStep(createStep());
+ return this;
+ }
+
+ public OpenJavaFormatConfig formatJavadoc(boolean formatJavadoc) {
+ this.formatJavadoc = formatJavadoc;
+ replaceStep(createStep());
+ return this;
+ }
+
+ private FormatterStep createStep() {
+ return OpenJavaFormatStep.create(version, style, formatJavadoc, provisioner());
+ }
+ }
+
/** Uses the palantir-java-format jar to format source code. */
public PalantirJavaFormatConfig palantirJavaFormat() {
return palantirJavaFormat(PalantirJavaFormatStep.defaultVersion());
diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/OpenJavaFormatIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/OpenJavaFormatIntegrationTest.java
new file mode 100644
index 0000000000..8626365d55
--- /dev/null
+++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/OpenJavaFormatIntegrationTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 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.gradle.spotless;
+
+import static org.junit.jupiter.api.condition.JRE.JAVA_21;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledForJreRange;
+
+@EnabledForJreRange(min = JAVA_21) // open-java-format is compiled for Java 21
+class OpenJavaFormatIntegrationTest extends GradleIntegrationHarness {
+ @Test
+ void integration() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "",
+ "spotless {",
+ " java {",
+ " target file('test.java')",
+ " openJavaFormat()",
+ " }",
+ "}");
+
+ setFile("test.java").toResource("java/palantirjavaformat/JavaCodeUnformatted.test");
+ gradleRunner().withArguments("spotlessApply").build();
+ assertFile("test.java").sameAsResource("java/palantirjavaformat/JavaCodeFormatted.test");
+
+ checkRunsThenUpToDate();
+ }
+
+ @Test
+ void formatJavaDoc() throws IOException {
+ setFile("build.gradle").toLines(
+ "plugins {",
+ " id 'com.diffplug.spotless'",
+ "}",
+ "repositories { mavenCentral() }",
+ "",
+ "spotless {",
+ " java {",
+ " target file('test.java')",
+ " openJavaFormat('2.98.0.1').formatJavadoc(true)",
+ " }",
+ "}");
+
+ setFile("test.java").toResource("java/palantirjavaformat/JavaCodeWithJavaDocUnformatted.test");
+ gradleRunner().withArguments("spotlessApply").build();
+ assertFile("test.java").sameAsResource("java/palantirjavaformat/JavaCodeWithJavaDocFormatted.test");
+
+ checkRunsThenUpToDate();
+ }
+}
diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md
index 674a6ee2ed..8158f8e59c 100644
--- a/plugin-maven/CHANGES.md
+++ b/plugin-maven/CHANGES.md
@@ -3,6 +3,9 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`).
## [Unreleased]
+### Added
+- `