/* * Copyright 2016-2024 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.kotlin; import static com.diffplug.spotless.kotlin.KtfmtStep.Style.DEFAULT; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; import javax.annotation.Nullable; import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.JarState; import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.ThrowingEx; /** * Wraps up ktfmt as a FormatterStep. */ public class KtfmtStep implements java.io.Serializable { private static final long serialVersionUID = 1L; private static final String DEFAULT_VERSION = "0.47"; private static final String NAME = "ktfmt"; private static final String MAVEN_COORDINATE = "com.facebook:ktfmt:"; private final String version; /** * Option that allows to apply formatting options to perform a 4-space block and continuation indent. */ @Nullable private final Style style; @Nullable private final KtfmtFormattingOptions options; /** The jar that contains the formatter. */ private final JarState.Promised jarState; private KtfmtStep(String version, JarState.Promised jarState, @Nullable Style style, @Nullable KtfmtFormattingOptions options) { this.version = Objects.requireNonNull(version, "version"); this.style = style; this.options = options; this.jarState = Objects.requireNonNull(jarState, "jarState"); } /** * Used to allow multiple style option through formatting options and since when is each of them available. * * @see ktfmt source */ public enum Style { DEFAULT("DEFAULT_FORMAT", "0.0"), DROPBOX("DROPBOX_FORMAT", "0.11"), GOOGLE("GOOGLE_FORMAT", "0.21"), KOTLINLANG("KOTLINLANG_FORMAT", "0.21"); final private String format; final private String since; Style(String format, String since) { this.format = format; this.since = since; } String getFormat() { return format; } String getSince() { return since; } } public static class KtfmtFormattingOptions implements Serializable { private static final long serialVersionUID = 1L; @Nullable private Integer maxWidth = null; @Nullable private Integer blockIndent = null; @Nullable private Integer continuationIndent = null; @Nullable private Boolean removeUnusedImport = null; public KtfmtFormattingOptions() {} public KtfmtFormattingOptions( @Nullable Integer maxWidth, @Nullable Integer blockIndent, @Nullable Integer continuationIndent, @Nullable Boolean removeUnusedImport) { this.maxWidth = maxWidth; this.blockIndent = blockIndent; this.continuationIndent = continuationIndent; this.removeUnusedImport = removeUnusedImport; } public void setMaxWidth(int maxWidth) { this.maxWidth = maxWidth; } public void setBlockIndent(int blockIndent) { this.blockIndent = blockIndent; } public void setContinuationIndent(int continuationIndent) { this.continuationIndent = continuationIndent; } public void setRemoveUnusedImport(boolean removeUnusedImport) { this.removeUnusedImport = removeUnusedImport; } } /** * The format method is available in the link below. * * @see ktfmt source */ static final String FORMATTER_METHOD = "format"; /** 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, provisioner, null, null); } /** Creates a step which formats everything - code, import order, and unused imports. */ public static FormatterStep create(String version, Provisioner provisioner, @Nullable Style style, @Nullable KtfmtFormattingOptions options) { 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), KtfmtStep::equalityState, State::createFormat); } public static String defaultVersion() { return DEFAULT_VERSION; } private State equalityState() { return new State(version, jarState.get(), style, options); } private static final class State implements Serializable { private static final long serialVersionUID = 1L; private static final String PACKAGE = "com.facebook.ktfmt"; private final String version; @Nullable private final Style style; @Nullable private final KtfmtFormattingOptions options; private final JarState jarState; State(String version, JarState jarState, @Nullable Style style, @Nullable KtfmtFormattingOptions options) { this.version = version; this.options = options; this.style = style; this.jarState = jarState; } FormatterFunc createFormat() throws Exception { final ClassLoader classLoader = jarState.getClassLoader(); if (BadSemver.version(version) < BadSemver.version(0, 32)) { if (options != null) { throw new IllegalStateException("Ktfmt formatting options supported for version 0.32 and later"); } return getFormatterFuncFallback(style != null ? style : DEFAULT, classLoader); } 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"); if (style == null && options == null) { final Constructor constructor = formatterFuncClass.getConstructor(); return (FormatterFunc) constructor.newInstance(); } final Object ktfmtStyle = style == null ? null : Enum.valueOf((Class) ktfmtStyleClass, getKtfmtStyleOption(style)); if (options == null) { final Constructor constructor = formatterFuncClass.getConstructor(ktfmtStyleClass); return (FormatterFunc) constructor.newInstance(ktfmtStyle); } final Constructor optionsConstructor = ktfmtFormattingOptionsClass.getConstructor( Integer.class, Integer.class, Integer.class, Boolean.class); final Object ktfmtFormattingOptions = optionsConstructor.newInstance( options.maxWidth, options.blockIndent, options.continuationIndent, options.removeUnusedImport); if (style == null) { final Constructor constructor = formatterFuncClass.getConstructor(ktfmtFormattingOptionsClass); return (FormatterFunc) constructor.newInstance(ktfmtFormattingOptions); } final Constructor constructor = formatterFuncClass.getConstructor(ktfmtStyleClass, ktfmtFormattingOptionsClass); return (FormatterFunc) constructor.newInstance(ktfmtStyle, ktfmtFormattingOptions); } /** * @param style * @return com.diffplug.spotless.glue.ktfmt.KtfmtStyle enum value name */ private String getKtfmtStyleOption(Style style) { switch (style) { case DEFAULT: return "DEFAULT"; case DROPBOX: return "DROPBOX"; case GOOGLE: return "GOOGLE"; case KOTLINLANG: return "KOTLIN_LANG"; default: throw new IllegalStateException("Unsupported style: " + style); } } private FormatterFunc getFormatterFuncFallback(Style style, ClassLoader classLoader) { return input -> { try { if (style == DEFAULT) { Method formatterMethod = getFormatterClazz(classLoader).getMethod(FORMATTER_METHOD, String.class); return (String) formatterMethod.invoke(getFormatterClazz(classLoader), input); } else { Method formatterMethod = getFormatterClazz(classLoader).getMethod(FORMATTER_METHOD, getFormattingOptionsClazz(classLoader), String.class); Object formattingOptions = getCustomFormattingOptions(classLoader, style); return (String) formatterMethod.invoke(getFormatterClazz(classLoader), formattingOptions, input); } } catch (InvocationTargetException e) { throw ThrowingEx.unwrapCause(e); } }; } private Object getCustomFormattingOptions(ClassLoader classLoader, Style style) throws Exception { if (BadSemver.version(version) < BadSemver.version(style.since)) { throw new IllegalStateException(String.format("The style %s is available from version %s (current version: %s)", style.name(), style.since, version)); } try { // ktfmt v0.19 and later return getFormatterClazz(classLoader).getField(style.getFormat()).get(null); } catch (NoSuchFieldException ignored) {} // fallback to old, pre-0.19 ktfmt interface. if (style == Style.DEFAULT || style == Style.DROPBOX) { Class formattingOptionsCompanionClazz = classLoader.loadClass(PACKAGE + ".FormattingOptions$Companion"); Object companion = formattingOptionsCompanionClazz.getConstructors()[0].newInstance((Object) null); Method formattingOptionsMethod = formattingOptionsCompanionClazz.getDeclaredMethod("dropboxStyle"); return formattingOptionsMethod.invoke(companion); } else { throw new IllegalStateException("Versions pre-0.19 can only use Default and Dropbox styles"); } } private Class getFormatterClazz(ClassLoader classLoader) throws Exception { Class formatterClazz; if (BadSemver.version(version) >= BadSemver.version(0, 31)) { formatterClazz = classLoader.loadClass(PACKAGE + ".format.Formatter"); } else { formatterClazz = classLoader.loadClass(PACKAGE + ".FormatterKt"); } return formatterClazz; } private Class getFormattingOptionsClazz(ClassLoader classLoader) throws Exception { Class formattingOptionsClazz; if (BadSemver.version(version) >= BadSemver.version(0, 31)) { formattingOptionsClazz = classLoader.loadClass(PACKAGE + ".format.FormattingOptions"); } else { formattingOptionsClazz = classLoader.loadClass(PACKAGE + ".FormattingOptions"); } return formattingOptionsClazz; } } }