From 0779ea6383cf3f433bbacac29e0e29019193b65b Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Wed, 3 Jun 2026 10:31:01 -0400 Subject: [PATCH] Support `ktfmt` 0.63 and use its new builder API This updates ktfmt to 0.63 and uses its new `FormattingOptions.Builder` API I added to better avoid ABI incompatibilities in the future. Disclosure: some of the grunt work here was done with the help of AI but I've cleaned things up as needed after to ensure no fluff! --- CHANGES.md | 2 + gradle/libs.versions.toml | 2 +- .../glue/ktfmt/KtfmtFormatterFunc.java | 24 +++++------ .../diffplug/spotless/kotlin/KtfmtStep.java | 32 +++++++++++---- .../spotless/kotlin/KtfmtStepTest.java | 40 ++++++++++++++++++- 5 files changed, 79 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 41d3df2dfb..f03de5e9f8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ 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] +### Fixed +- Support `ktfmt` 0.63 and use its new builder API for formatting options to better avoid future breaking changes. ## [4.6.2] - 2026-05-27 ### Fixed diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82ce64f52c..1c9f4933c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,7 +51,7 @@ gherkin-utils = "io.cucumber:gherkin-utils:10.0.0" google-java-format = "com.google.googlejavaformat:google-java-format:1.28.0" gson = "com.google.code.gson:gson:2.13.2" javaparser-symbol-solver-core = "com.github.javaparser:javaparser-symbol-solver-core:3.27.1" -ktfmt = "com.facebook:ktfmt:0.61" +ktfmt = "com.facebook:ktfmt:0.63" palantir-java-format = "com.palantir.javaformat:palantir-java-format:1.1.0" scalafmt-core = "org.scalameta:scalafmt-core_2.13:3.8.1" 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 4b8308e47c..b99af17015 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 @@ -1,5 +1,5 @@ /* - * Copyright 2022-2025 DiffPlug + * Copyright 2022-2026 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,18 +61,18 @@ private FormattingOptions createFormattingOptions() throws Exception { default -> throw new IllegalStateException("Unknown formatting option " + style); }; - if (ktfmtFormattingOptions != null) { - formattingOptions = formattingOptions.copy( - ktfmtFormattingOptions.getMaxWidth().orElse(formattingOptions.getMaxWidth()), - ktfmtFormattingOptions.getBlockIndent().orElse(formattingOptions.getBlockIndent()), - ktfmtFormattingOptions.getContinuationIndent().orElse(formattingOptions.getContinuationIndent()), - ktfmtFormattingOptions.getTrailingCommaManagementStrategy() - .map(KtfmtTrailingCommaManagementStrategy::toFormatterTrailingCommaManagementStrategy) - .orElse(formattingOptions.getTrailingCommaManagementStrategy()), - ktfmtFormattingOptions.getRemoveUnusedImports().orElse(formattingOptions.getRemoveUnusedImports()), - formattingOptions.getDebuggingPrintOpsAfterFormatting()); + if (ktfmtFormattingOptions == null) { + return formattingOptions; } - return formattingOptions; + FormattingOptions.Builder builder = formattingOptions.toBuilder(); + ktfmtFormattingOptions.getMaxWidth().ifPresent(builder::maxWidth); + ktfmtFormattingOptions.getBlockIndent().ifPresent(builder::blockIndent); + ktfmtFormattingOptions.getContinuationIndent().ifPresent(builder::continuationIndent); + ktfmtFormattingOptions.getTrailingCommaManagementStrategy() + .map(KtfmtTrailingCommaManagementStrategy::toFormatterTrailingCommaManagementStrategy) + .ifPresent(builder::trailingCommaManagementStrategy); + ktfmtFormattingOptions.getRemoveUnusedImports().ifPresent(builder::removeUnusedImports); + return builder.build(); } } 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 e3d9a4b829..689332d2a7 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java @@ -41,7 +41,7 @@ public final class KtfmtStep implements Serializable { @Serial private static final long serialVersionUID = 1L; - private static final String DEFAULT_VERSION = "0.61"; + private static final String DEFAULT_VERSION = "0.63"; private static final String NAME = "ktfmt"; private static final String MAVEN_COORDINATE = "com.facebook:ktfmt:"; @@ -253,6 +253,10 @@ FormatterFunc createFormat() throws Exception { return new KtfmtFormatterFuncCompat(version, style, options, classLoader).getFormatterFunc(); } + if (options != null && BadSemver.version(version) < BadSemver.version(0, 63)) { + return new KtfmtFormatterFuncCompat(version, style, options, classLoader).getFormatterFunc(); + } + final Class formatterFuncClass = classLoader.loadClass("com.diffplug.spotless.glue.ktfmt.KtfmtFormatterFunc"); final Class ktfmtStyleClass = classLoader.loadClass("com.diffplug.spotless.glue.ktfmt.KtfmtStyle"); final Class ktfmtFormattingOptionsClass = classLoader.loadClass("com.diffplug.spotless.glue.ktfmt.KtfmtFormattingOptions"); @@ -408,7 +412,7 @@ private Object getCustomFormattingOptions(Class formatterClass) throws Except /* continuationIndent = */ Objects.requireNonNullElse(options.continuationIndent, (Integer) formattingOptionsClass.getMethod("getContinuationIndent").invoke(formattingOptions)), /* removeUnusedImports = */ Objects.requireNonNullElse(options.removeUnusedImports, (Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions)); - } else if (BadSemver.version(version) < BadSemver.version(0, 57)) { + } else if (BadSemver.version(version) < BadSemver.version(0, 51)) { Class styleClass = classLoader.loadClass(formattingOptionsClass.getName() + "$Style"); formattingOptions = formattingOptions.getClass().getConstructor(styleClass, int.class, int.class, int.class, boolean.class, boolean.class, boolean.class).newInstance( /* style = */ formattingOptionsClass.getMethod("getStyle").invoke(formattingOptions), @@ -418,16 +422,26 @@ private Object getCustomFormattingOptions(Class formatterClass) throws Except /* removeUnusedImports = */ Objects.requireNonNullElse(options.removeUnusedImports, (Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions), /* manageTrailingCommas = */ Objects.requireNonNullElse(getManageTrailingCommasFrom(options.trailingCommaManagementStrategy), (Boolean) formattingOptionsClass.getMethod("getManageTrailingCommas").invoke(formattingOptions))); + } else if (BadSemver.version(version) < BadSemver.version(0, 57)) { + formattingOptions = formattingOptions.getClass().getConstructor(int.class, int.class, int.class, boolean.class, boolean.class, boolean.class).newInstance( + /* maxWidth = */ Objects.requireNonNullElse(options.maxWidth, (Integer) formattingOptionsClass.getMethod("getMaxWidth").invoke(formattingOptions)), + /* blockIndent = */ Objects.requireNonNullElse(options.blockIndent, (Integer) formattingOptionsClass.getMethod("getBlockIndent").invoke(formattingOptions)), + /* continuationIndent = */ Objects.requireNonNullElse(options.continuationIndent, (Integer) formattingOptionsClass.getMethod("getContinuationIndent").invoke(formattingOptions)), + /* 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 { - Class styleClass = classLoader.loadClass(formattingOptionsClass.getName() + "$Style"); - formattingOptions = formattingOptions.getClass().getConstructor(styleClass, int.class, int.class, int.class, boolean.class, boolean.class, TrailingCommaManagementStrategy.class).newInstance( - /* style = */ formattingOptionsClass.getMethod("getStyle").invoke(formattingOptions), + Class trailingCommaManagementStrategyClass = getTrailingCommaManagementStrategyClazz(); + Object trailingCommaManagementStrategy = options.trailingCommaManagementStrategy == null + ? formattingOptionsClass.getMethod("getTrailingCommaManagementStrategy").invoke(formattingOptions) + : Enum.valueOf((Class) trailingCommaManagementStrategyClass, options.trailingCommaManagementStrategy.name()); + formattingOptions = formattingOptions.getClass().getConstructor(int.class, int.class, int.class, trailingCommaManagementStrategyClass, boolean.class, boolean.class).newInstance( /* maxWidth = */ Objects.requireNonNullElse(options.maxWidth, (Integer) formattingOptionsClass.getMethod("getMaxWidth").invoke(formattingOptions)), /* blockIndent = */ Objects.requireNonNullElse(options.blockIndent, (Integer) formattingOptionsClass.getMethod("getBlockIndent").invoke(formattingOptions)), /* continuationIndent = */ Objects.requireNonNullElse(options.continuationIndent, (Integer) formattingOptionsClass.getMethod("getContinuationIndent").invoke(formattingOptions)), + /* trailingCommaManagementStrategy = */ trailingCommaManagementStrategy, /* removeUnusedImports = */ Objects.requireNonNullElse(options.removeUnusedImports, (Boolean) formattingOptionsClass.getMethod("getRemoveUnusedImports").invoke(formattingOptions)), - /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions), - /* trailingCommaManagementStrategy */ Objects.requireNonNullElse(options.trailingCommaManagementStrategy, (TrailingCommaManagementStrategy) formattingOptionsClass.getMethod("getTrailingCommaManagementStrategy").invoke(formattingOptions))); + /* debuggingPrintOpsAfterFormatting = */ (Boolean) formattingOptionsClass.getMethod("getDebuggingPrintOpsAfterFormatting").invoke(formattingOptions)); } } @@ -476,6 +490,10 @@ private Class getFormattingOptionsClazz() throws Exception { return formattingOptionsClazz; } + private Class getTrailingCommaManagementStrategyClazz() throws Exception { + return classLoader.loadClass(PACKAGE + ".format.TrailingCommaManagementStrategy"); + } + private @Nullable Boolean getManageTrailingCommasFrom( @Nullable TrailingCommaManagementStrategy trailingCommaManagementStrategy ) { 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 12d66728f4..783b5d50fe 100644 --- a/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/kotlin/KtfmtStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2025 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. @@ -40,6 +40,44 @@ void behaviorWithOptions() { StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); } + @Test + void behaviorWithOptions_0_61() { + KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions(); + options.setMaxWidth(100); + FormatterStep step = KtfmtStep.create("0.61", TestProvisioner.mavenCentral(), KtfmtStep.Style.GOOGLE, options); + StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); + } + + @Test + void behavior_0_62() throws Exception { + FormatterStep step = KtfmtStep.create("0.62", TestProvisioner.mavenCentral()); + StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); + } + + @Test + void behaviorWithOptions_0_62() { + KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions(); + options.setMaxWidth(100); + FormatterStep step = KtfmtStep.create("0.62", 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(); + options.setMaxWidth(100); + FormatterStep step = KtfmtStep.create("0.53", TestProvisioner.mavenCentral(), KtfmtStep.Style.GOOGLE, options); + StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); + } + + @Test + void behaviorWithOptions_0_56() { + KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions(); + options.setMaxWidth(100); + FormatterStep step = KtfmtStep.create("0.56", TestProvisioner.mavenCentral(), KtfmtStep.Style.GOOGLE, options); + StepHarness.forStep(step).testResource("kotlin/ktfmt/basic.dirty", "kotlin/ktfmt/basic.clean"); + } + @Test void dropboxStyle_0_16() throws Exception { KtfmtStep.KtfmtFormattingOptions options = new KtfmtStep.KtfmtFormattingOptions();