Skip to content

Commit

Permalink
Support Ktlint 1.0.0 (#1808 fixes #1803)
Browse files Browse the repository at this point in the history
  • Loading branch information
nedtwigg authored Sep 20, 2023
2 parents eb5dc61 + 4047ae0 commit d680622
Show file tree
Hide file tree
Showing 16 changed files with 315 additions and 28 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
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 Ktlint 1.0.0 ([#1808](https://github.com/diffplug/spotless/pull/1808)).

### Fixed
* Added support for plugins when using Prettier version `3.0.0` and newer. ([#1802](https://github.com/diffplug/spotless/pull/1802))
Expand Down
7 changes: 6 additions & 1 deletion lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ versionCompatibility {
'0.48.0',
'0.49.0',
'0.50.0',
'1.0.0',
]
targetSourceSetName = 'ktlint'
}
Expand Down Expand Up @@ -104,10 +105,14 @@ dependencies {
compatKtLint0Dot49Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-rule-engine:0.49.0'
compatKtLint0Dot49Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:0.49.0'
compatKtLint0Dot49Dot0CompileAndTestOnly 'org.slf4j:slf4j-api:2.0.0'
// ktlint latest supported version
// ktlint previous supported version
compatKtLint0Dot50Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-rule-engine:0.50.0'
compatKtLint0Dot50Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:0.50.0'
compatKtLint0Dot50Dot0CompileAndTestOnly 'org.slf4j:slf4j-api:2.0.0'
// ktlint latest supported version
compatKtLint1Dot0Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-rule-engine:1.0.0'
compatKtLint1Dot0Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:1.0.0'
compatKtLint1Dot0Dot0CompileAndTestOnly 'org.slf4j:slf4j-api:2.0.0'
// palantirJavaFormat
palantirJavaFormatCompileOnly 'com.palantir.javaformat:palantir-java-format:1.1.0' // this version needs to stay compilable against Java 8 for CI Job testNpm
// scalafmt
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright 2023 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.glue.ktlint.compat;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3;
import com.pinterest.ktlint.rule.engine.api.Code;
import com.pinterest.ktlint.rule.engine.api.EditorConfigDefaults;
import com.pinterest.ktlint.rule.engine.api.EditorConfigOverride;
import com.pinterest.ktlint.rule.engine.api.KtLintRuleEngine;
import com.pinterest.ktlint.rule.engine.api.LintError;
import com.pinterest.ktlint.rule.engine.core.api.Rule;
import com.pinterest.ktlint.rule.engine.core.api.RuleId;
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider;
import com.pinterest.ktlint.rule.engine.core.api.RuleSetId;
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleEditorConfigPropertyKt;
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty;
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EndOfLinePropertyKt;
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.IndentSizeEditorConfigPropertyKt;
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.IndentStyleEditorConfigPropertyKt;
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.InsertFinalNewLineEditorConfigPropertyKt;
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.MaxLineLengthEditorConfigPropertyKt;
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecution;
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.RuleExecutionEditorConfigPropertyKt;

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

public class KtLintCompat1Dot0Dot0Adapter implements KtLintCompatAdapter {

private static final Logger logger = LoggerFactory.getLogger(KtLintCompat1Dot0Dot0Adapter.class);

private static final List<EditorConfigProperty<?>> DEFAULT_EDITOR_CONFIG_PROPERTIES;

static {
List<EditorConfigProperty<?>> list = new ArrayList<>();
list.add(CodeStyleEditorConfigPropertyKt.getCODE_STYLE_PROPERTY());
list.add(EndOfLinePropertyKt.getEND_OF_LINE_PROPERTY());
list.add(IndentSizeEditorConfigPropertyKt.getINDENT_SIZE_PROPERTY());
list.add(IndentStyleEditorConfigPropertyKt.getINDENT_STYLE_PROPERTY());
list.add(InsertFinalNewLineEditorConfigPropertyKt.getINSERT_FINAL_NEWLINE_PROPERTY());
list.add(MaxLineLengthEditorConfigPropertyKt.getMAX_LINE_LENGTH_PROPERTY());
list.add(RuleExecutionEditorConfigPropertyKt.getEXPERIMENTAL_RULES_EXECUTION_PROPERTY());
DEFAULT_EDITOR_CONFIG_PROPERTIES = Collections.unmodifiableList(list);
}

static class FormatterCallback implements Function2<LintError, Boolean, Unit> {

@Override
public Unit invoke(LintError lint, Boolean corrected) {
if (!corrected) {
KtLintCompatReporting.report(lint.getLine(), lint.getCol(), lint.getRuleId().getValue(), lint.getDetail());
}
return Unit.INSTANCE;
}
}

@Override
public String format(final String text, Path path, final boolean isScript,
Path editorConfigPath, final Map<String, String> userData,
final Map<String, Object> editorConfigOverrideMap) {
final FormatterCallback formatterCallback = new FormatterCallback();

Set<RuleProvider> allRuleProviders = ServiceLoader.load(RuleSetProviderV3.class, RuleSetProviderV3.class.getClassLoader())
.stream()
.flatMap(loader -> loader.get().getRuleProviders().stream())
.collect(Collectors.toUnmodifiableSet());

EditorConfigOverride editorConfigOverride;
if (editorConfigOverrideMap.isEmpty()) {
editorConfigOverride = EditorConfigOverride.Companion.getEMPTY_EDITOR_CONFIG_OVERRIDE();
} else {
editorConfigOverride = createEditorConfigOverride(allRuleProviders.stream().map(
RuleProvider::createNewRuleInstance).collect(Collectors.toList()),
editorConfigOverrideMap);
}
EditorConfigDefaults editorConfig;
if (editorConfigPath == null || !Files.exists(editorConfigPath)) {
editorConfig = EditorConfigDefaults.Companion.getEMPTY_EDITOR_CONFIG_DEFAULTS();
} else {
editorConfig = EditorConfigDefaults.Companion.load(editorConfigPath, Collections.emptySet());
}

return new KtLintRuleEngine(
allRuleProviders,
editorConfig,
editorConfigOverride,
false,
path.getFileSystem())
.format(Code.Companion.fromPath(path), formatterCallback);
}

/**
* Create EditorConfigOverride from user provided parameters.
*/
private static EditorConfigOverride createEditorConfigOverride(final List<Rule> rules, Map<String, Object> editorConfigOverrideMap) {
// Get properties from rules in the rule sets
Stream<EditorConfigProperty<?>> ruleProperties = rules.stream()
.flatMap(rule -> rule.getUsesEditorConfigProperties().stream());

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

// The default style had been changed from intellij_idea to ktlint_official in version 1.0.0
if (!editorConfigOverrideMap.containsKey("ktlint_code_style")) {
editorConfigOverrideMap.put("ktlint_code_style", "intellij_idea");
}

// Create config properties based on provided property names and values
@SuppressWarnings("unchecked")
Pair<EditorConfigProperty<?>, ?>[] properties = editorConfigOverrideMap.entrySet().stream()
.map(entry -> {
EditorConfigProperty<?> property = supportedProperties.get(entry.getKey());

if (property == null && entry.getKey().startsWith("ktlint_")) {
String[] parts = entry.getKey().substring(7).split("_", 2);
if (parts.length == 1) {
// convert ktlint_{ruleset} to RuleSetId
RuleSetId id = new RuleSetId(parts[0]);
property = RuleExecutionEditorConfigPropertyKt.createRuleSetExecutionEditorConfigProperty(id, RuleExecution.enabled);
} else {
// convert ktlint_{ruleset}_{rulename} to RuleId
RuleId id = new RuleId(parts[0] + ":" + parts[1]);
property = RuleExecutionEditorConfigPropertyKt.createRuleExecutionEditorConfigProperty(id, RuleExecution.enabled);
}
}

if (property == null) {
return null;
} else {
return new Pair<>(property, entry.getValue());
}
})
.filter(Objects::nonNull)
.toArray(Pair[]::new);

return EditorConfigOverride.Companion.from(properties);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,28 @@ public class KtlintFormatterFunc implements FormatterFunc.NeedsFile {

public KtlintFormatterFunc(String version, boolean isScript, FileSignature editorConfigPath, Map<String, String> userData,
Map<String, Object> editorConfigOverrideMap) {
int minorVersion = Integer.parseInt(version.split("\\.")[1]);
if (minorVersion >= 50) {
// Fixed `RuleId` and `RuleSetId` issues
// New argument to `EditorConfigDefaults.Companion.load(...)` for custom property type parsing
// New argument to `new KtLintRuleEngine(...)` to fail on usage of `treeCopyHandler` extension point
this.adapter = new KtLintCompat0Dot50Dot0Adapter();
} else if (minorVersion == 49) {
// Packages and modules moved around (`ktlint-core` -> `ktlint-rule-engine`)
// Experimental ruleset was replaced by implementing `Rule.Experimental` and checking the `ktlint_experimental` `.editorconfig` property
// `RuleId` and `RuleSetId` became inline classes (mangled to be unrepresentable in Java source code, so reflection is needed), tracked here: https://github.com/pinterest/ktlint/issues/2041
this.adapter = new KtLintCompat0Dot49Dot0Adapter();
} else if (minorVersion == 48) {
// ExperimentalParams lost two constructor arguments, EditorConfigProperty moved to its own class
this.adapter = new KtLintCompat0Dot48Dot0Adapter();
String[] versions = version.split("\\.");
int majorVersion = Integer.parseInt(versions[0]);
int minorVersion = Integer.parseInt(versions[1]);
if (majorVersion == 1) {
this.adapter = new KtLintCompat1Dot0Dot0Adapter();
} else {
throw new IllegalStateException("Ktlint versions < 0.48.0 not supported!");
if (minorVersion >= 50) {
// Fixed `RuleId` and `RuleSetId` issues
// New argument to `EditorConfigDefaults.Companion.load(...)` for custom property type parsing
// New argument to `new KtLintRuleEngine(...)` to fail on usage of `treeCopyHandler` extension point
this.adapter = new KtLintCompat0Dot50Dot0Adapter();
} else if (minorVersion == 49) {
// Packages and modules moved around (`ktlint-core` -> `ktlint-rule-engine`)
// Experimental ruleset was replaced by implementing `Rule.Experimental` and checking the `ktlint_experimental` `.editorconfig` property
// `RuleId` and `RuleSetId` became inline classes (mangled to be unrepresentable in Java source code, so reflection is needed), tracked here: https://github.com/pinterest/ktlint/issues/2041
this.adapter = new KtLintCompat0Dot49Dot0Adapter();
} else if (minorVersion == 48) {
// ExperimentalParams lost two constructor arguments, EditorConfigProperty moved to its own class
this.adapter = new KtLintCompat0Dot48Dot0Adapter();
} else {
throw new IllegalStateException("Ktlint versions < 0.48.0 not supported!");
}
}
this.editorConfigPath = editorConfigPath;
this.editorConfigOverrideMap = editorConfigOverrideMap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ public class KtLintStep {
// prevent direct instantiation
private KtLintStep() {}

private static final String DEFAULT_VERSION = "0.50.0";
private static final String DEFAULT_VERSION = "1.0.0";
static final String NAME = "ktlint";
static final String PACKAGE = "com.pinterest";
static final String MAVEN_COORDINATE = PACKAGE + ":ktlint:";
static final String MAVEN_COORDINATE_0_DOT = "com.pinterest:ktlint:";
static final String MAVEN_COORDINATE_1_DOT = "com.pinterest.ktlint:ktlint-cli:";

public static FormatterStep create(Provisioner provisioner) {
return create(defaultVersion(), provisioner);
Expand Down Expand Up @@ -110,7 +110,8 @@ static final class State implements Serializable {
this.version = version;
this.userData = new TreeMap<>(userData);
this.editorConfigOverride = new TreeMap<>(editorConfigOverride);
this.jarState = JarState.from(MAVEN_COORDINATE + version, provisioner);
this.jarState = JarState.from((version.startsWith("0.") ? MAVEN_COORDINATE_0_DOT : MAVEN_COORDINATE_1_DOT) + version,
provisioner);
this.editorConfigPath = editorConfigPath;
this.isScript = isScript;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2023 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.glue.ktlint.compat;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

public class KtLintCompat1Dot0Dot0AdapterTest {
@Test
public void testDefaults(@TempDir Path path) throws IOException {
KtLintCompat1Dot0Dot0Adapter KtLintCompat1Dot0Dot0Adapter = new KtLintCompat1Dot0Dot0Adapter();
String text = loadAndWriteText(path, "EmptyClassBody.kt");
final Path filePath = Paths.get(path.toString(), "EmptyClassBody.kt");

Map<String, String> userData = new HashMap<>();

Map<String, Object> editorConfigOverrideMap = new HashMap<>();

String formatted = KtLintCompat1Dot0Dot0Adapter.format(text, filePath, false, null, userData, editorConfigOverrideMap);
assertEquals("class EmptyClassBody\n", formatted);
}

@Test
public void testEditorConfigCanDisable(@TempDir Path path) throws IOException {
KtLintCompat1Dot0Dot0Adapter KtLintCompat1Dot0Dot0Adapter = new KtLintCompat1Dot0Dot0Adapter();
String text = loadAndWriteText(path, "FailsNoSemicolons.kt");
final Path filePath = Paths.get(path.toString(), "FailsNoSemicolons.kt");

Map<String, String> userData = new HashMap<>();

Map<String, Object> editorConfigOverrideMap = new HashMap<>();
editorConfigOverrideMap.put("indent_style", "tab");
editorConfigOverrideMap.put("ktlint_standard_no-semi", "disabled");

String formatted = KtLintCompat1Dot0Dot0Adapter.format(text, filePath, false, null, userData, editorConfigOverrideMap);
assertEquals("class FailsNoSemicolons {\n\tval i = 0;\n}\n", formatted);
}

private static String loadAndWriteText(Path path, String name) throws IOException {
try (InputStream is = KtLintCompat1Dot0Dot0AdapterTest.class.getResourceAsStream("/" + name)) {
Files.copy(is, path.resolve(name));
}
return new String(Files.readAllBytes(path.resolve(name)), StandardCharsets.UTF_8);
}

}
3 changes: 3 additions & 0 deletions lib/src/testCompatKtLint1Dot0Dot0/resources/EmptyClassBody.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class EmptyClassBody {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class FailsNoSemicolons {
val i = 0;
}
2 changes: 2 additions & 0 deletions plugin-gradle/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`).

## [Unreleased]
### Added
* Support Ktlint 1.0.0 ([#1808](https://github.com/diffplug/spotless/pull/1808)).

### Fixed
* Added support for plugins when using Prettier version `3.0.0` and newer. ([#1802](https://github.com/diffplug/spotless/pull/1802))
Expand Down
8 changes: 6 additions & 2 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,14 @@ Additionally, `editorConfigOverride` options will override what's supplied in `.
spotless {
kotlin {
// version, userData and editorConfigOverride are all optional
ktlint("0.50.0")
ktlint("1.0.0")
.userData(mapOf("android" to "true"))
.setEditorConfigPath("$projectDir/config/.editorconfig") // sample unusual placement
.editorConfigOverride(mapOf("indent_size" to 2))
.editorConfigOverride(
mapOf(
"indent_size" to 2,
)
)
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.attributes.Bundling;
import org.gradle.api.attributes.Category;
import org.gradle.api.initialization.dsl.ScriptHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -121,6 +122,7 @@ private static Provisioner forConfigurationContainer(Project project, Configurat
config.setCanBeConsumed(false);
config.setVisible(false);
config.attributes(attr -> {
attr.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, Category.LIBRARY));
attr.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.EXTERNAL));
});
return config.resolve();
Expand Down
Loading

0 comments on commit d680622

Please sign in to comment.