diff --git a/CHANGES.md b/CHANGES.md index 66bd56355a..d902cfc748 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( - Bump default `palantir-java-format` version `2.98.0` -> `2.99.0`. ([#3103](https://github.com/diffplug/spotless/pull/3103)) - Bump default `mockito-core` version `5.23.0` -> `5.24.0`. ([#3106](https://github.com/diffplug/spotless/pull/3106)) - Bump default `greclipse` version to latest `4.40` -> `4.41`. ([#3123](https://github.com/diffplug/spotless/pull/3123)) +- Bump default `ktfmt` version `0.64` -> `0.65`. ([#3135](https://github.com/diffplug/spotless/pull/3135)) + Which moved to the `org.jetbrains.kotlinx:ktfmt` coordinate and `org.jetbrains.kotlinx.ktfmt` package. Older versions are still supported. ### Fixed - `shortenFullyQualifiedTypes` leaves a qualified name alone when the enclosing type extends or implements a type that is not in the same file, so an inherited member type cannot take the simple name. ([#3117](https://github.com/diffplug/spotless/issues/3117)) diff --git a/build-logic/src/main/kotlin/spotless.spotless-conventions.gradle.kts b/build-logic/src/main/kotlin/spotless.spotless-conventions.gradle.kts index 86f5fbe9ce..398b576af3 100644 --- a/build-logic/src/main/kotlin/spotless.spotless-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/spotless.spotless-conventions.gradle.kts @@ -2,6 +2,10 @@ plugins { id("com.diffplug.spotless") } +// The released Spotless plugin used here only knows the legacy `com.facebook:ktfmt` coordinate. +// TODO: switch back to `libs.ktfmt` once a Spotless release supporting ktfmt 0.65+ is used. +val KTFMT_VERSION = "0.64" + spotless { if (project != rootProject) { java { @@ -23,11 +27,11 @@ spotless { } kotlin { target("build-logic/src/**/*.kt") - ktfmt(libs.ktfmt.get().version) + ktfmt(KTFMT_VERSION) } kotlinGradle { target("*.gradle.kts", "build-logic/*.gradle.kts", "build-logic/src/**/*.gradle.kts") - ktfmt(libs.ktfmt.get().version) + ktfmt(KTFMT_VERSION) } groovyGradle { target("*.gradle", "gradle/*.gradle") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9c36d9206..41ce0c4b94 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,7 +55,7 @@ gherkin-utils = "io.cucumber:gherkin-utils:12.0.2" google-java-format = "com.google.googlejavaformat:google-java-format:1.36.1" gson = "com.google.code.gson:gson:2.14.0" json-simple = "org.json:json:20210307" -ktfmt = "com.facebook:ktfmt:0.64" +ktfmt = "org.jetbrains.kotlinx:ktfmt:0.65" 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" sortpom-sorter = "com.github.ekryd.sortpom:sortpom-sorter:4.0.0" diff --git a/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormatterFunc.java b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormatterFunc.java index b99af17015..32db66f126 100644 --- a/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormatterFunc.java +++ b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtFormatterFunc.java @@ -15,11 +15,15 @@ */ package com.diffplug.spotless.glue.ktfmt; +import java.io.File; + import javax.annotation.Nonnull; import javax.annotation.Nullable; -import com.facebook.ktfmt.format.Formatter; -import com.facebook.ktfmt.format.FormattingOptions; +import org.jetbrains.kotlinx.ktfmt.format.FileType; +import org.jetbrains.kotlinx.ktfmt.format.Formatter; +import org.jetbrains.kotlinx.ktfmt.format.FormattingOptions; +import org.jetbrains.kotlinx.ktfmt.format.KotlinCode; import com.diffplug.spotless.FormatterFunc; @@ -50,15 +54,26 @@ public KtfmtFormatterFunc(@Nonnull KtfmtStyle style, @Nullable KtfmtFormattingOp @Nonnull @Override public String apply(@Nonnull String input) throws Exception { - return Formatter.format(createFormattingOptions(), input); + return format(input, FileType.REGULAR); + } + + @Nonnull + @Override + public String apply(@Nonnull String input, @Nonnull File file) throws Exception { + FileType fileType = file.getName().endsWith("." + FileType.SCRIPT.getExtension()) ? FileType.SCRIPT : FileType.REGULAR; + return format(input, fileType); + } + + private String format(String input, FileType fileType) throws Exception { + return Formatter.format(createFormattingOptions(), KotlinCode.Companion.from(input, fileType)); } private FormattingOptions createFormattingOptions() throws Exception { FormattingOptions formattingOptions = switch (style) { - case META -> Formatter.META_FORMAT; - case GOOGLE -> Formatter.GOOGLE_FORMAT; - case KOTLIN_LANG -> Formatter.KOTLINLANG_FORMAT; - default -> throw new IllegalStateException("Unknown formatting option " + style); + case META -> Formatter.META_FORMAT; + case GOOGLE -> Formatter.GOOGLE_FORMAT; + case KOTLIN_LANG -> Formatter.KOTLINLANG_FORMAT; + default -> throw new IllegalStateException("Unknown formatting option " + style); }; if (ktfmtFormattingOptions == null) { diff --git a/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtTrailingCommaManagementStrategy.java b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtTrailingCommaManagementStrategy.java index f9ca437eee..3816d88e87 100644 --- a/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtTrailingCommaManagementStrategy.java +++ b/lib/src/ktfmt/java/com/diffplug/spotless/glue/ktfmt/KtfmtTrailingCommaManagementStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 DiffPlug + * Copyright 2025-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,16 +15,16 @@ */ package com.diffplug.spotless.glue.ktfmt; -import com.facebook.ktfmt.format.TrailingCommaManagementStrategy; +import org.jetbrains.kotlinx.ktfmt.format.TrailingCommaManagementStrategy; public enum KtfmtTrailingCommaManagementStrategy { NONE, ONLY_ADD, COMPLETE; public TrailingCommaManagementStrategy toFormatterTrailingCommaManagementStrategy() { return switch (this) { - case NONE -> TrailingCommaManagementStrategy.NONE; - case ONLY_ADD -> TrailingCommaManagementStrategy.ONLY_ADD; - case COMPLETE -> TrailingCommaManagementStrategy.COMPLETE; + case NONE -> TrailingCommaManagementStrategy.NONE; + case ONLY_ADD -> TrailingCommaManagementStrategy.ONLY_ADD; + case COMPLETE -> TrailingCommaManagementStrategy.COMPLETE; }; } } diff --git a/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java b/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java index 1984b58097..c3e63f7447 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java @@ -36,14 +36,18 @@ import com.diffplug.spotless.ThrowingEx; /** - * Wraps up ktfmt as a FormatterStep. + * Wraps up ktfmt as a FormatterStep. */ public final class KtfmtStep implements Serializable { @Serial private static final long serialVersionUID = 1L; private static final String NAME = "ktfmt"; - private static final String MAVEN_COORDINATE = "com.facebook:ktfmt:"; + /** + * Since 0.65, ktfmt is published under the {@code org.jetbrains.kotlinx} group with the {@code org.jetbrains.kotlinx.ktfmt} package. + */ + private static final String MAVEN_COORDINATE = "org.jetbrains.kotlinx:ktfmt:"; + private static final String MAVEN_COORDINATE_LEGACY = "com.facebook:ktfmt:"; private final String version; /** @@ -69,7 +73,7 @@ private KtfmtStep(String version, /** * Used to allow multiple style option through formatting options and since when is each of them available. * - * @see ktfmt source + * @see ktfmt source */ public enum Style { // @formatter:off @@ -213,7 +217,7 @@ public static FormatterStep create(String version, Provisioner provisioner, @Nul Objects.requireNonNull(version, "version"); Objects.requireNonNull(provisioner, "provisioner"); return FormatterStep.create(NAME, - new KtfmtStep(version, JarState.promise(() -> JarState.from(MAVEN_COORDINATE + version, provisioner)), style, options), + new KtfmtStep(version, JarState.promise(() -> JarState.from(mavenCoordinate(version) + version, provisioner)), style, options), KtfmtStep::equalityState, State::createFormat); } @@ -222,6 +226,17 @@ public static String defaultVersion() { return KotlinBuildConfig.VERSION_KTFMT; } + private static String mavenCoordinate(String version) { + return isLegacyPackage(version) ? MAVEN_COORDINATE_LEGACY : MAVEN_COORDINATE; + } + + /** + * Versions before 0.65 use the {@code com.facebook} group and package, so they can't use the glue code compiled against the latest ktfmt. + */ + private static boolean isLegacyPackage(String version) { + return BadSemver.version(version) < BadSemver.version(0, 65); + } + private State equalityState() { return new State(version, jarState.get(), style, options); } @@ -249,11 +264,7 @@ private static final class State implements Serializable { FormatterFunc createFormat() throws Exception { final ClassLoader classLoader = jarState.getClassLoader(); - if (BadSemver.version(version) < BadSemver.version(0, 51)) { - return new KtfmtFormatterFuncCompat(version, style, options, classLoader).getFormatterFunc(); - } - - if (options != null && BadSemver.version(version) < BadSemver.version(0, 63)) { + if (isLegacyPackage(version)) { return new KtfmtFormatterFuncCompat(version, style, options, classLoader).getFormatterFunc(); } @@ -342,13 +353,16 @@ private String getKtfmtStyleOption(Style style) { } } + /** + * Reflection-based formatter for ktfmt versions before 0.65, which use the {@code com.facebook.ktfmt} package. + */ private static final class KtfmtFormatterFuncCompat { private static final String PACKAGE = "com.facebook.ktfmt"; /** * The format method is available in the link below. * - * @see ktfmt source + * @see ktfmt source */ static final String FORMATTER_METHOD = "format"; @@ -430,7 +444,7 @@ private Object getCustomFormattingOptions(Class formatterClass) throws Except /* manageTrailingCommas = */ Objects.requireNonNullElse(getManageTrailingCommasFrom(options.trailingCommaManagementStrategy), (Boolean) formattingOptionsClass.getMethod("getManageTrailingCommas").invoke(formattingOptions)), /* removeUnusedImports = */ Objects.requireNonNullElse(options.removeUnusedImports, (Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions)); - } else { + } else if (BadSemver.version(version) < BadSemver.version(0, 63)) { Class trailingCommaManagementStrategyClass = getTrailingCommaManagementStrategyClazz(); Object trailingCommaManagementStrategy = options.trailingCommaManagementStrategy == null ? formattingOptionsClass.getMethod("getTrailingCommaManagementStrategy").invoke(formattingOptions) @@ -442,6 +456,28 @@ private Object getCustomFormattingOptions(Class formatterClass) throws Except /* trailingCommaManagementStrategy = */ trailingCommaManagementStrategy, /* removeUnusedImports = */ Objects.requireNonNullElse(options.removeUnusedImports, (Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions)); + } else { + // FormattingOptions.Builder is available since 0.63, and keeps the options not managed here (e.g. preserveLambdaBreaks) from the style. + Object builder = formattingOptionsClass.getMethod("toBuilder").invoke(formattingOptions); + Class builderClass = builder.getClass(); + if (options.maxWidth != null) { + builderClass.getMethod("maxWidth", int.class).invoke(builder, options.maxWidth); + } + if (options.blockIndent != null) { + builderClass.getMethod("blockIndent", int.class).invoke(builder, options.blockIndent); + } + if (options.continuationIndent != null) { + builderClass.getMethod("continuationIndent", int.class).invoke(builder, options.continuationIndent); + } + if (options.trailingCommaManagementStrategy != null) { + Class trailingCommaManagementStrategyClass = getTrailingCommaManagementStrategyClazz(); + builderClass.getMethod("trailingCommaManagementStrategy", trailingCommaManagementStrategyClass).invoke(builder, + Enum.valueOf((Class) trailingCommaManagementStrategyClass, options.trailingCommaManagementStrategy.name())); + } + if (options.removeUnusedImports != null) { + builderClass.getMethod("removeUnusedImports", boolean.class).invoke(builder, options.removeUnusedImports); + } + formattingOptions = builderClass.getMethod("build").invoke(builder); } } diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index ad18e213ca..a053b13358 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -11,6 +11,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( - Bump default `palantir-java-format` version `2.98.0` -> `2.99.0`. ([#3103](https://github.com/diffplug/spotless/pull/3103)) - Bump default `mockito-core` version `5.23.0` -> `5.24.0`. ([#3106](https://github.com/diffplug/spotless/pull/3106)) - Bump default `greclipse` version to latest `4.40` -> `4.41`. ([#3123](https://github.com/diffplug/spotless/pull/3123)) +- Bump default `ktfmt` version `0.64` -> `0.65`. ([#3135](https://github.com/diffplug/spotless/pull/3135)) + Which moved to the `org.jetbrains.kotlinx:ktfmt` coordinate and `org.jetbrains.kotlinx.ktfmt` package. Older versions are still supported. ### Fixed - `shortenFullyQualifiedTypes` leaves a qualified name alone when the enclosing type extends or implements a type that is not in the same file, so an inherited member type cannot take the simple name. ([#3117](https://github.com/diffplug/spotless/issues/3117)) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index cba7a85b35..44fbf1a3be 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -626,7 +626,7 @@ spotless { // if you are using build.gradle.kts, instead of 'spotless {' use: ### ktfmt -[homepage](https://github.com/facebook/ktfmt). [changelog](https://github.com/facebook/ktfmt/releases). +[homepage](https://github.com/Kotlin/ktfmt). [changelog](https://github.com/Kotlin/ktfmt/releases). ```kotlin spotless { diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java index c3dc731215..2c99624d00 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/BaseKotlinExtension.java @@ -57,13 +57,13 @@ public KtlintConfig ktlint(String version) throws IOException { return new KtlintConfig(version, Collections.emptyMap(), Collections.emptyList()); } - /** Uses the ktfmt jar to format source code. */ + /** Uses the ktfmt jar to format source code. */ public KtfmtConfig ktfmt() { return ktfmt(KtfmtStep.defaultVersion()); } /** - * Uses the given version of ktfmt and applies the dropbox style + * Uses the given version of ktfmt and applies the dropbox style * option to format source code. */ public KtfmtConfig ktfmt(String version) { diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 28cfea7f4d..19a9b05091 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -11,6 +11,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( - Bump default `palantir-java-format` version `2.98.0` -> `2.99.0`. ([#3103](https://github.com/diffplug/spotless/pull/3103)) - Bump default `mockito-core` version `5.23.0` -> `5.24.0`. ([#3106](https://github.com/diffplug/spotless/pull/3106)) - Bump default `greclipse` version to latest `4.40` -> `4.41`. ([#3123](https://github.com/diffplug/spotless/pull/3123)) +- Bump default `ktfmt` version `0.64` -> `0.65`. ([#3135](https://github.com/diffplug/spotless/pull/3135)) + Which moved to the `org.jetbrains.kotlinx:ktfmt` coordinate and `org.jetbrains.kotlinx.ktfmt` package. Older versions are still supported. ### Fixed - Migrate Plexus component injection to JSR-330 and Maven parameter. ([#3122](https://github.com/diffplug/spotless/pull/3122)) diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 896bfbb61c..c40876d0d5 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -575,7 +575,7 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ### ktfmt -[homepage](https://github.com/facebook/ktfmt). [changelog](https://github.com/facebook/ktfmt/releases). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktfmt.java). +[homepage](https://github.com/Kotlin/ktfmt). [changelog](https://github.com/Kotlin/ktfmt/releases). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktfmt.java). ```xml diff --git a/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java b/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java index 0707a8fa09..1492624a9b 100644 --- a/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java @@ -64,6 +64,20 @@ void behaviorWithOptions_0_62() { StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); } + @Test + void behavior_0_64() throws Exception { + FormatterStep step = KtfmtStep.create("0.64", TestProvisioner.mavenCentral()); + StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); + } + + @Test + void behaviorWithOptions_0_64() { + KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions(); + options.setMaxWidth(100); + FormatterStep step = KtfmtStep.create("0.64", TestProvisioner.mavenCentral(), KtfmtStep.Style.GOOGLE, options); + StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); + } + @Test void behaviorWithOptions_0_53() { KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions();