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

Support Ktlint 1.0.0 #1808

Merged
merged 5 commits into from
Sep 20, 2023
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
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)).

## [2.41.0] - 2023-08-29
### Added
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'
Goooler marked this conversation as resolved.
Show resolved Hide resolved
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]);
Goooler marked this conversation as resolved.
Show resolved Hide resolved
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);
nedtwigg marked this conversation as resolved.
Show resolved Hide resolved
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)).

## [6.21.0] - 2023-08-29
### Added
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));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

attr.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.EXTERNAL));
});
return config.resolve();
Expand Down
Loading