Skip to content

Commit

Permalink
Add support for KtLint editorConfigOverride (#1218 fixes #1193)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg committed Jun 5, 2022
2 parents 873f0ef + 75616a0 commit 95dc988
Show file tree
Hide file tree
Showing 14 changed files with 273 additions and 46 deletions.
2 changes: 2 additions & 0 deletions .editorconfig
Expand Up @@ -16,6 +16,8 @@ indent_size = 2
# Doc: https://youtrack.jetbrains.com/issue/IDEA-170643#focus=streamItem-27-3708697.0-0
ij_java_imports_layout = java.**,|,javax.**,|,org.**,|,com.**,|,com.diffplug.**,|,*
ij_java_use_single_class_imports = true
ij_java_class_count_to_use_import_on_demand = 999
ij_java_names_count_to_use_import_on_demand = 999

[*.xml.mustache]
indent_style = space
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
* Support for `editorConfigOverride` in `ktlint`. ([#1218](https://github.com/diffplug/spotless/pull/1218) fixes [#1193](https://github.com/diffplug/spotless/issues/1193))

## [2.25.3] - 2022-05-10
### Fixed
Expand Down
Expand Up @@ -17,18 +17,26 @@

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.pinterest.ktlint.core.KtLint;
import com.pinterest.ktlint.core.KtLint.Params;
import com.pinterest.ktlint.core.KtLint.ExperimentalParams;
import com.pinterest.ktlint.core.LintError;
import com.pinterest.ktlint.core.RuleSet;
import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties;
import com.pinterest.ktlint.core.api.EditorConfigOverride;
import com.pinterest.ktlint.core.api.UsesEditorConfigProperties;
import com.pinterest.ktlint.ruleset.experimental.ExperimentalRuleSetProvider;
import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider;

import com.diffplug.spotless.FormatterFunc;

import kotlin.Pair;
import kotlin.Unit;
import kotlin.jvm.functions.Function2;

Expand All @@ -38,8 +46,13 @@ public class KtlintFormatterFunc implements FormatterFunc.NeedsFile {
private final Map<String, String> userData;
private final Function2<? super LintError, ? super Boolean, Unit> formatterCallback;
private final boolean isScript;
private final EditorConfigOverride editorConfigOverride;

public KtlintFormatterFunc(boolean isScript, boolean useExperimental, Map<String, String> userData) {
/**
* Non-empty editorConfigOverrideMap requires KtLint 0.45.2.
*/
public KtlintFormatterFunc(boolean isScript, boolean useExperimental, Map<String, String> userData,
Map<String, Object> editorConfigOverrideMap) {
rulesets = new ArrayList<>();
rulesets.add(new StandardRuleSetProvider().get());

Expand All @@ -49,6 +62,46 @@ public KtlintFormatterFunc(boolean isScript, boolean useExperimental, Map<String
this.userData = userData;
formatterCallback = new FormatterCallback();
this.isScript = isScript;

if (editorConfigOverrideMap.isEmpty()) {
this.editorConfigOverride = null;
} else {
this.editorConfigOverride = createEditorConfigOverride(editorConfigOverrideMap);
}
}

/**
* Create EditorConfigOverride from user provided parameters.
* Calling this method requires KtLint 0.45.2.
*/
private EditorConfigOverride createEditorConfigOverride(Map<String, Object> editorConfigOverrideMap) {
// Get properties from rules in the rule sets
Stream<UsesEditorConfigProperties.EditorConfigProperty<?>> ruleProperties = rulesets.stream()
.flatMap(ruleSet -> Arrays.stream(ruleSet.getRules()))
.filter(rule -> rule instanceof UsesEditorConfigProperties)
.flatMap(rule -> ((UsesEditorConfigProperties) rule).getEditorConfigProperties().stream());

// Create a mapping of properties to their names based on rule properties and default properties
Map<String, UsesEditorConfigProperties.EditorConfigProperty<?>> supportedProperties = Stream
.concat(ruleProperties, DefaultEditorConfigProperties.INSTANCE.getDefaultEditorConfigProperties().stream())
.distinct()
.collect(Collectors.toMap(property -> property.getType().getName(), property -> property));

// Create config properties based on provided property names and values
@SuppressWarnings("unchecked")
Pair<UsesEditorConfigProperties.EditorConfigProperty<?>, ?>[] properties = editorConfigOverrideMap.entrySet().stream()
.map(entry -> {
UsesEditorConfigProperties.EditorConfigProperty<?> property = supportedProperties.get(entry.getKey());
if (property != null) {
return new Pair<>(property, entry.getValue());
} else {
return null;
}
})
.filter(Objects::nonNull)
.toArray(Pair[]::new);

return EditorConfigOverride.Companion.from(properties);
}

static class FormatterCallback implements Function2<LintError, Boolean, Unit> {
Expand All @@ -63,14 +116,31 @@ public Unit invoke(LintError lint, Boolean corrected) {

@Override
public String applyWithFile(String unix, File file) throws Exception {
return KtLint.INSTANCE.format(new Params(
file.getName(),
unix,
rulesets,
userData,
formatterCallback,
isScript,
null,
false));

if (editorConfigOverride != null) {
// Use ExperimentalParams with EditorConfigOverride which requires KtLint 0.45.2
return KtLint.INSTANCE.format(new ExperimentalParams(
file.getName(),
unix,
rulesets,
userData,
formatterCallback,
isScript,
null,
false,
editorConfigOverride,
false));
} else {
// Use Params for backward compatibility
return KtLint.INSTANCE.format(new KtLint.Params(
file.getName(),
unix,
rulesets,
userData,
formatterCallback,
isScript,
null,
false));
}
}
}
13 changes: 9 additions & 4 deletions lib/src/main/java/com/diffplug/spotless/kotlin/BadSemver.java
@@ -1,5 +1,5 @@
/*
* Copyright 2021 DiffPlug
* Copyright 2021-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 @@ -26,13 +26,18 @@ protected static int version(String input) {
}
String major = matcher.group(1);
String minor = matcher.group(2);
return version(Integer.parseInt(major), Integer.parseInt(minor));
String patch = matcher.group(3);
return version(Integer.parseInt(major), Integer.parseInt(minor), patch != null ? Integer.parseInt(patch) : 0);
}

/** Ambiguous after 2147.483647.blah-blah */
protected static int version(int major, int minor, int patch) {
return major * 1_000_000 + minor * 1_000 + patch;
}

protected static int version(int major, int minor) {
return major * 1_000_000 + minor;
return version(major, minor, 0);
}

private static final Pattern BAD_SEMVER = Pattern.compile("(\\d+)\\.(\\d+)");
private static final Pattern BAD_SEMVER = Pattern.compile("(\\d+)\\.(\\d+)\\.*(\\d+)*");
}
34 changes: 23 additions & 11 deletions lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java
Expand Up @@ -50,26 +50,29 @@ public static FormatterStep create(Provisioner provisioner) {
}

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

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

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

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

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

Expand All @@ -87,11 +90,20 @@ static final class State implements Serializable {
final JarState jarState;
private final boolean useExperimental;
private final TreeMap<String, String> userData;
private final TreeMap<String, Object> editorConfigOverride;
private final boolean useParams;

State(String version, Provisioner provisioner, boolean isScript, boolean useExperimental, Map<String, String> userData) throws IOException {
State(String version, Provisioner provisioner, boolean isScript, boolean useExperimental,
Map<String, String> userData, Map<String, Object> editorConfigOverride) throws IOException {

if (!editorConfigOverride.isEmpty() &&
BadSemver.version(version) < BadSemver.version(0, 45, 2)) {
throw new IllegalStateException("KtLint editorConfigOverride supported for version 0.45.2 and later");
}

this.useExperimental = useExperimental;
this.userData = new TreeMap<>(userData);
this.editorConfigOverride = new TreeMap<>(editorConfigOverride);
String coordinate;
if (BadSemver.version(version) < BadSemver.version(0, 32)) {
coordinate = MAVEN_COORDINATE_PRE_0_32;
Expand All @@ -108,8 +120,8 @@ 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, boolean.class, Map.class);
return (FormatterFunc.NeedsFile) constructor.newInstance(isScript, useExperimental, userData);
Constructor<?> constructor = formatterFunc.getConstructor(boolean.class, boolean.class, Map.class, Map.class);
return (FormatterFunc.NeedsFile) constructor.newInstance(isScript, useExperimental, userData, editorConfigOverride);
}

ClassLoader classLoader = jarState.getClassLoader();
Expand Down
3 changes: 3 additions & 0 deletions plugin-gradle/CHANGES.md
Expand Up @@ -3,6 +3,9 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).

## [Unreleased]
### Added
* Support for `editorConfigOverride` in `ktlint`. ([#1218](https://github.com/diffplug/spotless/pull/1218) fixes [#1193](https://github.com/diffplug/spotless/issues/1193))
* If you are using properties like `indent_size`, you should pass now pass them as `editorConfigOverride` and not as `userData`.

## [6.6.1] - 2022-05-13
### Fixed
Expand Down
11 changes: 7 additions & 4 deletions plugin-gradle/README.md
Expand Up @@ -335,15 +335,18 @@ spotless {

### ktlint

[homepage](https://github.com/pinterest/ktlint). [changelog](https://github.com/pinterest/ktlint/releases). Spotless does not ([yet](https://github.com/diffplug/spotless/issues/142)) respect the `.editorconfig` settings ([ktlint docs](https://github.com/pinterest/ktlint#editorconfig)), but you can provide them manually as `userData`.
[homepage](https://github.com/pinterest/ktlint). [changelog](https://github.com/pinterest/ktlint/releases). Spotless does not ([yet](https://github.com/diffplug/spotless/issues/142)) respect the `.editorconfig` settings ([ktlint docs](https://github.com/pinterest/ktlint#editorconfig)), but you can provide them manually as `editorConfigOverride`.

```kotlin
spotless {
kotlin {
// version, setUseExperimental and userData are all optional
ktlint('0.43.2')
// version, setUseExperimental, userData and editorConfigOverride are all optional
ktlint("0.45.2")
.setUseExperimental(true)
.userData(mapOf('indent_size' to '2', 'continuation_indent_size' to '2'))
.userData(mapOf("android" to "true"))
.editorConfigOverride(mapOf("indent_size" to 2))
}
}
```

### diktat
Expand Down
Expand Up @@ -30,6 +30,7 @@
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;

import com.diffplug.common.collect.ImmutableSortedMap;
import com.diffplug.spotless.FileSignature;
import com.diffplug.spotless.FormatterStep;
import com.diffplug.spotless.kotlin.DiktatStep;
Expand Down Expand Up @@ -59,7 +60,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, false, Collections.emptyMap());
return new KotlinFormatExtension(version, false, Collections.emptyMap(), Collections.emptyMap());
}

public KotlinFormatExtension ktlint() {
Expand All @@ -71,11 +72,14 @@ public class KotlinFormatExtension {
private final String version;
private boolean useExperimental;
private Map<String, String> userData;
private Map<String, Object> editorConfigOverride;

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

Expand All @@ -88,13 +92,21 @@ public KotlinFormatExtension setUseExperimental(boolean useExperimental) {
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;
this.userData = ImmutableSortedMap.copyOf(userData);
replaceStep(createStep());
return this;
}

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

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

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, false, Collections.emptyMap());
return new KotlinFormatExtension(version, false, Collections.emptyMap(), Collections.emptyMap());
}

public KotlinFormatExtension ktlint() {
Expand All @@ -57,11 +57,14 @@ public class KotlinFormatExtension {
private final String version;
private boolean useExperimental;
private Map<String, String> userData;
private Map<String, Object> editorConfigOverride;

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

Expand All @@ -79,8 +82,16 @@ public KotlinFormatExtension userData(Map<String, String> userData) {
return this;
}

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

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

Expand Down

0 comments on commit 95dc988

Please sign in to comment.