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 9f3b62dadb..2fcdc566cb 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java @@ -21,9 +21,11 @@ import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.Objects; import com.diffplug.spotless.*; +import javax.annotation.Nullable; /** * Wraps up ktfmt as a FormatterStep. @@ -64,6 +66,14 @@ String getSince() { private static final String DROPBOX_STYLE_METHOD = "dropboxStyle"; + private static final String FORMATTING_OPTIONS_METHOD_COPY = "copy"; + private static final String FORMATTING_OPTIONS_METHOD_GET_STYLE = "getStyle"; + private static final String FORMATTING_OPTIONS_METHOD_GET_MAX_WIDTH = "getMaxWidth"; + private static final String FORMATTING_OPTIONS_METHOD_GET_BLOCK_INDENT = "getBlockIndent"; + private static final String FORMATTING_OPTIONS_METHOD_GET_CONTINUATION_INDENT = "getContinuationIndent"; + private static final String FORMATTING_OPTIONS_METHOD_GET_REMOVE_UNUSED_IMPORTS = "getRemoveUnusedImports"; + private static final String FORMATTING_OPTIONS_METHOD_GET_DEBUGGING_PRINT_OPS_AFTER_FORMATTING = "getDebuggingPrintOpsAfterFormatting"; + /** * The format method is available in the link below. * @@ -78,16 +88,16 @@ public static FormatterStep create(Provisioner 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, DEFAULT); + return create(version, provisioner, null, DEFAULT); } /** Creates a step which formats everything - code, import order, and unused imports. */ - public static FormatterStep create(String version, Provisioner provisioner, Style style) { + public static FormatterStep create(String version, Provisioner provisioner, @Nullable Integer maxWidth, Style style) { Objects.requireNonNull(version, "version"); Objects.requireNonNull(provisioner, "provisioner"); Objects.requireNonNull(style, "style"); return FormatterStep.createLazy( - NAME, () -> new State(version, provisioner, style), State::createFormat); + NAME, () -> new State(version, provisioner, maxWidth, style), State::createFormat); } public static String defaultVersion() { @@ -104,6 +114,11 @@ static final class State implements Serializable { private final String version; private final String pkg; + /** + * Option that allows change line width before breaking + */ + @Nullable + private final Integer maxWidth; /** * Option that allows to apply formatting options to perform a 4 spaces block and continuation indent. */ @@ -111,9 +126,10 @@ static final class State implements Serializable { /** The jar that contains the formatter. */ final JarState jarState; - State(String version, Provisioner provisioner, Style style) throws IOException { + State(String version, Provisioner provisioner, @Nullable Integer maxWidth, Style style) throws IOException { this.version = version; this.pkg = PACKAGE; + this.maxWidth = maxWidth; this.style = style; this.jarState = JarState.from(MAVEN_COORDINATE + version, provisioner); } @@ -122,29 +138,55 @@ FormatterFunc createFormat() throws Exception { ClassLoader classLoader = jarState.getClassLoader(); 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); - } + Method formatterMethod = getFormatterClazz(classLoader).getMethod(FORMATTER_METHOD, getFormattingOptionsClazz(classLoader), + String.class); + Object formattingOptions = getCustomFormattingOptions(classLoader, maxWidth, 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 { + private Object getCustomFormattingOptions(ClassLoader classLoader, @Nullable Integer maxWidth, 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); + Object formattingOptionVariable; + if (style == DEFAULT) { + formattingOptionVariable = getFormattingOptionsClazz(classLoader).getDeclaredConstructor().newInstance(); + } else { + formattingOptionVariable = getFormatterClazz(classLoader).getField(style.getFormat()).get(null); + } + + if (maxWidth != null) { + if (BadSemver.version(version) < BadSemver.version(0, 31)) { + throw new IllegalStateException("Max width configuration supported only for ktfmt 0.31 and later"); + } + + Class formattingOptionsClass = getFormattingOptionsClazz(classLoader); + Object styleValue = formattingOptionsClass.getMethod(FORMATTING_OPTIONS_METHOD_GET_STYLE).invoke(formattingOptionVariable); + Object maxWidthValue = formattingOptionsClass.getMethod(FORMATTING_OPTIONS_METHOD_GET_MAX_WIDTH).invoke(formattingOptionVariable); + Object blockIndentValue = formattingOptionsClass.getMethod(FORMATTING_OPTIONS_METHOD_GET_BLOCK_INDENT).invoke(formattingOptionVariable); + Object continuationIndentValue = formattingOptionsClass.getMethod(FORMATTING_OPTIONS_METHOD_GET_CONTINUATION_INDENT).invoke(formattingOptionVariable); + Object removeUnusedImportsValue = formattingOptionsClass.getMethod(FORMATTING_OPTIONS_METHOD_GET_REMOVE_UNUSED_IMPORTS).invoke(formattingOptionVariable); + Object debuggingPrintOpsAfterFormattingValue = formattingOptionsClass.getMethod(FORMATTING_OPTIONS_METHOD_GET_DEBUGGING_PRINT_OPS_AFTER_FORMATTING).invoke(formattingOptionVariable); + + Method copyFormattingOption = Arrays.stream(formattingOptionsClass.getDeclaredMethods()) + .filter(method -> FORMATTING_OPTIONS_METHOD_COPY.equals(method.getName())).findFirst().get(); + formattingOptionVariable = copyFormattingOption.invoke(formattingOptionVariable, + styleValue, + maxWidth > 0 ? maxWidth : maxWidthValue, + blockIndentValue, + continuationIndentValue, + removeUnusedImportsValue, + debuggingPrintOpsAfterFormattingValue); + } + + return formattingOptionVariable; } catch (NoSuchFieldException ignored) {} // fallback to old, pre-0.19 ktfmt interface. diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java index d88b4aac7f..d6ba28a3c4 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java @@ -103,6 +103,7 @@ public KtfmtConfig ktfmt(String version) { public class KtfmtConfig { final String version; + Integer maxWidth; Style style; KtfmtConfig(String version) { @@ -111,25 +112,35 @@ public class KtfmtConfig { addStep(createStep()); } - public void dropboxStyle() { - style(Style.DROPBOX); + public KtfmtConfig maxWidth(int maxWidth) { + if (maxWidth <= 0) { + throw new IllegalArgumentException("Passed maxWidth parameter must be positive value"); + } + this.maxWidth = maxWidth; + replaceStep(createStep()); + return this; } - public void googleStyle() { - style(Style.GOOGLE); + public KtfmtConfig dropboxStyle() { + return style(Style.DROPBOX); } - public void kotlinlangStyle() { - style(Style.KOTLINLANG); + public KtfmtConfig googleStyle() { + return style(Style.GOOGLE); } - public void style(Style style) { + public KtfmtConfig kotlinlangStyle() { + return style(Style.KOTLINLANG); + } + + public KtfmtConfig style(Style style) { this.style = style; replaceStep(createStep()); + return this; } private FormatterStep createStep() { - return KtfmtStep.create(version, provisioner(), style); + return KtfmtStep.create(version, provisioner(), maxWidth, style); } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java index 5ea3c29594..4429f541c0 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java @@ -89,33 +89,45 @@ public KtfmtConfig ktfmt(String version) { public class KtfmtConfig { final String version; + Integer maxWidth; Style style; KtfmtConfig(String version) { this.version = Objects.requireNonNull(version); + this.maxWidth = null; this.style = Style.DEFAULT; addStep(createStep()); } - public void style(Style style) { + public KtfmtConfig maxWidth(int maxWidth) { + if (maxWidth <= 0) { + throw new IllegalArgumentException("Passed maxWidth parameter must be positive value"); + } + this.maxWidth = maxWidth; + replaceStep(createStep()); + return this; + } + + public KtfmtConfig style(Style style) { this.style = style; replaceStep(createStep()); + return this; } - public void dropboxStyle() { - style(Style.DROPBOX); + public KtfmtConfig dropboxStyle() { + return style(Style.DROPBOX); } - public void googleStyle() { - style(Style.GOOGLE); + public KtfmtConfig googleStyle() { + return style(Style.GOOGLE); } - public void kotlinlangStyle() { - style(Style.KOTLINLANG); + public KtfmtConfig kotlinlangStyle() { + return style(Style.KOTLINLANG); } private FormatterStep createStep() { - return KtfmtStep.create(version, provisioner(), style); + return KtfmtStep.create(version, provisioner(), maxWidth, style); } } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java index f3d8ad442d..0feb110666 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java @@ -269,4 +269,44 @@ void testWithNonStandardYearSeparatorKtfmt() throws IOException { matcher.startsWith("// License Header 2012, 2014"); }); } + + @Test + @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. + void testWithCustomMaxWidthDefaultStyleKtfmt() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " kotlin {", + " ktfmt().maxWidth(120)", + " }", + "}"); + + setFile("src/main/kotlin/max-width.kt").toResource("kotlin/ktfmt/max-width.dirty"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/kotlin/max-width.kt").sameAsResource("kotlin/ktfmt/max-width.clean"); + } + + @Test + @EnabledForJreRange(min = JAVA_11) // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. + void testWithCustomMaxWidthDropboxStyleKtfmt() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'org.jetbrains.kotlin.jvm' version '1.5.31'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " kotlin {", + " ktfmt().dropboxStyle().maxWidth(120)", + " }", + "}"); + + setFile("src/main/kotlin/max-width.kt").toResource("kotlin/ktfmt/max-width.dirty"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/kotlin/max-width.kt").sameAsResource("kotlin/ktfmt/max-width-dropbox.clean"); + } } diff --git a/testlib/src/main/resources/kotlin/ktfmt/max-width-dropbox.clean b/testlib/src/main/resources/kotlin/ktfmt/max-width-dropbox.clean new file mode 100644 index 0000000000..eb0715b1ec --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/max-width-dropbox.clean @@ -0,0 +1,12 @@ +import a.* +import a.b +import a.b.c.* +import kotlinx.android.synthetic.main.layout_name.* + +fun main() {} + +fun foo(param1: Any, param2: Any, param3: Any, param4: Any, param5: Any, param6: Any, param7: Any, param8: Any) { + a() + functionNameWithAlmost120SymbolsToValidateThatKtfmtNotBreakLineAtDefault100(param1, param2, param3, param4, param5) + return b +} \ No newline at end of file diff --git a/testlib/src/main/resources/kotlin/ktfmt/max-width.clean b/testlib/src/main/resources/kotlin/ktfmt/max-width.clean new file mode 100644 index 0000000000..4592181044 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/max-width.clean @@ -0,0 +1,12 @@ +import a.* +import a.b +import a.b.c.* +import kotlinx.android.synthetic.main.layout_name.* + +fun main() {} + +fun foo(param1: Any, param2: Any, param3: Any, param4: Any, param5: Any, param6: Any, param7: Any, param8: Any) { + a() + functionNameWithAlmost120SymbolsToValidateThatKtfmtNotBreakLineAtDefault100(param1, param2, param3, param4, param5) + return b +} diff --git a/testlib/src/main/resources/kotlin/ktfmt/max-width.dirty b/testlib/src/main/resources/kotlin/ktfmt/max-width.dirty new file mode 100644 index 0000000000..ef9504e1c3 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktfmt/max-width.dirty @@ -0,0 +1,12 @@ +import a.* + +import kotlinx.android.synthetic.main.layout_name.* + +import a.b.c.* +import a.b + +fun main() {} +fun foo(param1: Any, param2: Any, param3: Any, param4: Any, param5: Any, param6: Any, param7: Any, param8: Any) { + a(); functionNameWithAlmost120SymbolsToValidateThatKtfmtNotBreakLineAtDefault100(param1, param2, param3, param4, param5) + return b +}