Skip to content
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

Add option to use ktlint experimental ruleset #1168

Merged
merged 5 commits into from Apr 22, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Expand Up @@ -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]
### Added
* Added support for enabling ktlint experimental ruleset. ([#1145](https://github.com/diffplug/spotless/pull/1168))

## [2.24.2] - 2022-04-06

Expand Down
4 changes: 0 additions & 4 deletions build.gradle
Expand Up @@ -19,7 +19,3 @@ spotless {
endWithNewline()
}
}

static Class<?> spotBugsTaskType() {
return com.github.spotbugs.snom.SpotBugsTask
}
1 change: 1 addition & 0 deletions lib/build.gradle
Expand Up @@ -47,6 +47,7 @@ dependencies {
String VER_KTLINT='0.43.2'
ktlintCompileOnly "com.pinterest:ktlint:$VER_KTLINT"
ktlintCompileOnly "com.pinterest.ktlint:ktlint-core:$VER_KTLINT"
ktlintCompileOnly "com.pinterest.ktlint:ktlint-ruleset-experimental:$VER_KTLINT"
ktlintCompileOnly "com.pinterest.ktlint:ktlint-ruleset-standard:$VER_KTLINT"

// used for markdown formatting
Expand Down
Expand Up @@ -16,14 +16,15 @@
package com.diffplug.spotless.glue.ktlint;

import java.io.File;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.pinterest.ktlint.core.KtLint;
import com.pinterest.ktlint.core.KtLint.Params;
import com.pinterest.ktlint.core.LintError;
import com.pinterest.ktlint.core.RuleSet;
import com.pinterest.ktlint.ruleset.experimental.ExperimentalRuleSetProvider;
import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider;

import com.diffplug.spotless.FormatterFunc;
Expand All @@ -38,8 +39,13 @@ public class KtlintFormatterFunc implements FormatterFunc.NeedsFile {
private final Function2<? super LintError, ? super Boolean, Unit> formatterCallback;
private final boolean isScript;

public KtlintFormatterFunc(boolean isScript, Map<String, String> userData) {
rulesets = Collections.singletonList(new StandardRuleSetProvider().get());
public KtlintFormatterFunc(boolean isScript, boolean useExperimental, Map<String, String> userData) {
rulesets = new ArrayList<>();
rulesets.add(new StandardRuleSetProvider().get());

if (useExperimental) {
rulesets.add(new ExperimentalRuleSetProvider().get());
}
this.userData = userData;
formatterCallback = new FormatterCallback();
this.isScript = isScript;
Expand Down
38 changes: 25 additions & 13 deletions lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java
@@ -1,5 +1,5 @@
/*
* Copyright 2016-2021 DiffPlug
* Copyright 2016-2022 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,6 +21,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -49,26 +50,26 @@ public static FormatterStep create(Provisioner provisioner) {
}

public static FormatterStep create(String version, Provisioner provisioner) {
return create(version, provisioner, Collections.emptyMap());
return create(version, provisioner, false, Collections.emptyMap());
}

public static FormatterStep create(String version, Provisioner provisioner, Map<String, String> userData) {
return create(version, provisioner, false, userData);
public static FormatterStep create(String version, Provisioner provisioner, boolean useExperimental, Map<String, String> userData) {
return create(version, provisioner, false, useExperimental, userData);
}

public static FormatterStep createForScript(String version, Provisioner provisioner) {
return create(version, provisioner, true, Collections.emptyMap());
return create(version, provisioner, true, false, Collections.emptyMap());
}

public static FormatterStep createForScript(String version, Provisioner provisioner, Map<String, String> userData) {
return create(version, provisioner, true, userData);
public static FormatterStep createForScript(String version, Provisioner provisioner, boolean useExperimental, Map<String, String> userData) {
return create(version, provisioner, true, useExperimental, userData);
}

private static FormatterStep create(String version, Provisioner provisioner, boolean isScript, Map<String, String> userData) {
private static FormatterStep create(String version, Provisioner provisioner, boolean isScript, boolean useExperimental, Map<String, String> userData) {
Objects.requireNonNull(version, "version");
Objects.requireNonNull(provisioner, "provisioner");
return FormatterStep.createLazy(NAME,
() -> new State(version, provisioner, isScript, userData),
() -> new State(version, provisioner, isScript, useExperimental, userData),
State::createFormat);
}

Expand All @@ -84,10 +85,12 @@ static final class State implements Serializable {
private final String pkg;
/** The jar that contains the formatter. */
final JarState jarState;
private final boolean useExperimental;
private final TreeMap<String, String> userData;
private final boolean useParams;

State(String version, Provisioner provisioner, boolean isScript, Map<String, String> userData) throws IOException {
State(String version, Provisioner provisioner, boolean isScript, boolean useExperimental, Map<String, String> userData) throws IOException {
this.useExperimental = useExperimental;
this.userData = new TreeMap<>(userData);
String coordinate;
if (BadSemver.version(version) < BadSemver.version(0, 32)) {
Expand All @@ -105,17 +108,26 @@ static final class State implements Serializable {
FormatterFunc createFormat() throws Exception {
if (useParams) {
Class<?> formatterFunc = jarState.getClassLoader().loadClass("com.diffplug.spotless.glue.ktlint.KtlintFormatterFunc");
Constructor<?> constructor = formatterFunc.getConstructor(boolean.class, Map.class);
return (FormatterFunc.NeedsFile) constructor.newInstance(isScript, userData);
Constructor<?> constructor = formatterFunc.getConstructor(boolean.class, boolean.class, Map.class);
return (FormatterFunc.NeedsFile) constructor.newInstance(isScript, useExperimental, userData);
}

ClassLoader classLoader = jarState.getClassLoader();
// String KtLint::format(String input, Iterable<RuleSet> rules, Function2 errorCallback)

ArrayList<Object> ruleSets = new ArrayList<>();

// first, we get the standard rules
Class<?> standardRuleSetProviderClass = classLoader.loadClass(pkg + ".ktlint.ruleset.standard.StandardRuleSetProvider");
Object standardRuleSet = standardRuleSetProviderClass.getMethod("get").invoke(standardRuleSetProviderClass.newInstance());
Iterable<?> ruleSets = Collections.singletonList(standardRuleSet);
ruleSets.add(standardRuleSet);

// second, we get the experimental rules if desired
if (useExperimental) {
Class<?> experimentalRuleSetProviderClass = classLoader.loadClass(pkg + ".ktlint.ruleset.experimental.ExperimentalRuleSetProvider");
Object experimentalRuleSet = experimentalRuleSetProviderClass.getMethod("get").invoke(experimentalRuleSetProviderClass.newInstance());
ruleSets.add(experimentalRuleSet);
}

// next, we create an error callback which throws an assertion error when the format is bad
Class<?> function2Interface = classLoader.loadClass("kotlin.jvm.functions.Function2");
Expand Down
2 changes: 1 addition & 1 deletion plugin-gradle/CHANGES.md
Expand Up @@ -5,9 +5,9 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (
## [Unreleased]
### Added
* Added a runToFixMessage property to customize the run-to-fix message in spotlessCheck task ([#1175](https://github.com/diffplug/spotless/issues/1175)).
* Added support for enabling ktlint experimental ruleset. ([#1145](https://github.com/diffplug/spotless/pull/1168))

## [6.4.2] - 2022-04-06

### Fixed
* Git user config and system config also included for defaultEndings configuration. ([#540](https://github.com/diffplug/spotless/issues/540))

Expand Down
6 changes: 4 additions & 2 deletions plugin-gradle/README.md
Expand Up @@ -340,8 +340,10 @@ spotless {
```kotlin
spotless {
kotlin {
// version and userData are both optional
ktlint('0.43.2').userData(mapOf('indent_size' to '2', 'continuation_indent_size' to '2'))
// version, setUseExperimental and userData are all optional
ktlint('0.43.2')
.setUseExperimental(true)
.userData(mapOf('indent_size' to '2', 'continuation_indent_size' to '2'))
```

### diktat
Expand Down
Expand Up @@ -59,7 +59,7 @@ public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile) {
/** Adds the specified version of <a href="https://github.com/pinterest/ktlint">ktlint</a>. */
public KotlinFormatExtension ktlint(String version) {
Objects.requireNonNull(version);
return new KotlinFormatExtension(version, Collections.emptyMap());
return new KotlinFormatExtension(version, false, Collections.emptyMap());
}

public KotlinFormatExtension ktlint() {
Expand All @@ -69,23 +69,32 @@ public KotlinFormatExtension ktlint() {
public class KotlinFormatExtension {

private final String version;
private boolean useExperimental;
private Map<String, String> userData;

KotlinFormatExtension(String version, Map<String, String> config) {
KotlinFormatExtension(String version, boolean useExperimental, Map<String, String> config) {
this.version = version;
this.useExperimental = useExperimental;
this.userData = config;
addStep(createStep());
}

public void userData(Map<String, String> userData) {
public KotlinFormatExtension setUseExperimental(boolean useExperimental) {
this.useExperimental = useExperimental;
replaceStep(createStep());
return this;
}

public KotlinFormatExtension userData(Map<String, String> userData) {
// Copy the map to a sorted map because up-to-date checking is based on binary-equals of the serialized
// representation.
this.userData = userData;
replaceStep(createStep());
return this;
}

private FormatterStep createStep() {
return KtLintStep.create(version, provisioner(), userData);
return KtLintStep.create(version, provisioner(), useExperimental, userData);
}
}

Expand Down
Expand Up @@ -45,7 +45,7 @@ public KotlinGradleExtension(SpotlessExtension spotless) {
/** Adds the specified version of <a href="https://github.com/pinterest/ktlint">ktlint</a>. */
public KotlinFormatExtension ktlint(String version) {
Objects.requireNonNull(version, "version");
return new KotlinFormatExtension(version, Collections.emptyMap());
return new KotlinFormatExtension(version, false, Collections.emptyMap());
}

public KotlinFormatExtension ktlint() {
Expand All @@ -55,23 +55,32 @@ public KotlinFormatExtension ktlint() {
public class KotlinFormatExtension {

private final String version;
private boolean useExperimental;
private Map<String, String> userData;

KotlinFormatExtension(String version, Map<String, String> config) {
KotlinFormatExtension(String version, boolean useExperimental, Map<String, String> config) {
this.version = version;
this.useExperimental = useExperimental;
this.userData = config;
addStep(createStep());
}

public void userData(Map<String, String> userData) {
public KotlinFormatExtension setUseExperimental(boolean useExperimental) {
this.useExperimental = useExperimental;
replaceStep(createStep());
return this;
}

public KotlinFormatExtension userData(Map<String, String> userData) {
// Copy the map to a sorted map because up-to-date checking is based on binary-equals of the serialized
// representation.
this.userData = ImmutableSortedMap.copyOf(userData);
replaceStep(createStep());
return this;
}

private FormatterStep createStep() {
return KtLintStep.createForScript(version, provisioner(), userData);
return KtLintStep.createForScript(version, provisioner(), useExperimental, userData);
}
}

Expand Down
Expand Up @@ -139,6 +139,67 @@ void testWithIndentation() throws IOException {
assertThat(result.getOutput()).contains("Unexpected indentation (4) (it should be 6)");
}

@Test
void withExperimental() throws IOException {
setFile("build.gradle").toLines(
"plugins {",
" id 'org.jetbrains.kotlin.jvm' version '1.5.31'",
" id 'com.diffplug.spotless'",
"}",
"repositories { mavenCentral() }",
"spotless {",
" kotlin {",
" ktlint().setUseExperimental(true)",
" }",
"}");
setFile("src/main/kotlin/experimental.kt").toResource("kotlin/ktlint/experimental.dirty");
gradleRunner().withArguments("spotlessApply").build();
assertFile("src/main/kotlin/experimental.kt").sameAsResource("kotlin/ktlint/experimental.clean");
}

@Test
void withExperimental_0_32() throws IOException {
setFile("build.gradle").toLines(
"plugins {",
" id 'org.jetbrains.kotlin.jvm' version '1.5.31'",
" id 'com.diffplug.spotless'",
"}",
"repositories { mavenCentral() }",
"spotless {",
" kotlin {",
" ktlint('0.32.0').setUseExperimental(true)",
" }",
"}");
setFile("src/main/kotlin/basic.kt").toResource("kotlin/ktlint/basic.dirty");
gradleRunner().withArguments("spotlessApply").build();
assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktlint/basic.clean");
}

/**
* Check that the sample used to verify the experimental ruleset is untouched by the default ruleset, to verify
* that enabling the experimental ruleset is actually doing something.
*
* If this test fails, it's likely that the experimental rule being used as a test graduated into the standard
* ruleset, and therefore a new experimental rule should be used to verify functionality.
*/
@Test
void experimentalSampleUnchangedWithDefaultRuleset() throws IOException {
setFile("build.gradle").toLines(
"plugins {",
" id 'org.jetbrains.kotlin.jvm' version '1.5.31'",
" id 'com.diffplug.spotless'",
"}",
"repositories { mavenCentral() }",
"spotless {",
" kotlin {",
" ktlint()",
" }",
"}");
setFile("src/main/kotlin/experimental.kt").toResource("kotlin/ktlint/experimental.dirty");
gradleRunner().withArguments("spotlessApply").build();
assertFile("src/main/kotlin/experimental.kt").sameAsResource("kotlin/ktlint/experimental.dirty");
}

@Test
void testWithHeader() throws IOException {
setFile("build.gradle").toLines(
Expand Down