diff --git a/CHANGES.md b/CHANGES.md index ab72dc3ab0..0faf333268 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,11 @@ 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 +* `enum OnMatch { INCLUDE, EXCLUDE }` so that `FormatterStep.filterByContent` can not only include based on the pattern but also exclude. ([#1749](https://github.com/diffplug/spotless/pull/1749)) +* Bump default `ktlint` version to latest `0.49.1` -> `0.50.0`.([#1741](https://github.com/diffplug/spotless/issues/1741)) + * Dropped support for `ktlint 0.47.x` following our policy of supporting two breaking changes at a time. + * Dropped support for deprecated `useExperimental` parameter in favor of the `ktlint_experimental` property. ### Changes * Bump default `cleanthat` version to latest `2.13` -> `2.16`. ([#1725](https://github.com/diffplug/spotless/pull/1725)) ### Fixed diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa7..afba109285 100755 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 508322917b..4e86b92707 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/lib/build.gradle b/lib/build.gradle index 8a2b82d196..eeb9d1d1ac 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -44,9 +44,9 @@ versionCompatibility { // we will support no more than 2 breaking changes at a time = 3 incompatible versions // we will try to drop down to only one version if a stable API can be maintained for a full year versions = [ - '0.47.0', '0.48.0', '0.49.0', + '0.50.0', ] targetSourceSetName = 'ktlint' } @@ -95,16 +95,18 @@ dependencies { strictly '1.7' // for JDK 8 compatibility } } - // ktlint - compatKtLint0Dot47Dot0CompileOnly 'com.pinterest.ktlint:ktlint-core:0.47.0' - compatKtLint0Dot47Dot0CompileOnly 'com.pinterest.ktlint:ktlint-ruleset-experimental:0.47.0' - compatKtLint0Dot47Dot0CompileOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:0.47.0' + // ktlint oldest supported version compatKtLint0Dot48Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-core:0.48.0' compatKtLint0Dot48Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-experimental:0.48.0' compatKtLint0Dot48Dot0CompileAndTestOnly 'com.pinterest.ktlint:ktlint-ruleset-standard:0.48.0' + // ktlint previous supported version 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 + 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' // 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 diff --git a/lib/src/compatKtLint0Dot47Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot47Dot0Adapter.java b/lib/src/compatKtLint0Dot47Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot47Dot0Adapter.java deleted file mode 100644 index 0a38730b6a..0000000000 --- a/lib/src/compatKtLint0Dot47Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot47Dot0Adapter.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2022-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 java.util.Collections.emptySet; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import com.pinterest.ktlint.core.KtLint; -import com.pinterest.ktlint.core.LintError; -import com.pinterest.ktlint.core.Rule; -import com.pinterest.ktlint.core.RuleProvider; -import com.pinterest.ktlint.core.api.DefaultEditorConfigProperties; -import com.pinterest.ktlint.core.api.EditorConfigDefaults; -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 kotlin.Pair; -import kotlin.Unit; -import kotlin.jvm.functions.Function2; - -public class KtLintCompat0Dot47Dot0Adapter implements KtLintCompatAdapter { - - static class FormatterCallback implements Function2 { - @Override - public Unit invoke(LintError lint, Boolean corrected) { - if (!corrected) { - KtLintCompatReporting.report(lint.getLine(), lint.getCol(), lint.getRuleId(), lint.getDetail()); - } - return null; - } - } - - @Override - public String format(final String text, Path path, final boolean isScript, - final boolean useExperimental, - Path editorConfigPath, final Map userData, - final Map editorConfigOverrideMap) { - final FormatterCallback formatterCallback = new FormatterCallback(); - - Set allRuleProviders = new LinkedHashSet<>( - new StandardRuleSetProvider().getRuleProviders()); - if (useExperimental) { - allRuleProviders.addAll(new ExperimentalRuleSetProvider().getRuleProviders()); - } - - EditorConfigOverride editorConfigOverride; - if (editorConfigOverrideMap.isEmpty()) { - editorConfigOverride = new EditorConfigOverride(); - } else { - editorConfigOverride = createEditorConfigOverride(allRuleProviders.stream().map( - RuleProvider::createNewRuleInstance).collect( - Collectors.toList()), - editorConfigOverrideMap); - } - - EditorConfigDefaults editorConfig; - if (editorConfigPath == null || !Files.exists(editorConfigPath)) { - editorConfig = EditorConfigDefaults.Companion.getEmptyEditorConfigDefaults(); - } else { - editorConfig = EditorConfigDefaults.Companion.load(editorConfigPath); - } - - return KtLint.INSTANCE.format(new KtLint.ExperimentalParams( - path.toFile().getAbsolutePath(), - text, - emptySet(), - allRuleProviders, - userData, - formatterCallback, - isScript, - null, - false, - editorConfig, - editorConfigOverride, - false)); - } - - /** - * Create EditorConfigOverride from user provided parameters. - * Calling this method requires KtLint 0.45.2. - */ - private static EditorConfigOverride createEditorConfigOverride(final List rules, Map editorConfigOverrideMap) { - // Get properties from rules in the rule sets - Stream> ruleProperties = rules.stream() - .filter(rule -> rule instanceof UsesEditorConfigProperties) - .flatMap(rule -> ((UsesEditorConfigProperties) rule).getEditorConfigProperties().stream()); - - // get complete list of supported properties in DefaultEditorConfigProperties.INSTANCE - List> editorConfigProperties = new ArrayList<>(DefaultEditorConfigProperties.INSTANCE.getEditorConfigProperties()); - editorConfigProperties.add(DefaultEditorConfigProperties.INSTANCE.getKtlintDisabledRulesProperty()); - - // Create a mapping of properties to their names based on rule properties and default properties - Map> supportedProperties = Stream - .concat(ruleProperties, editorConfigProperties.stream()) - .distinct() - .collect(Collectors.toMap(property -> property.getType().getName(), property -> property)); - - // Create config properties based on provided property names and values - @SuppressWarnings("unchecked") - Pair, ?>[] 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); - } -} diff --git a/lib/src/compatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0Adapter.java b/lib/src/compatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0Adapter.java index 1e54a80859..4ee70c6c88 100644 --- a/lib/src/compatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0Adapter.java +++ b/lib/src/compatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0Adapter.java @@ -19,10 +19,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashSet; 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; @@ -31,6 +31,7 @@ import com.pinterest.ktlint.core.LintError; import com.pinterest.ktlint.core.Rule; import com.pinterest.ktlint.core.RuleProvider; +import com.pinterest.ktlint.core.RuleSetProviderV2; import com.pinterest.ktlint.core.api.EditorConfigDefaults; import com.pinterest.ktlint.core.api.EditorConfigOverride; import com.pinterest.ktlint.core.api.UsesEditorConfigProperties; @@ -42,8 +43,6 @@ import com.pinterest.ktlint.core.api.editorconfig.InsertFinalNewLineEditorConfigPropertyKt; import com.pinterest.ktlint.core.api.editorconfig.MaxLineLengthEditorConfigPropertyKt; import com.pinterest.ktlint.core.api.editorconfig.RuleExecutionEditorConfigPropertyKt; -import com.pinterest.ktlint.ruleset.experimental.ExperimentalRuleSetProvider; -import com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider; import kotlin.Pair; import kotlin.Unit; @@ -79,16 +78,14 @@ public Unit invoke(LintError lint, Boolean corrected) { @Override public String format(final String text, Path path, final boolean isScript, - final boolean useExperimental, Path editorConfigPath, final Map userData, final Map editorConfigOverrideMap) { final FormatterCallback formatterCallback = new FormatterCallback(); - Set allRuleProviders = new LinkedHashSet<>( - new StandardRuleSetProvider().getRuleProviders()); - if (useExperimental) { - allRuleProviders.addAll(new ExperimentalRuleSetProvider().getRuleProviders()); - } + Set allRuleProviders = ServiceLoader.load(RuleSetProviderV2.class, RuleSetProviderV2.class.getClassLoader()) + .stream() + .flatMap(loader -> loader.get().getRuleProviders().stream()) + .collect(Collectors.toUnmodifiableSet()); EditorConfigOverride editorConfigOverride; if (editorConfigOverrideMap.isEmpty()) { @@ -111,7 +108,7 @@ public String format(final String text, Path path, final boolean isScript, editorConfig, editorConfigOverride, false) - .format(path, formatterCallback); + .format(path, formatterCallback); } /** @@ -134,9 +131,8 @@ private static EditorConfigOverride createEditorConfigOverride(final List Pair, ?>[] properties = editorConfigOverrideMap.entrySet().stream() .map(entry -> { EditorConfigProperty property = supportedProperties.get(entry.getKey()); - if (property != null) { - return new Pair<>(property, entry.getValue()); - } else if (entry.getKey().startsWith("ktlint_")) { + + if (property == null && entry.getKey().startsWith("ktlint_")) { String[] parts = entry.getKey().substring(7).split("_", 2); if (parts.length == 1) { // convert ktlint_{ruleset} to {ruleset} @@ -147,9 +143,12 @@ private static EditorConfigOverride createEditorConfigOverride(final List String qualifiedRuleId = parts[0] + ":" + parts[1]; property = RuleExecutionEditorConfigPropertyKt.createRuleExecutionEditorConfigProperty(qualifiedRuleId); } - return new Pair<>(property, entry.getValue()); - } else { + } + + if (property == null) { return null; + } else { + return new Pair<>(property, entry.getValue()); } }) .filter(Objects::nonNull) diff --git a/lib/src/compatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0Adapter.java b/lib/src/compatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0Adapter.java index 86ca69c352..b391c7b740 100644 --- a/lib/src/compatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0Adapter.java +++ b/lib/src/compatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0Adapter.java @@ -21,10 +21,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashSet; 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; @@ -32,6 +32,7 @@ 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; @@ -48,7 +49,6 @@ 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 com.pinterest.ktlint.ruleset.standard.StandardRuleSetProvider; import kotlin.Pair; import kotlin.Unit; @@ -123,23 +123,14 @@ public Unit invoke(LintError lint, Boolean corrected) { @Override public String format(final String text, Path path, final boolean isScript, - final boolean useExperimental, Path editorConfigPath, final Map userData, final Map editorConfigOverrideMap) { final FormatterCallback formatterCallback = new FormatterCallback(); - Set allRuleProviders = new LinkedHashSet<>( - new StandardRuleSetProvider().getRuleProviders()); - - // TODO: Should we keep `useExperimental` now that ktlint uses an EditorConfig property for this purpose? - if (useExperimental) { - String experimentalRulesPropertyName = RuleExecutionEditorConfigPropertyKt.getEXPERIMENTAL_RULES_EXECUTION_PROPERTY().getName(); - Object experimentalOverride = editorConfigOverrideMap.get(experimentalRulesPropertyName); - if (experimentalOverride != null) { - logger.warn("`useExperimental` parameter is `true` and `ktlint_experimental` property is set, `useExperimental` will take priority!"); - editorConfigOverrideMap.put(experimentalRulesPropertyName, "enabled"); - } - } + Set allRuleProviders = ServiceLoader.load(RuleSetProviderV3.class, RuleSetProviderV3.class.getClassLoader()) + .stream() + .flatMap(loader -> loader.get().getRuleProviders().stream()) + .collect(Collectors.toUnmodifiableSet()); EditorConfigOverride editorConfigOverride; if (editorConfigOverrideMap.isEmpty()) { @@ -184,22 +175,24 @@ private static EditorConfigOverride createEditorConfigOverride(final List Pair, ?>[] properties = editorConfigOverrideMap.entrySet().stream() .map(entry -> { EditorConfigProperty property = supportedProperties.get(entry.getKey()); - if (property != null) { - return new Pair<>(property, entry.getValue()); - } else if (entry.getKey().startsWith("ktlint_")) { + + if (property == null && entry.getKey().startsWith("ktlint_")) { String[] parts = entry.getKey().substring(7).split("_", 2); if (parts.length == 1) { // convert ktlint_{ruleset} to {ruleset} - String qualifiedRuleId = parts[0] + ":"; - property = createRuleSetExecution(qualifiedRuleId, RuleExecution.disabled); + String id = parts[0]; + property = createRuleSetExecution(id, RuleExecution.enabled); } else { // convert ktlint_{ruleset}_{rulename} to {ruleset}:{rulename} - String qualifiedRuleId = parts[0] + ":" + parts[1]; - property = createRuleExecution(qualifiedRuleId, RuleExecution.disabled); + String id = parts[0] + ":" + parts[1]; + property = createRuleExecution(id, RuleExecution.enabled); } - return new Pair<>(property, entry.getValue()); - } else { + } + + if (property == null) { return null; + } else { + return new Pair<>(property, entry.getValue()); } }) .filter(Objects::nonNull) diff --git a/lib/src/compatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0Adapter.java b/lib/src/compatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0Adapter.java new file mode 100644 index 0000000000..22333392c1 --- /dev/null +++ b/lib/src/compatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0Adapter.java @@ -0,0 +1,166 @@ +/* + * 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 KtLintCompat0Dot50Dot0Adapter implements KtLintCompatAdapter { + + private static final Logger logger = LoggerFactory.getLogger(KtLintCompat0Dot50Dot0Adapter.class); + + private static final List> DEFAULT_EDITOR_CONFIG_PROPERTIES; + + static { + List> 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 { + + @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 userData, + final Map editorConfigOverrideMap) { + final FormatterCallback formatterCallback = new FormatterCallback(); + + Set 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, + true, + false, + path.getFileSystem()) + .format(Code.Companion.fromPath(path), formatterCallback); + } + + /** + * Create EditorConfigOverride from user provided parameters. + */ + private static EditorConfigOverride createEditorConfigOverride(final List rules, Map editorConfigOverrideMap) { + // Get properties from rules in the rule sets + Stream> ruleProperties = rules.stream() + .flatMap(rule -> rule.getUsesEditorConfigProperties().stream()); + + // Create a mapping of properties to their names based on rule properties and default properties + Map> supportedProperties = Stream + .concat(ruleProperties, DEFAULT_EDITOR_CONFIG_PROPERTIES.stream()) + .distinct() + .collect(Collectors.toMap(EditorConfigProperty::getName, property -> property)); + + // Create config properties based on provided property names and values + @SuppressWarnings("unchecked") + Pair, ?>[] 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); + } +} diff --git a/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatAdapter.java b/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatAdapter.java index 68a65eb6c8..5e5e5fb445 100644 --- a/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatAdapter.java +++ b/lib/src/compatKtLintApi/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompatAdapter.java @@ -20,6 +20,6 @@ public interface KtLintCompatAdapter { - String format(String text, Path path, boolean isScript, boolean useExperimental, Path editorConfigPath, Map userData, + String format(String text, Path path, boolean isScript, Path editorConfigPath, Map userData, Map editorConfigOverrideMap); } diff --git a/lib/src/ktlint/java/com/diffplug/spotless/glue/ktlint/KtlintFormatterFunc.java b/lib/src/ktlint/java/com/diffplug/spotless/glue/ktlint/KtlintFormatterFunc.java index fba855bb9a..1a11e8bcce 100644 --- a/lib/src/ktlint/java/com/diffplug/spotless/glue/ktlint/KtlintFormatterFunc.java +++ b/lib/src/ktlint/java/com/diffplug/spotless/glue/ktlint/KtlintFormatterFunc.java @@ -28,14 +28,18 @@ public class KtlintFormatterFunc implements FormatterFunc.NeedsFile { private final Map userData; private final boolean isScript; private final KtLintCompatAdapter adapter; - private final boolean useExperimental; private final FileSignature editorConfigPath; private final Map editorConfigOverrideMap; - public KtlintFormatterFunc(String version, boolean isScript, boolean useExperimental, FileSignature editorConfigPath, Map userData, + public KtlintFormatterFunc(String version, boolean isScript, FileSignature editorConfigPath, Map userData, Map editorConfigOverrideMap) { int minorVersion = Integer.parseInt(version.split("\\.")[1]); - if (minorVersion >= 49) { + 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 @@ -44,11 +48,9 @@ public KtlintFormatterFunc(String version, boolean isScript, boolean useExperime // ExperimentalParams lost two constructor arguments, EditorConfigProperty moved to its own class this.adapter = new KtLintCompat0Dot48Dot0Adapter(); } else { - // rename RuleSet to RuleProvider - this.adapter = new KtLintCompat0Dot47Dot0Adapter(); + throw new IllegalStateException("Ktlint versions < 0.48.0 not supported!"); } this.editorConfigPath = editorConfigPath; - this.useExperimental = useExperimental; this.editorConfigOverrideMap = editorConfigOverrideMap; this.userData = userData; this.isScript = isScript; @@ -61,6 +63,6 @@ public String applyWithFile(String unix, File file) { if (editorConfigPath != null) { absoluteEditorConfigPath = editorConfigPath.getOnlyFile().toPath(); } - return adapter.format(unix, file.toPath(), isScript, useExperimental, absoluteEditorConfigPath, userData, editorConfigOverrideMap); + return adapter.format(unix, file.toPath(), isScript, absoluteEditorConfigPath, userData, editorConfigOverrideMap); } } diff --git a/lib/src/main/java/com/diffplug/spotless/FilterByContentPatternFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/FilterByContentPatternFormatterStep.java index d9a6fe4093..4cc336e101 100644 --- a/lib/src/main/java/com/diffplug/spotless/FilterByContentPatternFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/FilterByContentPatternFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,10 +23,12 @@ import javax.annotation.Nullable; final class FilterByContentPatternFormatterStep extends DelegateFormatterStep { + final OnMatch onMatch; final Pattern contentPattern; - FilterByContentPatternFormatterStep(FormatterStep delegateStep, String contentPattern) { + FilterByContentPatternFormatterStep(FormatterStep delegateStep, OnMatch onMatch, String contentPattern) { super(delegateStep); + this.onMatch = onMatch; this.contentPattern = Pattern.compile(Objects.requireNonNull(contentPattern)); } @@ -35,7 +37,7 @@ final class FilterByContentPatternFormatterStep extends DelegateFormatterStep { Objects.requireNonNull(raw, "raw"); Objects.requireNonNull(file, "file"); Matcher matcher = contentPattern.matcher(raw); - if (matcher.find()) { + if (matcher.find() == (onMatch == OnMatch.INCLUDE)) { return delegateStep.format(raw, file); } else { return raw; @@ -52,13 +54,14 @@ public boolean equals(Object o) { } FilterByContentPatternFormatterStep that = (FilterByContentPatternFormatterStep) o; return Objects.equals(delegateStep, that.delegateStep) && + Objects.equals(onMatch, that.onMatch) && Objects.equals(contentPattern.pattern(), that.contentPattern.pattern()); } @Override public int hashCode() { - return Objects.hash(delegateStep, contentPattern.pattern()); + return Objects.hash(delegateStep, onMatch, contentPattern.pattern()); } - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 2L; } diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStep.java b/lib/src/main/java/com/diffplug/spotless/FormatterStep.java index ce09f68450..5f6ec168d4 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStep.java @@ -53,8 +53,23 @@ public interface FormatterStep extends Serializable { * java regular expression used to filter out files which content doesn't contain pattern * @return FormatterStep */ + @Deprecated public default FormatterStep filterByContentPattern(String contentPattern) { - return new FilterByContentPatternFormatterStep(this, contentPattern); + return filterByContent(OnMatch.INCLUDE, contentPattern); + } + + /** + * Returns a new {@code FormatterStep} which, observing the value of {@code formatIfMatches}, + * will only apply, or not, its changes to files which pass the given filter. + * + * @param onMatch + * determines if matches are included or excluded + * @param contentPattern + * java regular expression used to filter in or out files which content contain pattern + * @return FormatterStep + */ + public default FormatterStep filterByContent(OnMatch onMatch, String contentPattern) { + return new FilterByContentPatternFormatterStep(this, onMatch, contentPattern); } /** diff --git a/lib/src/main/java/com/diffplug/spotless/OnMatch.java b/lib/src/main/java/com/diffplug/spotless/OnMatch.java new file mode 100644 index 0000000000..098fb28f13 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/OnMatch.java @@ -0,0 +1,21 @@ +/* + * 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; + +/** Enum to make boolean logic more readable. */ +public enum OnMatch { + INCLUDE, EXCLUDE +} diff --git a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java index 073af855d3..6d04c25962 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/LicenseHeaderStep.java @@ -38,6 +38,7 @@ import com.diffplug.spotless.FormatterFunc; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.OnMatch; import com.diffplug.spotless.SerializableFileFilter; import com.diffplug.spotless.ThrowingEx; @@ -150,7 +151,7 @@ public FormatterStep build() { return formatterStep; } - return formatterStep.filterByContentPattern(contentPattern); + return formatterStep.filterByContent(OnMatch.INCLUDE, contentPattern); } private String sanitizeName(@Nullable String name) { diff --git a/lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java b/lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java index 6e82538b04..8f3fde5731 100644 --- a/lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java @@ -36,7 +36,7 @@ public class KtLintStep { // prevent direct instantiation private KtLintStep() {} - private static final String DEFAULT_VERSION = "0.49.1"; + private static final String DEFAULT_VERSION = "0.50.0"; static final String NAME = "ktlint"; static final String PACKAGE = "com.pinterest"; static final String MAVEN_COORDINATE = PACKAGE + ":ktlint:"; @@ -46,28 +46,26 @@ public static FormatterStep create(Provisioner provisioner) { } public static FormatterStep create(String version, Provisioner provisioner) { - return create(version, provisioner, false, Collections.emptyMap(), Collections.emptyMap()); + return create(version, provisioner, Collections.emptyMap(), Collections.emptyMap()); } - public static FormatterStep create(String version, Provisioner provisioner, boolean useExperimental, + public static FormatterStep create(String version, Provisioner provisioner, Map userData, Map editorConfigOverride) { - return create(version, provisioner, false, useExperimental, null, userData, editorConfigOverride); + return create(version, provisioner, false, null, userData, editorConfigOverride); } public static FormatterStep createForScript(String version, Provisioner provisioner) { - return create(version, provisioner, true, false, null, Collections.emptyMap(), Collections.emptyMap()); + return create(version, provisioner, true, null, Collections.emptyMap(), Collections.emptyMap()); } public static FormatterStep createForScript(String version, Provisioner provisioner, - boolean useExperimental, @Nullable FileSignature editorConfigPath, Map userData, Map editorConfigOverride) { return create(version, provisioner, true, - useExperimental, editorConfigPath, userData, editorConfigOverride); @@ -76,14 +74,13 @@ public static FormatterStep createForScript(String version, public static FormatterStep create(String version, Provisioner provisioner, boolean isScript, - boolean useExperimental, @Nullable FileSignature editorConfig, Map userData, Map editorConfigOverride) { Objects.requireNonNull(version, "version"); Objects.requireNonNull(provisioner, "provisioner"); return FormatterStep.createLazy(NAME, - () -> new State(version, provisioner, isScript, useExperimental, editorConfig, userData, editorConfigOverride), + () -> new State(version, provisioner, isScript, editorConfig, userData, editorConfigOverride), State::createFormat); } @@ -98,7 +95,6 @@ static final class State implements Serializable { private final boolean isScript; /** The jar that contains the formatter. */ final JarState jarState; - private final boolean useExperimental; private final TreeMap userData; private final TreeMap editorConfigOverride; private final String version; @@ -108,15 +104,10 @@ static final class State implements Serializable { State(String version, Provisioner provisioner, boolean isScript, - boolean useExperimental, @Nullable FileSignature editorConfigPath, Map userData, Map editorConfigOverride) throws IOException { - if (BadSemver.version(version) < BadSemver.version(0, 46, 0)) { - throw new IllegalStateException("KtLint versions < 0.46.0 not supported!"); - } this.version = version; - this.useExperimental = useExperimental; this.userData = new TreeMap<>(userData); this.editorConfigOverride = new TreeMap<>(editorConfigOverride); this.jarState = JarState.from(MAVEN_COORDINATE + version, provisioner); @@ -128,8 +119,8 @@ FormatterFunc createFormat() throws Exception { final ClassLoader classLoader = jarState.getClassLoader(); Class formatterFunc = classLoader.loadClass("com.diffplug.spotless.glue.ktlint.KtlintFormatterFunc"); Constructor constructor = formatterFunc.getConstructor( - String.class, boolean.class, boolean.class, FileSignature.class, Map.class, Map.class); - return (FormatterFunc.NeedsFile) constructor.newInstance(version, isScript, useExperimental, editorConfigPath, userData, editorConfigOverride); + String.class, boolean.class, FileSignature.class, Map.class, Map.class); + return (FormatterFunc.NeedsFile) constructor.newInstance(version, isScript, editorConfigPath, userData, editorConfigOverride); } } } diff --git a/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java b/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java index c811b51233..7fc9d1d9a4 100644 --- a/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java +++ b/lib/src/testCompatKtLint0Dot48Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot48Dot0AdapterTest.java @@ -40,7 +40,7 @@ public void testDefaults(@TempDir Path path) throws IOException { Map editorConfigOverrideMap = new HashMap<>(); - String formatted = ktLintCompat0Dot48Dot0Adapter.format(text, filePath, false, false, null, userData, editorConfigOverrideMap); + String formatted = ktLintCompat0Dot48Dot0Adapter.format(text, filePath, false, null, userData, editorConfigOverrideMap); assertEquals("class empty_class_body\n", formatted); } @@ -58,7 +58,7 @@ public void testEditorConfigCanDisable(@TempDir Path path) throws IOException { // ktlint_filename is an invalid rule in ktlint 0.48.0 editorConfigOverrideMap.put("ktlint_filename", "disabled"); - String formatted = ktLintCompat0Dot48Dot0Adapter.format(text, filePath, false, false, null, userData, editorConfigOverrideMap); + String formatted = ktLintCompat0Dot48Dot0Adapter.format(text, filePath, false, null, userData, editorConfigOverrideMap); assertEquals("class fails_no_semicolons {\n\tval i = 0;\n}\n", formatted); } diff --git a/lib/src/testCompatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0AdapterTest.java b/lib/src/testCompatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0AdapterTest.java index ede67e0174..a0db6ecddc 100644 --- a/lib/src/testCompatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0AdapterTest.java +++ b/lib/src/testCompatKtLint0Dot49Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot49Dot0AdapterTest.java @@ -40,7 +40,7 @@ public void testDefaults(@TempDir Path path) throws IOException { Map editorConfigOverrideMap = new HashMap<>(); - String formatted = ktLintCompat0Dot49Dot0Adapter.format(text, filePath, false, false, null, userData, editorConfigOverrideMap); + String formatted = ktLintCompat0Dot49Dot0Adapter.format(text, filePath, false, null, userData, editorConfigOverrideMap); assertEquals("class EmptyClassBody\n", formatted); } @@ -56,7 +56,7 @@ public void testEditorConfigCanDisable(@TempDir Path path) throws IOException { editorConfigOverrideMap.put("indent_style", "tab"); editorConfigOverrideMap.put("ktlint_standard_no-semi", "disabled"); - String formatted = ktLintCompat0Dot49Dot0Adapter.format(text, filePath, false, false, null, userData, editorConfigOverrideMap); + String formatted = ktLintCompat0Dot49Dot0Adapter.format(text, filePath, false, null, userData, editorConfigOverrideMap); assertEquals("class FailsNoSemicolons {\n\tval i = 0;\n}\n", formatted); } diff --git a/lib/src/testCompatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0AdapterTest.java b/lib/src/testCompatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0AdapterTest.java new file mode 100644 index 0000000000..f5b76ccae0 --- /dev/null +++ b/lib/src/testCompatKtLint0Dot50Dot0/java/com/diffplug/spotless/glue/ktlint/compat/KtLintCompat0Dot50Dot0AdapterTest.java @@ -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 KtLintCompat0Dot50Dot0AdapterTest { + @Test + public void testDefaults(@TempDir Path path) throws IOException { + KtLintCompat0Dot50Dot0Adapter KtLintCompat0Dot50Dot0Adapter = new KtLintCompat0Dot50Dot0Adapter(); + String text = loadAndWriteText(path, "EmptyClassBody.kt"); + final Path filePath = Paths.get(path.toString(), "EmptyClassBody.kt"); + + Map userData = new HashMap<>(); + + Map editorConfigOverrideMap = new HashMap<>(); + + String formatted = KtLintCompat0Dot50Dot0Adapter.format(text, filePath, false, null, userData, editorConfigOverrideMap); + assertEquals("class EmptyClassBody\n", formatted); + } + + @Test + public void testEditorConfigCanDisable(@TempDir Path path) throws IOException { + KtLintCompat0Dot50Dot0Adapter KtLintCompat0Dot50Dot0Adapter = new KtLintCompat0Dot50Dot0Adapter(); + String text = loadAndWriteText(path, "FailsNoSemicolons.kt"); + final Path filePath = Paths.get(path.toString(), "FailsNoSemicolons.kt"); + + Map userData = new HashMap<>(); + + Map editorConfigOverrideMap = new HashMap<>(); + editorConfigOverrideMap.put("indent_style", "tab"); + editorConfigOverrideMap.put("ktlint_standard_no-semi", "disabled"); + + String formatted = KtLintCompat0Dot50Dot0Adapter.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 = KtLintCompat0Dot50Dot0AdapterTest.class.getResourceAsStream("/" + name)) { + Files.copy(is, path.resolve(name)); + } + return new String(Files.readAllBytes(path.resolve(name)), StandardCharsets.UTF_8); + } + +} diff --git a/lib/src/testCompatKtLint0Dot50Dot0/resources/EmptyClassBody.kt b/lib/src/testCompatKtLint0Dot50Dot0/resources/EmptyClassBody.kt new file mode 100644 index 0000000000..7da53fb78d --- /dev/null +++ b/lib/src/testCompatKtLint0Dot50Dot0/resources/EmptyClassBody.kt @@ -0,0 +1,3 @@ +class EmptyClassBody { + +} diff --git a/lib/src/testCompatKtLint0Dot50Dot0/resources/FailsNoSemicolons.kt b/lib/src/testCompatKtLint0Dot50Dot0/resources/FailsNoSemicolons.kt new file mode 100644 index 0000000000..4cf05ceacf --- /dev/null +++ b/lib/src/testCompatKtLint0Dot50Dot0/resources/FailsNoSemicolons.kt @@ -0,0 +1,3 @@ +class FailsNoSemicolons { + val i = 0; +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 744c6efce7..31c307d529 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,7 +3,12 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] - +### Added +* Add target option `targetExcludeIfContentContains` and `targetExcludeIfContentContainsRegex` to exclude files based on their text content. ([#1749](https://github.com/diffplug/spotless/pull/1749)) +* Bump default `ktlint` version to latest `0.49.1` -> `0.50.0`. ([#1741](https://github.com/diffplug/spotless/issues/1741)) + * Dropped support for `ktlint 0.47.x` following our policy of supporting two breaking changes at a time. + * Dropped support for deprecated `useExperimental` parameter in favor of the `ktlint_experimental` property. +* Add an overload for `FormatExtension.addStep` which provides access to the `FormatExtension`'s `Provisioner`, enabling custom steps to make use of third-party dependencies. ### Fixed * Correctly support the syntax ``` diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index ef93e31d45..704e3b694a 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -397,9 +397,8 @@ Additionally, `editorConfigOverride` options will override what's supplied in `. ```kotlin spotless { kotlin { - // version, setUseExperimental, userData and editorConfigOverride are all optional - ktlint("0.45.2") - .setUseExperimental(true) + // version, userData and editorConfigOverride are all optional + ktlint("0.50.0") .userData(mapOf("android" to "true")) .setEditorConfigPath("$projectDir/config/.editorconfig") // sample unusual placement .editorConfigOverride(mapOf("indent_size" to 2)) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index 4fa74bc2ea..e28ec13dcd 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -30,6 +30,8 @@ import java.util.Random; import java.util.TreeMap; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.inject.Inject; @@ -48,6 +50,7 @@ import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.LazyForwardingEquality; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.OnMatch; import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.cpp.ClangFormatStep; import com.diffplug.spotless.extra.EclipseBasedStepBuilder; @@ -161,6 +164,10 @@ public void encoding(String charset) { /** The files to be formatted = (target - targetExclude). */ protected FileCollection target, targetExclude; + /** The value from which files will be excluded if their content contain it. */ + @Nullable + protected String targetExcludeContentPattern = null; + protected boolean isLicenseHeaderStep(FormatterStep formatterStep) { String formatterStepName = formatterStep.getName(); @@ -202,6 +209,24 @@ public void targetExclude(Object... targets) { this.targetExclude = parseTargetsIsExclude(targets, true); } + /** + * Excludes all files whose content contains {@code string}. + * + * When this method is called multiple times, only the last call has any effect. + */ + public void targetExcludeIfContentContains(String string) { + targetExcludeIfContentContainsRegex(Pattern.quote(string)); + } + + /** + * Excludes all files whose content contains the given regex. + * + * When this method is called multiple times, only the last call has any effect. + */ + public void targetExcludeIfContentContainsRegex(String regex) { + this.targetExcludeContentPattern = regex; + } + private FileCollection parseTargetsIsExclude(Object[] targets, boolean isExclude) { requireElementsNonNull(targets); if (targets.length == 0) { @@ -300,6 +325,13 @@ public void addStep(FormatterStep newStep) { steps.add(newStep); } + /** Adds a new step that requires a Provisioner. */ + public void addStep(Function createStepFn) { + requireNonNull(createStepFn); + FormatterStep newStep = createStepFn.apply(provisioner()); + addStep(newStep); + } + /** Returns the index of the existing step with the given name, or -1 if no such step exists. */ protected int getExistingStepIdx(String stepName) { for (int i = 0; i < steps.size(); ++i) { @@ -889,6 +921,9 @@ protected void setupTask(SpotlessTask task) { } else { steps = this.steps; } + if (targetExcludeContentPattern != null) { + steps.replaceAll(formatterStep -> formatterStep.filterByContent(OnMatch.EXCLUDE, targetExcludeContentPattern)); + } task.setSteps(steps); task.setLineEndingsPolicy(getLineEndings().createPolicy(getProject().getProjectDir(), () -> totalTarget)); spotless.getRegisterDependenciesTask().hookSubprojectTask(task); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java index 363a9cac6f..f2c3a29d75 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java @@ -61,7 +61,7 @@ public KotlinFormatExtension ktlint(String version) throws IOException { Objects.requireNonNull(version); File defaultEditorConfig = getProject().getRootProject().file(".editorconfig"); FileSignature editorConfigPath = defaultEditorConfig.exists() ? FileSignature.signAsList(defaultEditorConfig) : null; - return new KotlinFormatExtension(version, false, editorConfigPath, Collections.emptyMap(), Collections.emptyMap()); + return new KotlinFormatExtension(version, editorConfigPath, Collections.emptyMap(), Collections.emptyMap()); } public KotlinFormatExtension ktlint() throws IOException { @@ -71,28 +71,20 @@ public KotlinFormatExtension ktlint() throws IOException { public class KotlinFormatExtension { private final String version; - private boolean useExperimental; @Nullable private FileSignature editorConfigPath; private Map userData; private Map editorConfigOverride; - KotlinFormatExtension(String version, boolean useExperimental, @Nullable FileSignature editorConfigPath, Map config, + KotlinFormatExtension(String version, @Nullable FileSignature editorConfigPath, Map config, Map editorConfigOverride) { this.version = version; - this.useExperimental = useExperimental; this.editorConfigPath = editorConfigPath; this.userData = config; this.editorConfigOverride = editorConfigOverride; addStep(createStep()); } - public KotlinFormatExtension setUseExperimental(boolean useExperimental) { - this.useExperimental = useExperimental; - replaceStep(createStep()); - return this; - } - public KotlinFormatExtension setEditorConfigPath(Object editorConfigFile) throws IOException { if (editorConfigFile == null) { this.editorConfigPath = null; @@ -120,7 +112,7 @@ public KotlinFormatExtension editorConfigOverride(Map editorConf } private FormatterStep createStep() { - return KtLintStep.create(version, provisioner(), useExperimental, false, editorConfigPath, userData, editorConfigOverride); + return KtLintStep.create(version, provisioner(), false, editorConfigPath, userData, editorConfigOverride); } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java index a0657e99d9..b3351194ba 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java @@ -49,7 +49,7 @@ public KotlinFormatExtension ktlint(String version) throws IOException { Objects.requireNonNull(version, "version"); File defaultEditorConfig = getProject().getRootProject().file(".editorconfig"); FileSignature editorConfigPath = defaultEditorConfig.exists() ? FileSignature.signAsList(defaultEditorConfig) : null; - return new KotlinFormatExtension(version, false, editorConfigPath, Collections.emptyMap(), Collections.emptyMap()); + return new KotlinFormatExtension(version, editorConfigPath, Collections.emptyMap(), Collections.emptyMap()); } public KotlinFormatExtension ktlint() throws IOException { @@ -59,16 +59,14 @@ public KotlinFormatExtension ktlint() throws IOException { public class KotlinFormatExtension { private final String version; - private boolean useExperimental; @Nullable private FileSignature editorConfigPath; private Map userData; private Map editorConfigOverride; - KotlinFormatExtension(String version, boolean useExperimental, FileSignature editorConfigPath, Map config, + KotlinFormatExtension(String version, FileSignature editorConfigPath, Map config, Map editorConfigOverride) { this.version = version; - this.useExperimental = useExperimental; this.editorConfigPath = editorConfigPath; this.userData = config; this.editorConfigOverride = editorConfigOverride; @@ -86,12 +84,6 @@ public KotlinFormatExtension setEditorConfigPath(Object editorConfigPath) throws return this; } - public KotlinFormatExtension setUseExperimental(boolean useExperimental) { - this.useExperimental = useExperimental; - replaceStep(createStep()); - return this; - } - public KotlinFormatExtension userData(Map userData) { // Copy the map to a sorted map because up-to-date checking is based on binary-equals of the serialized // representation. @@ -112,7 +104,6 @@ private FormatterStep createStep() { return KtLintStep.createForScript( version, provisioner(), - useExperimental, editorConfigPath, userData, editorConfigOverride); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java index 947b5bd728..10fbe5bd90 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java @@ -69,11 +69,11 @@ void withExperimentalEditorConfigOverride() throws IOException { "repositories { mavenCentral() }", "spotless {", " kotlin {", - " ktlint().setUseExperimental(true)", - " .editorConfigOverride([", - " ij_kotlin_allow_trailing_comma: true,", - " ij_kotlin_allow_trailing_comma_on_call_site: true", - " ])", + " ktlint().editorConfigOverride([", + " ktlint_experimental: \"enabled\",", + " ij_kotlin_allow_trailing_comma: true,", + " ij_kotlin_allow_trailing_comma_on_call_site: true", + " ])", " }", "}"); setFile("src/main/kotlin/Main.kt").toResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinGradleExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinGradleExtensionTest.java index 13a7cd8472..7dade5f2ff 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinGradleExtensionTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinGradleExtensionTest.java @@ -51,11 +51,11 @@ void withExperimentalEditorConfigOverride() throws IOException { "repositories { mavenCentral() }", "spotless {", " kotlinGradle {", - " ktlint().setUseExperimental(true)", - " .editorConfigOverride([", - " ij_kotlin_allow_trailing_comma: true,", - " ij_kotlin_allow_trailing_comma_on_call_site: true", - " ])", + " ktlint().editorConfigOverride([", + " ktlint_experimental: \"enabled\",", + " ij_kotlin_allow_trailing_comma: true,", + " ij_kotlin_allow_trailing_comma_on_call_site: true", + " ])", " }", "}"); setFile("configuration.gradle.kts").toResource("kotlin/ktlint/experimentalEditorConfigOverride.dirty"); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeIfContentContainsTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeIfContentContainsTest.java new file mode 100644 index 0000000000..f1a2cf4298 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TargetExcludeIfContentContainsTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020-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.gradle.spotless; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +class TargetExcludeIfContentContainsTest extends GradleIntegrationHarness { + @Test + void targetExcludeIfContentContainsWithOneValue() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExcludeIfContentContains '// Generated by Mr. Roboto'", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + String content = "// Generated by Mr. Roboto, do not edit.\n" + + "A B C\n" + + "D E F\n" + + "G H I"; + setFile("test_generated.md").toContent(content); + setFile("test_manual.md").toLines( + "A B C", + "D E F", + "G H I"); + gradleRunner().withArguments("spotlessApply").build(); + // `test_generated` contains the excluding text so didn't change. + assertFile("test_generated.md").hasContent(content); + // `test_manual` does not so it changed. + assertFile("test_manual.md").hasLines( + "a b c", + "d e f", + "g h i"); + } + + @Test + void targetExcludeIfContentContainsWithMultipleSteps() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExcludeIfContentContains '// Generated by Mr. Roboto'", + " custom 'lowercase', { str -> str.toLowerCase() }", + " licenseHeader('" + "// My CopyRights header" + "', '--')", + " }", + "}"); + String generatedContent = "// Generated by Mr. Roboto, do not edit.\n" + + "--\n" + + "public final class MyMessage {}\n"; + setFile("test_generated.md").toContent(generatedContent); + String manualContent = "// Typo in License\n" + + "--\n" + + "public final class MyMessage {\n" + + "}"; + setFile("test_manual.md").toContent(manualContent); + gradleRunner().withArguments("spotlessApply").build(); + + // `test_generated` contains the excluding text so didn't change, including the header. + assertFile("test_generated.md").hasContent(generatedContent); + // `test_manual` does not, it changed. + assertFile("test_manual.md").hasContent( + "// My CopyRights header\n" + + "--\n" + + "public final class mymessage {\n" + + "}"); + } + + @Test + void targetExcludeIfContentContainsRegex() throws IOException { + setFile("build.gradle").toLines( + "plugins { id 'com.diffplug.spotless' }", + "spotless {", + " format 'toLower', {", + " target '**/*.md'", + " targetExcludeIfContentContainsRegex '// Generated by Mr. Roboto|// Generated by Mrs. Call'", + " custom 'lowercase', { str -> str.toLowerCase() }", + " }", + "}"); + String robotoContent = "A B C\n" + + "// Generated by Mr. Roboto, do not edit.\n" + + "D E F\n" + + "G H I"; + setFile("test_generated_roboto.md").toContent(robotoContent); + String callContent = "A B C\n" + + "D E F\n" + + "// Generated by Mrs. Call, do not edit.\n" + + "G H I"; + setFile("test_generated_call.md").toContent(callContent); + String collaborationContent = "A B C\n" + + "// Generated by Mr. Roboto, do not edit.\n" + + "D E F\n" + + "// Generated by Mrs. Call, do not edit.\n" + + "G H I"; + setFile("test_generated_collaboration.md").toContent(collaborationContent); + String intruderContent = "A B C\n" + + "// Generated by K2000, do not edit.\n" + + "D E F\n" + + "G H I"; + setFile("test_generated_intruder.md").toContent(intruderContent); + setFile("test_manual.md").toLines( + "A B C", + "D E F", + "G H I"); + gradleRunner().withArguments("spotlessApply").build(); + // Part of the excluding values so has not changed. + assertFile("test_generated_roboto.md").hasContent(robotoContent); + // Part of the excluding values so has not changed. + assertFile("test_generated_call.md").hasContent(callContent); + // Part of the excluding values so has not changed. + assertFile("test_generated_collaboration.md").hasContent(collaborationContent); + // Not part of the excluding values so has changed. + assertFile("test_generated_intruder.md").hasContent( + "a b c\n" + + "// generated by k2000, do not edit.\n" + + "d e f\n" + + "g h i"); + // `test_manual` does not, it changed. + assertFile("test_manual.md").hasLines( + "a b c", + "d e f", + "g h i"); + } +} diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index c19cc804af..db7fff9851 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -5,6 +5,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ## [Unreleased] ### Added * Support pass skip (`-Dspotless.skip=true`) from command-line. ([#1729](https://github.com/diffplug/spotless/pull/1729)) +* Bump default `ktlint` version to latest `0.49.1` -> `0.50.0`.([#1741](https://github.com/diffplug/spotless/issues/1741)) + * Dropped support for `ktlint 0.47.x` following our policy of supporting two breaking changes at a time. ### Fixed * Update documented default `semanticSort` to `false`. ([#1728](https://github.com/diffplug/spotless/pull/1728)) diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java index 2825980733..fd59d8ba9d 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Ktlint.java @@ -48,6 +48,6 @@ public FormatterStep newFormatterStep(final FormatterStepConfig stepConfig) { editorConfigOverride = new HashMap<>(); } - return KtLintStep.create(ktlintVersion, stepConfig.getProvisioner(), false, false, configPath, Collections.emptyMap(), editorConfigOverride); + return KtLintStep.create(ktlintVersion, stepConfig.getProvisioner(), false, configPath, Collections.emptyMap(), editorConfigOverride); } } diff --git a/testlib/src/main/resources/kotlin/ktlint/basic-old.clean b/testlib/src/main/resources/kotlin/ktlint/basic-old.clean new file mode 100644 index 0000000000..fd5371bed2 --- /dev/null +++ b/testlib/src/main/resources/kotlin/ktlint/basic-old.clean @@ -0,0 +1,5 @@ +fun main() { + fun name() { a(); return b } + println(";") + println() +} diff --git a/testlib/src/main/resources/kotlin/ktlint/basic.clean b/testlib/src/main/resources/kotlin/ktlint/basic.clean index fd5371bed2..b727a7d016 100644 --- a/testlib/src/main/resources/kotlin/ktlint/basic.clean +++ b/testlib/src/main/resources/kotlin/ktlint/basic.clean @@ -1,5 +1,8 @@ fun main() { - fun name() { a(); return b } + fun name() { + a() + return b + } println(";") println() } diff --git a/testlib/src/test/java/com/diffplug/spotless/kotlin/KtLintStepTest.java b/testlib/src/test/java/com/diffplug/spotless/kotlin/KtLintStepTest.java index 9af6685906..5b62eeed48 100644 --- a/testlib/src/test/java/com/diffplug/spotless/kotlin/KtLintStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/kotlin/KtLintStepTest.java @@ -25,59 +25,48 @@ class KtLintStepTest extends ResourceHarness { @Test - void behavior() { - FormatterStep step = KtLintStep.create(TestProvisioner.mavenCentral()); - StepHarnessWithFile.forStep(this, step) - .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") - .testResourceExceptionMsg("kotlin/ktlint/unsolvable.dirty").isEqualTo( - "Error on line: 1, column: 1\n" + - "rule: standard:no-wildcard-imports\n" + - "Wildcard import"); - } - - @Test - void works0_47_0() { - FormatterStep step = KtLintStep.create("0.47.0", TestProvisioner.mavenCentral()); + void works0_48_0() { + FormatterStep step = KtLintStep.create("0.48.0", TestProvisioner.mavenCentral()); StepHarnessWithFile.forStep(this, step) - .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") + .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic-old.clean") .testResourceExceptionMsg("kotlin/ktlint/unsolvable.dirty").isEqualTo("Error on line: 1, column: 1\n" + "rule: no-wildcard-imports\n" + "Wildcard import"); } @Test - void works0_47_1() { - FormatterStep step = KtLintStep.create("0.47.1", TestProvisioner.mavenCentral()); + void works0_48_1() { + FormatterStep step = KtLintStep.create("0.48.1", TestProvisioner.mavenCentral()); StepHarnessWithFile.forStep(this, step) - .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") + .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic-old.clean") .testResourceExceptionMsg("kotlin/ktlint/unsolvable.dirty").isEqualTo("Error on line: 1, column: 1\n" + "rule: no-wildcard-imports\n" + "Wildcard import"); } @Test - void works0_48_0() { - FormatterStep step = KtLintStep.create("0.48.0", TestProvisioner.mavenCentral()); + void works0_49_0() { + FormatterStep step = KtLintStep.create("0.49.0", TestProvisioner.mavenCentral()); StepHarnessWithFile.forStep(this, step) - .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") + .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic-old.clean") .testResourceExceptionMsg("kotlin/ktlint/unsolvable.dirty").isEqualTo("Error on line: 1, column: 1\n" + - "rule: no-wildcard-imports\n" + + "rule: standard:no-wildcard-imports\n" + "Wildcard import"); } @Test - void works0_48_1() { - FormatterStep step = KtLintStep.create("0.48.1", TestProvisioner.mavenCentral()); + void works0_49_1() { + FormatterStep step = KtLintStep.create("0.49.1", TestProvisioner.mavenCentral()); StepHarnessWithFile.forStep(this, step) - .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") + .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic-old.clean") .testResourceExceptionMsg("kotlin/ktlint/unsolvable.dirty").isEqualTo("Error on line: 1, column: 1\n" + - "rule: no-wildcard-imports\n" + + "rule: standard:no-wildcard-imports\n" + "Wildcard import"); } @Test - void works0_49_0() { - FormatterStep step = KtLintStep.create("0.49.0", TestProvisioner.mavenCentral()); + void works0_50_0() { + FormatterStep step = KtLintStep.create("0.50.0", TestProvisioner.mavenCentral()); StepHarnessWithFile.forStep(this, step) .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") .testResourceExceptionMsg("kotlin/ktlint/unsolvable.dirty").isEqualTo("Error on line: 1, column: 1\n" + @@ -86,8 +75,8 @@ void works0_49_0() { } @Test - void works0_49_1() { - FormatterStep step = KtLintStep.create("0.49.1", TestProvisioner.mavenCentral()); + void behavior() { + FormatterStep step = KtLintStep.create(TestProvisioner.mavenCentral()); StepHarnessWithFile.forStep(this, step) .testResource("kotlin/ktlint/basic.dirty", "kotlin/ktlint/basic.clean") .testResourceExceptionMsg("kotlin/ktlint/unsolvable.dirty").isEqualTo("Error on line: 1, column: 1\n" +