Skip to content

feat(ktfmt): add ktfmt a new Kotlin formatter from Facebook Incubator #569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
### Added
* Support for google-java-format 1.8 (including test infrastructure for Java 11). ([#562](https://github.com/diffplug/spotless/issues/562))
* Improved PaddedCell such that it is as performant as non-padded cell - no reason not to have it always enabled. Deprecated all of `PaddedCellBulk`. ([#561](https://github.com/diffplug/spotless/pull/561))
* Support for ktfmt 0.13 ([#569](https://github.com/diffplug/spotless/pull/569))
### Changed
* Updated a bunch of dependencies, most notably: ([#564](https://github.com/diffplug/spotless/pull/564))
* jgit `5.5.0.201909110433-r` -> `5.7.0.202003110725-r`
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ lib('java.ImportOrderStep') +'{{yes}} | {{yes}}
lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} | {{no}} |',
extra('java.EclipseFormatterStep') +'{{yes}} | {{yes}} | {{no}} |',
lib('kotlin.KtLintStep') +'{{yes}} | {{yes}} | {{no}} |',
lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}} | {{no}} |',
lib('markdown.FreshMarkStep') +'{{yes}} | {{no}} | {{no}} |',
lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} |',
lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} |',
Expand Down Expand Up @@ -73,6 +74,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}}
| [`java.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :white_large_square: |
| [`java.EclipseFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseFormatterStep.java) | :+1: | :+1: | :white_large_square: |
| [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :white_large_square: |
| [`kotlin.KtfmtStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java) | :+1: | :+1: | :white_large_square: |
| [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: |
| [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: |
| [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: |
Expand Down
88 changes: 88 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2016 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 java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;

import com.diffplug.spotless.*;

/** Wraps up [ktfmt](https://github.com/facebookincubator/ktfmt) as a FormatterStep. */
public class KtfmtStep {
// prevent direct instantiation
private KtfmtStep() {}

private static final String DEFAULT_VERSION = "0.13";
static final String NAME = "ktfmt";
static final String PACKAGE = "com.facebook";
static final String MAVEN_COORDINATE = PACKAGE + ":ktfmt:";

/**
* The <code>format</code> method is available in the link below.
*
* @see:
* https://github.com/facebookincubator/ktfmt/blob/master/core/src/main/java/com/facebook/ktfmt/Formatter.kt#L61-L65
*/
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) {
Objects.requireNonNull(version, "version");
Objects.requireNonNull(provisioner, "provisioner");
return FormatterStep.createLazy(
NAME, () -> new State(version, provisioner), State::createFormat);
}

public static String defaultVersion() {
return DEFAULT_VERSION;
}

static final class State implements Serializable {
private static final long serialVersionUID = 1L;

private final String pkg;
/** The jar that contains the eclipse formatter. */
final JarState jarState;

State(String version, Provisioner provisioner) throws IOException {
this.pkg = PACKAGE;
this.jarState = JarState.from(MAVEN_COORDINATE + version, provisioner);
}

FormatterFunc createFormat() throws Exception {
ClassLoader classLoader = jarState.getClassLoader();

Class<?> formatterClazz = classLoader.loadClass(pkg + ".ktfmt.FormatterKt");
Method formatterMethod = formatterClazz.getMethod(FORMATTER_METHOD, String.class);

return input -> {
try {
return (String) formatterMethod.invoke(formatterClazz, input);
} catch (InvocationTargetException e) {
throw ThrowingEx.unwrapCause(e);
}
};
}
}
}
3 changes: 2 additions & 1 deletion plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* [**VS Code Extension**](https://marketplace.visualstudio.com/items?itemName=richardwillis.vscode-spotless-gradle) thanks to [@badsyntax](https://github.com/badsyntax)
* Support for google-java-format 1.8 (requires you to run build on Java 11) ([#562](https://github.com/diffplug/spotless/issues/562))
* Support for google-java-format 1.8 (requires build to run on Java 11+) ([#562](https://github.com/diffplug/spotless/issues/562))
* Support for ktfmt 0.13 (requires build to run on Java 11+) ([#569](https://github.com/diffplug/spotless/pull/569))
### Changed
* PaddedCell is now always enabled. It is strictly better than non-padded cell, and there is no performance penalty. [See here](https://github.com/diffplug/spotless/pull/560#issuecomment-621752798) for detailed explanation. ([#561](https://github.com/diffplug/spotless/pull/561))
* Updated a bunch of dependencies, most notably jgit `5.5.0.201909110433-r` -> `5.7.0.202003110725-r`. ([#564](https://github.com/diffplug/spotless/pull/564))
Expand Down
18 changes: 18 additions & 0 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Spotless can check and apply formatting to any plain-text file, using simple rul
* [Groovy Eclipse](#groovy-eclipse)'s groovy code formatter
* [FreshMark](https://github.com/diffplug/freshmark) (markdown with variables)
* [ktlint](https://github.com/pinterest/ktlint)
* [ktfmt](https://github.com/facebookincubator/ktfmt)
* [scalafmt](https://github.com/olafurpg/scalafmt)
* [DBeaver sql format](https://dbeaver.jkiss.org/)
* [Prettier: An opinionated code formatter](https://prettier.io)
Expand Down Expand Up @@ -278,6 +279,23 @@ spotless {
}
```

<a name="ktfmt"></a>

## Applying [ktfmt](https://github.com/facebookincubator/ktfmt) to Kotlin files

```gradle
spotless {
kotlin {
// optionally takes a version
ktfmt()

// also supports license headers
licenseHeader '/* Licensed under Apache-2.0 */' // License header
licenseHeaderFile 'path-to-license-file' // License header file
}
}
```

<a name="sql-dbeaver"></a>

## Applying [DBeaver](https://dbeaver.jkiss.org/) to SQL scripts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.kotlin.KtLintStep;
import com.diffplug.spotless.kotlin.KtfmtStep;

public class KotlinExtension extends FormatExtension implements HasBuiltinDelimiterForLicense {
static final String NAME = "kotlin";
Expand Down Expand Up @@ -79,6 +80,33 @@ private FormatterStep createStep() {
}
}

/** Uses the [ktfmt](https://github.com/facebookincubator/ktfmt) jar to format source code. */
public KtfmtConfig ktfmt() {
return ktfmt(KtfmtStep.defaultVersion());
}

/**
* Uses the given version of [ktfmt](https://github.com/facebookincubator/ktfmt) to format source
* code.
*/
public KtfmtConfig ktfmt(String version) {
Objects.requireNonNull(version);
return new KtfmtConfig(version);
}

public class KtfmtConfig {
final String version;

KtfmtConfig(String version) {
this.version = Objects.requireNonNull(version);
addStep(createStep());
}

private FormatterStep createStep() {
return KtfmtStep.create(version, GradleProvisioner.fromProject(getProject()));
}
}

/** If the user hasn't specified the files yet, we'll assume he/she means all of the kotlin files. */
@Override
protected void setupTask(SpotlessTask task) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.gradle.testkit.runner.BuildResult;
import org.junit.Test;

import com.diffplug.spotless.JreVersion;

public class KotlinExtensionTest extends GradleIntegrationTest {
private static final String HEADER = "// License Header";
private static final String HEADER_WITH_YEAR = "// License Header $YEAR";
Expand All @@ -45,6 +47,28 @@ public void integration() throws IOException {
assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktlint/basic.clean");
}

@Test
public void integrationKtfmt() throws IOException {
if (JreVersion.thisVm() == JreVersion._8) {
// ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+.
return;
}
setFile("build.gradle").toLines(
"plugins {",
" id 'nebula.kotlin' version '1.0.6'",
" id 'com.diffplug.gradle.spotless'",
"}",
"repositories { mavenCentral() }",
"spotless {",
" kotlin {",
" ktfmt()",
" }",
"}");
setFile("src/main/kotlin/basic.kt").toResource("kotlin/ktfmt/basic.dirty");
gradleRunner().withArguments("spotlessApply").build();
assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktfmt/basic.clean");
}

@Test
public void testWithIndentation() throws IOException {
setFile("build.gradle").toLines(
Expand Down Expand Up @@ -82,6 +106,29 @@ public void testWithHeader() throws IOException {
assertFile("src/main/kotlin/test.kt").hasContent(HEADER + "\n" + getTestResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test"));
}

@Test
public void testWithHeaderKtfmt() throws IOException {
if (JreVersion.thisVm() == JreVersion._8) {
// ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+.
return;
}
setFile("build.gradle").toLines(
"plugins {",
" id 'nebula.kotlin' version '1.0.6'",
" id 'com.diffplug.gradle.spotless'",
"}",
"repositories { mavenCentral() }",
"spotless {",
" kotlin {",
" licenseHeader('" + HEADER + "')",
" ktfmt()",
" }",
"}");
setFile("src/main/kotlin/test.kt").toResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test");
gradleRunner().withArguments("spotlessApply").build();
assertFile("src/main/kotlin/test.kt").hasContent(HEADER + "\n" + getTestResource("kotlin/licenseheader/KotlinCodeWithoutHeaderKtfmt.test"));
}

@Test
public void testWithCustomHeaderSeparator() throws IOException {
setFile("build.gradle").toLines(
Expand All @@ -101,6 +148,29 @@ public void testWithCustomHeaderSeparator() throws IOException {
assertFile("src/main/kotlin/test.kt").hasContent(HEADER + "\n" + getTestResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test"));
}

@Test
public void testWithCustomHeaderSeparatorKtfmt() throws IOException {
if (JreVersion.thisVm() == JreVersion._8) {
// ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+.
return;
}
setFile("build.gradle").toLines(
"plugins {",
" id 'nebula.kotlin' version '1.0.6'",
" id 'com.diffplug.gradle.spotless'",
"}",
"repositories { mavenCentral() }",
"spotless {",
" kotlin {",
" licenseHeader ('" + HEADER + "', '@file')",
" ktfmt()",
" }",
"}");
setFile("src/main/kotlin/test.kt").toResource("kotlin/licenseheader/KotlinCodeWithoutHeader.test");
gradleRunner().withArguments("spotlessApply").build();
assertFile("src/main/kotlin/test.kt").hasContent(HEADER + "\n" + getTestResource("kotlin/licenseheader/KotlinCodeWithoutHeaderKtfmt.test"));
}

@Test
public void testWithNonStandardYearSeparator() throws IOException {
setFile("build.gradle").toLines(
Expand All @@ -126,4 +196,34 @@ public void testWithNonStandardYearSeparator() throws IOException {
matcher.startsWith(HEADER_WITH_YEAR.replace("$YEAR", String.valueOf(YearMonth.now().getYear())));
});
}

@Test
public void testWithNonStandardYearSeparatorKtfmt() throws IOException {
if (JreVersion.thisVm() == JreVersion._8) {
// ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+.
return;
}
setFile("build.gradle").toLines(
"plugins {",
" id 'nebula.kotlin' version '1.0.6'",
" id 'com.diffplug.gradle.spotless'",
"}",
"repositories { mavenCentral() }",
"spotless {",
" kotlin {",
" licenseHeader('" + HEADER_WITH_YEAR + "').yearSeparator(', ')",
" ktfmt()",
" }",
"}");

setFile("src/main/kotlin/test.kt").toResource("kotlin/licenseheader/KotlinCodeWithMultiYearHeader.test");
setFile("src/main/kotlin/test2.kt").toResource("kotlin/licenseheader/KotlinCodeWithMultiYearHeader2.test");
gradleRunner().withArguments("spotlessApply").build();
assertFile("src/main/kotlin/test.kt").matches(matcher -> {
matcher.startsWith("// License Header 2012, 2014");
});
assertFile("src/main/kotlin/test2.kt").matches(matcher -> {
matcher.startsWith(HEADER_WITH_YEAR.replace("$YEAR", String.valueOf(YearMonth.now().getYear())));
});
}
}
3 changes: 2 additions & 1 deletion plugin-maven/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (

## [Unreleased]
### Added
* Support for google-java-format 1.8 (requires you to run build on Java 11) ([#562](https://github.com/diffplug/spotless/issues/562))
* Support for google-java-format 1.8 (requires build to run on Java 11+) ([#562](https://github.com/diffplug/spotless/issues/562))
* Support for ktfmt 0.13 (requires build to run on Java 11+) ([#569](https://github.com/diffplug/spotless/pull/569))
* `mvn spotless:apply` is now guaranteed to be idempotent, even if some of the formatters are not. See [`PADDEDCELL.md` for details](https://github.com/diffplug/spotless/blob/master/PADDEDCELL.md) if you're curious. ([#565](https://github.com/diffplug/spotless/pull/565))
* Updated a bunch of dependencies, most notably jgit `5.5.0.201909110433-r` -> `5.7.0.202003110725-r`. ([#564](https://github.com/diffplug/spotless/pull/564))

Expand Down
22 changes: 22 additions & 0 deletions plugin-maven/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ By default, all files matching `src/main/scala/**/*.scala`, `src/test/scala/**/*

By default, all files matching `src/main/kotlin/**/*.kt` and `src/test/kotlin/**/*.kt` Ant style pattern will be formatted. Each element under `<kotlin>` is a step, and they will be applied in the order specified. Every step is optional.

### Applying [ktlint](https://github.com/pinterest/ktlint) to Kotlin files

```xml
<configuration>
<kotlin>
Expand All @@ -171,6 +173,26 @@ By default, all files matching `src/main/kotlin/**/*.kt` and `src/test/kotlin/**
</configuration>
```

### Applying [ktfmt](https://github.com/facebookincubator/ktfmt) to Kotlin files

```xml
<configuration>
<kotlin>
<licenseHeader>
<!-- Specify either content or file, but not both -->
<content>/* Licensed under Apache-2.0 */</content>
<file>${basedir}/license-header</file>
</licenseHeader>
<endWithNewline/>
<trimTrailingWhitespace/>
<ktfmt>
<!-- Optional, available versions: https://github.com/facebookincubator/ktfmt/releases -->
<version>0.11</version>
</ktfmt>
</kotlin>
</configuration>
```

<a name="cpp"></a>

## Applying to C/C++ source
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,8 @@ public String licenseHeaderDelimiter() {
public void addKtlint(Ktlint ktlint) {
addStepFactory(ktlint);
}

public void addKtfmt(Ktfmt ktfmt) {
addStepFactory(ktfmt);
}
}
Loading