diff --git a/CHANGES.md b/CHANGES.md index 351e456eed..6216662fe0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* Support for diktat ([#789](https://github.com/diffplug/spotless/pull/789)) ## [2.11.0] - 2021-01-04 ### Added diff --git a/README.md b/README.md index bac3e7458e..06ecd36171 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('kotlin.KtLintStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', +lib('kotlin.DiktatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('markdown.FreshMarkStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |', lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', @@ -95,6 +96,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`kotlin.KtfmtStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | +| [`kotlin.DiktatStep`](lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | | [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | diff --git a/lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java b/lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java new file mode 100644 index 0000000000..ce3f2f7454 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java @@ -0,0 +1,186 @@ +/* + * Copyright 2021 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.kotlin; + +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.*; + +import com.diffplug.spotless.*; + +/** Wraps up [diktat](https://github.com/cqfn/diKTat) as a FormatterStep. */ +public class DiktatStep { + + // prevent direct instantiation + private DiktatStep() {} + + private static final String DEFAULT_VERSION = "0.4.0"; + static final String NAME = "diktat"; + static final String PACKAGE_DIKTAT = "org.cqfn.diktat"; + static final String PACKAGE_KTLINT = "com.pinterest.ktlint"; + static final String MAVEN_COORDINATE = PACKAGE_DIKTAT + ":diktat-rules:"; + + public static String defaultVersionDiktat() { + return DEFAULT_VERSION; + } + + public static FormatterStep create(Provisioner provisioner) { + return create(defaultVersionDiktat(), provisioner); + } + + public static FormatterStep create(String versionDiktat, Provisioner provisioner) { + return create(versionDiktat, provisioner, Collections.emptyMap(), null); + } + + public static FormatterStep create(String versionDiktat, Provisioner provisioner, FileSignature config) { + return create(versionDiktat, provisioner, Collections.emptyMap(), config); + } + + public static FormatterStep create(String versionDiktat, Provisioner provisioner, Map userData, FileSignature config) { + return create(versionDiktat, provisioner, false, userData, config); + } + + public static FormatterStep createForScript(String versionDiktat, Provisioner provisioner, FileSignature config) { + return createForScript(versionDiktat, provisioner, Collections.emptyMap(), config); + } + + public static FormatterStep createForScript(String versionDiktat, Provisioner provisioner, Map userData, FileSignature config) { + return create(versionDiktat, provisioner, true, userData, config); + } + + public static FormatterStep create(String versionDiktat, Provisioner provisioner, boolean isScript, Map userData, FileSignature config) { + Objects.requireNonNull(versionDiktat, "versionDiktat"); + Objects.requireNonNull(provisioner, "provisioner"); + return FormatterStep.createLazy(NAME, + () -> new DiktatStep.State(versionDiktat, provisioner, isScript, userData, config), + DiktatStep.State::createFormat); + } + + static final class State implements Serializable { + + private static final long serialVersionUID = 1L; + + /** Are the files being linted Kotlin script files. */ + private final boolean isScript; + private final FileSignature config; + private final String pkg; + private final String pkgKtlint; + final JarState jar; + private final TreeMap userData; + + State(String versionDiktat, Provisioner provisioner, boolean isScript, Map userData, FileSignature config) throws IOException { + + HashSet pkgSet = new HashSet<>(); + pkgSet.add(MAVEN_COORDINATE + versionDiktat); + + this.userData = new TreeMap<>(userData); + this.pkg = PACKAGE_DIKTAT; + this.pkgKtlint = PACKAGE_KTLINT; + this.jar = JarState.from(pkgSet, provisioner); + this.isScript = isScript; + this.config = config; + } + + FormatterFunc createFormat() throws Exception { + + ClassLoader classLoader = jar.getClassLoader(); + + // first, we get the diktat rules + if (config != null) { + System.setProperty("diktat.config.path", config.getOnlyFile().getAbsolutePath()); + } + + Class ruleSetProviderClass = classLoader.loadClass(pkg + ".ruleset.rules.DiktatRuleSetProvider"); + Object diktatRuleSet = ruleSetProviderClass.getMethod("get").invoke(ruleSetProviderClass.newInstance()); + Iterable ruleSets = Collections.singletonList(diktatRuleSet); + + // next, we create an error callback which throws an assertion error when the format is bad + Class function2Interface = classLoader.loadClass("kotlin.jvm.functions.Function2"); + Class lintErrorClass = classLoader.loadClass(pkgKtlint + ".core.LintError"); + Method detailGetter = lintErrorClass.getMethod("getDetail"); + Method lineGetter = lintErrorClass.getMethod("getLine"); + Method colGetter = lintErrorClass.getMethod("getCol"); + + // grab the KtLint singleton + Class ktlintClass = classLoader.loadClass(pkgKtlint + ".core.KtLint"); + Object ktlint = ktlintClass.getDeclaredField("INSTANCE").get(null); + + Class paramsClass = classLoader.loadClass(pkgKtlint + ".core.KtLint$Params"); + // and its constructor + Constructor constructor = paramsClass.getConstructor( + /* fileName, nullable */ String.class, + /* text */ String.class, + /* ruleSets */ Iterable.class, + /* userData */ Map.class, + /* callback */ function2Interface, + /* script */ boolean.class, + /* editorConfigPath, nullable */ String.class, + /* debug */ boolean.class); + Method formatterMethod = ktlintClass.getMethod("format", paramsClass); + FormatterFunc.NeedsFile formatterFunc = (input, file) -> { + ArrayList errors = new ArrayList<>(); + + Object formatterCallback = Proxy.newProxyInstance(classLoader, new Class[]{function2Interface}, + (proxy, method, args) -> { + Object lintError = args[0]; //ktlint.core.LintError + boolean corrected = (Boolean) args[1]; + if (!corrected) { + errors.add(lintError); + } + return null; + }); + + userData.put("file_path", file.getAbsolutePath()); + try { + Object params = constructor.newInstance( + /* fileName, nullable */ file.getName(), + /* text */ input, + /* ruleSets */ ruleSets, + /* userData */ userData, + /* callback */ formatterCallback, + /* script */ isScript, + /* editorConfigPath, nullable */ null, + /* debug */ false); + String result = (String) formatterMethod.invoke(ktlint, params); + if (!errors.isEmpty()) { + StringBuilder error = new StringBuilder(""); + error.append("There are ").append(errors.size()).append(" unfixed errors:"); + for (Object er : errors) { + String detail = (String) detailGetter.invoke(er); + int line = (Integer) lineGetter.invoke(er); + int col = (Integer) colGetter.invoke(er); + + error.append(System.lineSeparator()).append("Error on line: ").append(line).append(", column: ").append(col).append(" cannot be fixed automatically") + .append(System.lineSeparator()).append(detail); + } + throw new AssertionError(error); + } + return result; + } catch (InvocationTargetException e) { + throw ThrowingEx.unwrapCause(e); + } + }; + + return formatterFunc; + } + + } + +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index b67cead5c5..273006877d 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -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 for diktat in KotlinGradleExtension ([#789](https://github.com/diffplug/spotless/pull/789)) ## [5.9.0] - 2021-01-04 ### Added diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 6e6ac62154..7f7b070438 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -60,7 +60,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - **Languages** - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - - [Kotlin](#kotlin) ([ktlint](#ktlint), [ktfmt](#ktfmt), [prettier](#prettier)) + - [Kotlin](#kotlin) ([ktlint](#ktlint), [ktfmt](#ktfmt), [prettier](#prettier), [diktat](#diktat)) - [Scala](#scala) ([scalafmt](#scalafmt)) - [C/C++](#cc) ([clang-format](#clang-format), [eclipse cdt](#eclipse-cdt)) - [Python](#python) ([black](#black)) @@ -288,6 +288,19 @@ spotless { ktfmt('0.15').dropboxStyle() // version and dropbox style are optional ``` + + +### diktat + +[homepage](https://github.com/cqfn/diKTat). [changelog](https://github.com/cqfn/diKTat/releases). You can provide configuration path manually as `configPath`. + +```kotlin +spotless { + kotlin { + // version and configPath are both optional + diktat('0.4.0').configPath("full/path/to/diktat-analysis.yml") +``` + ## Scala diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle index f24f2cd233..296379f106 100644 --- a/plugin-gradle/build.gradle +++ b/plugin-gradle/build.gradle @@ -69,6 +69,7 @@ if (version.endsWith('-SNAPSHOT')) { 'eclipse', 'ktlint', 'ktfmt', + 'diktat', 'tsfmt', 'prettier', 'scalafmt', 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 0c1f0c44c8..a929bd3e32 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 @@ -15,8 +15,11 @@ */ package com.diffplug.gradle.spotless; +import static com.diffplug.spotless.FileSignature.signAsList; import static com.diffplug.spotless.kotlin.KotlinConstants.LICENSE_HEADER_DELIMITER; +import java.io.File; +import java.io.IOException; import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -28,7 +31,9 @@ import org.gradle.api.plugins.JavaPluginConvention; import org.gradle.api.tasks.SourceSet; +import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.kotlin.DiktatStep; import com.diffplug.spotless.kotlin.KtLintStep; import com.diffplug.spotless.kotlin.KtfmtStep; import com.diffplug.spotless.kotlin.KtfmtStep.Style; @@ -122,6 +127,38 @@ private FormatterStep createStep() { } } + /** Adds the specified version of [diktat](https://github.com/cqfn/diKTat). */ + public DiktatFormatExtension diktat(String version) { + Objects.requireNonNull(version); + return new DiktatFormatExtension(version, null); + } + + public DiktatFormatExtension diktat() { + return diktat(DiktatStep.defaultVersionDiktat()); + } + + public class DiktatFormatExtension { + + private final String version; + private FileSignature config; + + DiktatFormatExtension(String version, FileSignature config) { + this.version = version; + this.config = config; + addStep(createStep()); + } + + public void withConfig(String path) throws IOException { + // Specify the path to the configuration file + this.config = signAsList(new File(path)); + replaceStep(createStep()); + } + + private FormatterStep createStep() { + return DiktatStep.create(version, provisioner(), config); + } + } + /** If the user hasn't specified the files yet, we'll assume he/she means all of the kotlin files. */ @Override protected void setupTask(SpotlessTask task) { 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 fdc2f25be8..c58b2e5187 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,10 @@ */ package com.diffplug.gradle.spotless; +import static com.diffplug.spotless.FileSignature.signAsList; + +import java.io.File; +import java.io.IOException; import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -22,7 +26,9 @@ import javax.inject.Inject; import com.diffplug.common.collect.ImmutableSortedMap; +import com.diffplug.spotless.FileSignature; import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.kotlin.DiktatStep; import com.diffplug.spotless.kotlin.KtLintStep; import com.diffplug.spotless.kotlin.KtfmtStep; import com.diffplug.spotless.kotlin.KtfmtStep.Style; @@ -104,6 +110,38 @@ private FormatterStep createStep() { } } + /** Adds the specified version of [diktat](https://github.com/cqfn/diKTat). */ + public DiktatFormatExtension diktat(String version) { + Objects.requireNonNull(version, "version"); + return new DiktatFormatExtension(version, null); + } + + public DiktatFormatExtension diktat() { + return diktat(DiktatStep.defaultVersionDiktat()); + } + + public class DiktatFormatExtension { + + private final String version; + private FileSignature config; + + DiktatFormatExtension(String version, FileSignature config) { + this.version = version; + this.config = config; + addStep(createStep()); + } + + public void withConfig(String path) throws IOException { + // Specify the path to the configuration file + this.config = signAsList(new File(path)); + replaceStep(createStep()); + } + + private FormatterStep createStep() { + return DiktatStep.createForScript(version, provisioner(), config); + } + } + @Override protected void setupTask(SpotlessTask task) { if (target == null) { 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 dcadfaf819..672486fedb 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 @@ -46,6 +46,24 @@ public void integration() throws IOException { assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktlint/basic.clean"); } + @Test + public void integrationDiktat() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'org.jetbrains.kotlin.jvm' version '1.4.30'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " kotlin {", + " diktat()", + " }", + "}"); + setFile("src/main/kotlin/com/example/Main.kt").toResource("kotlin/diktat/main.dirty"); + gradleRunner().withArguments("spotlessApply").build(); + assertFile("src/main/kotlin/com/example/Main.kt").sameAsResource("kotlin/diktat/main.clean"); + } + @Test public void integrationKtfmt() throws IOException { // ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+. 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 d71e4d2773..6678988fd5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,6 +75,25 @@ public void integration_default() throws IOException { assertFile("configuration.gradle.kts").sameAsResource("kotlin/ktlint/basic.clean"); } + @Test + public void integration_default_diktat() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'org.jetbrains.kotlin.jvm' version '1.4.30'", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " kotlinGradle {", + " diktat()", + " }", + "}"); + setFile("configuration.gradle.kts").toResource("kotlin/diktat/basic.dirty"); + BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail(); + assertThat(result.getOutput()).contains("[HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple " + + "or no classes should contain description of what is inside of this file: there are 0 declared classes and/or objects"); + } + @Test public void integration_pinterest() throws IOException { setFile("build.gradle").toLines( diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 915d5cfb91..afeed5f4cf 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* Support for diktat ([#789](https://github.com/diffplug/spotless/pull/789)) ## [2.7.0] - 2021-01-04 ### Added diff --git a/plugin-maven/README.md b/plugin-maven/README.md index e7ba13c876..784e6c6820 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -49,7 +49,7 @@ user@machine repo % mvn spotless:check - **Languages** - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - - [Kotlin](#kotlin) ([ktlint](#ktlint), [ktfmt](#ktfmt), [prettier](#prettier)) + - [Kotlin](#kotlin) ([ktlint](#ktlint), [ktfmt](#ktfmt), [prettier](#prettier), [diktat](#diktat)) - [Scala](#scala) ([scalafmt](#scalafmt)) - [C/C++](#cc) ([eclipse cdt](#eclipse-cdt)) - [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter)) @@ -277,6 +277,7 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T + @@ -311,6 +312,19 @@ Groovy-Eclipse formatting errors/warnings lead per default to a build failure. T ``` + + +### diktat + +[homepage](https://github.com/cqfn/diKTat). [changelog](https://github.com/cqfn/diKTat/releases). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Diktat.java). You can provide configuration path manually as `configPath`. + +```xml + + 0.4.0 + "full/path/to/diktat-analysis.yml" + +``` + ## Scala diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Diktat.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Diktat.java new file mode 100644 index 0000000000..77d624b350 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Diktat.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021 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.maven.kotlin; + +import static com.diffplug.spotless.FileSignature.signAsList; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.kotlin.DiktatStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class Diktat implements FormatterStepFactory { + + @Parameter + private String version; + + @Parameter + private String configPath; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + String diktatConfigPath = configPath != null ? configPath : null; + FileSignature configFile = getConfigFile(diktatConfigPath); + String diktatVersion = version != null ? version : DiktatStep.defaultVersionDiktat(); + return DiktatStep.create(diktatVersion, config.getProvisioner(), Collections.emptyMap(), configFile); + } + + private FileSignature getConfigFile(String path) { + if (path != null) { + try { + return signAsList(new File(path)); + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; + } + +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Kotlin.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Kotlin.java index dcfce10fc4..cfb0aad39e 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Kotlin.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/kotlin/Kotlin.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2021 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,4 +43,8 @@ public void addKtlint(Ktlint ktlint) { public void addKtfmt(Ktfmt ktfmt) { addStepFactory(ktfmt); } + + public void addDiktat(Diktat diktat) { + addStepFactory(diktat); + } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/DiktatTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/DiktatTest.java new file mode 100644 index 0000000000..0e3fed2ed9 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/kotlin/DiktatTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 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.maven.kotlin; + +import java.io.File; + +import org.junit.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +public class DiktatTest extends MavenIntegrationHarness { + + @Test + public void testDiktat() throws Exception { + + writePomWithKotlinSteps(""); + + String path = "src/main/kotlin/Main.kt"; + setFile(path).toResource("kotlin/diktat/main.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("kotlin/diktat/main.clean"); + + } + + @Test + public void testDiktatWithVersion() throws Exception { + + writePomWithKotlinSteps("0.4.0"); + + String path = "src/main/kotlin/Main.kt"; + setFile(path).toResource("kotlin/diktat/main.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("kotlin/diktat/main.clean"); + } + + @Test + public void testDiktatConfig() throws Exception { + + String configPath = "src/main/kotlin/diktat-analysis.yml"; + File conf = setFile(configPath).toResource("kotlin/diktat/diktat-analysis.yml"); + writePomWithKotlinSteps("0.4.0" + conf.getAbsolutePath() + ""); + + String path = "src/main/kotlin/Main.kt"; + setFile(path).toResource("kotlin/diktat/main.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("kotlin/diktat/main.clean"); + } + +} diff --git a/testlib/src/main/resources/kotlin/diktat/Unsolvable.kt b/testlib/src/main/resources/kotlin/diktat/Unsolvable.kt new file mode 100644 index 0000000000..88bd0b3f6d --- /dev/null +++ b/testlib/src/main/resources/kotlin/diktat/Unsolvable.kt @@ -0,0 +1,15 @@ +package org.cqfn.diktat.example.gradle.multiproject.resources.kotlin.diktat + +/** + * @return print. + */ +class Unsolvable { + /** + * foo a [member] to this group. + * + */ + fun foo() { + println(";") + println(); + } +} diff --git a/testlib/src/main/resources/kotlin/diktat/basic.clean b/testlib/src/main/resources/kotlin/diktat/basic.clean new file mode 100644 index 0000000000..309b40e252 --- /dev/null +++ b/testlib/src/main/resources/kotlin/diktat/basic.clean @@ -0,0 +1,13 @@ +/** + * @property main + / + +fun main() { + + /** + * @return print. + */ + fun name() { a(); return b } + println(";") + println() +} diff --git a/testlib/src/main/resources/kotlin/diktat/basic.dirty b/testlib/src/main/resources/kotlin/diktat/basic.dirty new file mode 100644 index 0000000000..ca4758f4f2 --- /dev/null +++ b/testlib/src/main/resources/kotlin/diktat/basic.dirty @@ -0,0 +1,13 @@ +/** + * @property main + */ + +fun main() { + + /** + * @property name + */ + fun name() { a(); return b } + println(";") + println(); +} diff --git a/testlib/src/main/resources/kotlin/diktat/diktat-analysis.yml b/testlib/src/main/resources/kotlin/diktat/diktat-analysis.yml new file mode 100644 index 0000000000..8c88553907 --- /dev/null +++ b/testlib/src/main/resources/kotlin/diktat/diktat-analysis.yml @@ -0,0 +1,298 @@ +- name: DIKTAT_COMMON + configuration: + # put your package name here - it will be autofixed and checked + domainName: org.cqfn.diktat.example.gradle.multiproject + testDirs: test +- name: CLASS_NAME_INCORRECT + enabled: true +- name: CONSTANT_UPPERCASE + enabled: true +- name: ENUM_VALUE + enabled: true +- name: EXCEPTION_SUFFIX + enabled: true +- name: FILE_NAME_INCORRECT + enabled: true +- name: FILE_NAME_MATCH_CLASS + enabled: true +- name: FUNCTION_BOOLEAN_PREFIX + enabled: true +- name: FUNCTION_NAME_INCORRECT_CASE + enabled: true +- name: GENERIC_NAME + enabled: true +- name: IDENTIFIER_LENGTH + enabled: true +- name: OBJECT_NAME_INCORRECT + enabled: true +- name: PACKAGE_NAME_INCORRECT_CASE + enabled: true # configuration domainName is taken from DIKTAT_COMMON +- name: PACKAGE_NAME_INCORRECT_PREFIX + enabled: false +- name: PACKAGE_NAME_INCORRECT_SYMBOLS + enabled: true +- name: PACKAGE_NAME_INCORRECT_PATH + enabled: true # configuration domainName is taken from DIKTAT_COMMON +- name: PACKAGE_NAME_MISSING + enabled: true +- name: VARIABLE_HAS_PREFIX + enabled: true +- name: VARIABLE_NAME_INCORRECT + enabled: true +- name: VARIABLE_NAME_INCORRECT_FORMAT + enabled: true +- name: MISSING_KDOC_ON_FUNCTION + enabled: true +- name: MISSING_KDOC_TOP_LEVEL + enabled: true +- name: MISSING_KDOC_CLASS_ELEMENTS + enabled: true +- name: KDOC_WITHOUT_PARAM_TAG + enabled: true +- name: KDOC_WITHOUT_RETURN_TAG + enabled: true +- name: KDOC_WITHOUT_THROWS_TAG + enabled: true +- name: KDOC_EMPTY_KDOC + enabled: true +- name: INCORRECT_PACKAGE_SEPARATOR + enabled: true +- name: KDOC_NO_DEPRECATED_TAG + enabled: true +- name: KDOC_NO_EMPTY_TAGS + enabled: true +- name: KDOC_WRONG_SPACES_AFTER_TAG + enabled: true +- name: KDOC_WRONG_TAGS_ORDER + enabled: true +- name: KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS + enabled: true +- name: KDOC_NEWLINES_BEFORE_BASIC_TAGS + enabled: true +- name: KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS + enabled: true +- name: KDOC_TRIVIAL_KDOC_ON_FUNCTION + enabled: 'true' +- name: HEADER_WRONG_FORMAT + enabled: true +- name: HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE + enabled: true +- name: HEADER_MISSING_OR_WRONG_COPYRIGHT + enabled: true + configuration: + isCopyrightMandatory: false + copyrightText: 'Copyright (c) Your Company Name Here. 2010-2021' +- name: HEADER_NOT_BEFORE_PACKAGE + enabled: true +- name: FILE_IS_TOO_LONG + enabled: true + configuration: + maxSize: '2000' + ignoreFolders: '' +- name: COMMENTED_OUT_CODE + enabled: true +- name: FILE_CONTAINS_ONLY_COMMENTS + enabled: true + # order imports alphabetically +- name: FILE_UNORDERED_IMPORTS + enabled: true + configuration: + # use logical imports grouping with sorting inside of a group + useRecommendedImportsOrder: true +- name: FILE_INCORRECT_BLOCKS_ORDER + enabled: true +- name: FILE_NO_BLANK_LINE_BETWEEN_BLOCKS + enabled: true +# Check: warns if wildcard imports are used except allows. (e.g. import org.cqfn.diktat.*) +- name: FILE_WILDCARD_IMPORTS + enabled: true + configuration: + allowedWildcards: "" # Allowed wildcards for imports (e.g. "import org.cqfn.diktat.*, import org.jetbrains.kotlin.*") + useRecommendedImportsOrder: true +- name: NO_BRACES_IN_CONDITIONALS_AND_LOOPS + enabled: true +- name: WRONG_ORDER_IN_CLASS_LIKE_STRUCTURES + enabled: true +- name: BLANK_LINE_BETWEEN_PROPERTIES + enabled: true +- name: BRACES_BLOCK_STRUCTURE_ERROR + enabled: true + configuration: + openBraceNewline: 'True' + closeBraceNewline: 'True' +- name: WRONG_INDENTATION + enabled: true + configuration: + newlineAtEnd: true + extendedIndentOfParameters: true + alignedParameters: true + extendedIndentAfterOperators: true + indentationSize: 4 +- name: EMPTY_BLOCK_STRUCTURE_ERROR + enabled: true + configuration: + styleEmptyBlockWithNewline: 'True' + allowEmptyBlocks: 'False' +- name: MORE_THAN_ONE_STATEMENT_PER_LINE + enabled: true +- name: LONG_LINE + enabled: true + configuration: + lineLength: '120' +- name: REDUNDANT_SEMICOLON + enabled: true +- name: WRONG_NEWLINES + enabled: true + configuration: + maxParametersInOneLine: 2 +- name: TOO_MANY_CONSECUTIVE_SPACES + enabled: true + configuration: + max_spaces: '1' + saveInitialFormattingForEnums: false +- name: TOO_MANY_BLANK_LINES + enabled: true +- name: WRONG_WHITESPACE + enabled: true +- name: BACKTICKS_PROHIBITED + enabled: true +- name: STRING_CONCATENATION + enabled: true +- name: WHEN_WITHOUT_ELSE + enabled: true +- name: ANNOTATION_NEW_LINE + enabled: true +- name: ENUMS_SEPARATED + enabled: true +- name: LONG_NUMERICAL_VALUES_SEPARATED + enabled: true + configuration: + maxNumberLength: '5' + maxBlockLength: '3' +- name: WRONG_DECLARATIONS_ORDER + enabled: true + configuration: + sortEnum: true + sortProperty: true +- name: WRONG_MULTIPLE_MODIFIERS_ORDER + enabled: true +- name: CONFUSING_IDENTIFIER_NAMING + enabled: true +# Inspection that checks if there is a blank line before kDoc and none after +- name: WRONG_NEWLINES_AROUND_KDOC + enabled: true +# Inspection that checks if there is no blank lines before first comment +- name: FIRST_COMMENT_NO_SPACES + enabled: true +# Inspection that checks if there are blank lines between code and comment and between code start token and comment's text +- name: COMMENT_WHITE_SPACE + enabled: true + configuration: + maxSpacesBeforeComment: 2 + maxSpacesInComment: 1 +# Inspection that checks if all comment's are inside if-else code blocks. Exception is general if comment +- name: IF_ELSE_COMMENTS + enabled: true +- name: WRONG_COPYRIGHT_YEAR + enabled: true +# Inspection that checks if local variables are declared close to the first usage site +- name: LOCAL_VARIABLE_EARLY_DECLARATION + enabled: true +# Try to avoid initialize val by null (e.g. val a: Int? = null -> val a: Int = 0) +- name: NULLABLE_PROPERTY_TYPE + enabled: true +# Type aliases provide alternative names for existing types when type's reference text is longer 25 chars +- name: TYPE_ALIAS + enabled: true + configuration: + typeReferenceLength: '25' # max length of type reference +- name: SMART_CAST_NEEDED + enabled: true +- name: GENERIC_VARIABLE_WRONG_DECLARATION + enabled: true +# Inspection that checks if string template has redundant curly braces +- name: STRING_TEMPLATE_CURLY_BRACES + enabled: true +# Variables with `val` modifier - are immutable (read-only). Usage of such variables instead of `var` variables increases +# robustness and readability of code, because `var` variables can be reassigned several times in the business logic. +# This rule prohibits usage of `var`s as local variables - the only exception is accumulators and counters +- name: SAY_NO_TO_VAR + enabled: true +# Inspection that checks if string template has redundant quotes +- name: STRING_TEMPLATE_QUOTES + enabled: true +# Checks that floating-point values are not used in arithmetic expressions +- name: FLOAT_IN_ACCURATE_CALCULATIONS + enabled: true +# Checks that function length isn't too long +- name: TOO_LONG_FUNCTION + enabled: true + configuration: + maxFunctionLength: '30' # max length of function + isIncludeHeader: 'false' # count function's header +# Warns if there are nested functions +- name: AVOID_NESTED_FUNCTIONS + enabled: true +# Checks that lambda inside function parameters is in the end +- name: LAMBDA_IS_NOT_LAST_PARAMETER + enabled: true +# Checks that function doesn't contains too many parameters +- name: TOO_MANY_PARAMETERS + enabled: true + configuration: + maxParameterListSize: '5' # max parameters size +# Checks that function doesn't have too many nested blocks +- name: NESTED_BLOCK + enabled: true + configuration: + maxNestedBlockQuantity: '4' +# Checks that function use default values, instead overloading +- name: WRONG_OVERLOADING_FUNCTION_ARGUMENTS + enabled: true +# Checks that KDoc in constructor has property tag +- name: KDOC_NO_CONSTRUCTOR_PROPERTY + enabled: true +# Checks that KDoc in constructor has property tag but with comment inside constructor +- name: KDOC_NO_CONSTRUCTOR_PROPERTY_WITH_COMMENT + enabled: true +# if a class has single constructor, it should be converted to a primary constructor +- name: SINGLE_CONSTRUCTOR_SHOULD_BE_PRIMARY + enabled: true +# Checks if class can be made as data class +- name: USE_DATA_CLASS + enabled: true +# Checks that never use the name of a variable in the custom getter or setter +- name: WRONG_NAME_OF_VARIABLE_INSIDE_ACCESSOR + enabled: true +# Checks that classes have only one init block +- name: MULTIPLE_INIT_BLOCKS + enabled: true +# Checks that there are abstract functions in abstract class +- name: CLASS_SHOULD_NOT_BE_ABSTRACT + enabled: true +# Checks if there are any trivial getters or setters +- name: TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED + enabled: true +# Checks that no custom getters and setters are used for properties. It is a more wide rule than TRIVIAL_ACCESSORS_ARE_NOT_RECOMMENDED +# Kotlin compiler automatically generates `get` and `set` methods for properties and also lets the possibility to override it. +# But in all cases it is very confusing when `get` and `set` are overridden for a developer who uses this particular class. +# Developer expects to get the value of the property, but receives some unknown value and some extra side effect hidden by the custom getter/setter. +# Use extra functions for it instead. +- name: CUSTOM_GETTERS_SETTERS + enabled: true +# Checks if null-check was used explicitly (for example: if (a == null)) +# Try to avoid explicit null checks (explicit comparison with `null`) +# Kotlin is declared as [Null-safe](https://kotlinlang.org/docs/reference/null-safety.html) language. +# But Kotlin architects wanted Kotlin to be fully compatible with Java, that's why `null` keyword was also introduced in Kotlin. +# There are several code-structures that can be used in Kotlin to avoid null-checks. For example: `?:`, `.let {}`, `.also {}`, e.t.c +- name: AVOID_NULL_CHECKS + enabled: true +# Checks if class instantiation can be wrapped in `apply` for better readability +- name: COMPACT_OBJECT_INITIALIZATION + enabled: true +# Checks explicit supertype qualification +- name: USELESS_SUPERTYPE + enabled: true +# Checks if extension function with the same signature don't have related classes +- name: EXTENSION_FUNCTION_SAME_SIGNATURE + enabled: true diff --git a/testlib/src/main/resources/kotlin/diktat/main.clean b/testlib/src/main/resources/kotlin/diktat/main.clean new file mode 100644 index 0000000000..4405165ed5 --- /dev/null +++ b/testlib/src/main/resources/kotlin/diktat/main.clean @@ -0,0 +1,15 @@ +package org.cqfn.diktat.example.gradle.multiproject + +/** + * @return print. + */ +class Main { + /** + * foo a [member] to this group. + * + */ + fun foo() { + println(";") + println() + } +} diff --git a/testlib/src/main/resources/kotlin/diktat/main.dirty b/testlib/src/main/resources/kotlin/diktat/main.dirty new file mode 100644 index 0000000000..5f7be993aa --- /dev/null +++ b/testlib/src/main/resources/kotlin/diktat/main.dirty @@ -0,0 +1,14 @@ +package org.cqfn.diktat.example.gradle.multiproject +/** + * @return print. + */ +class Main { + /** + * foo a [member] to this group. + * + */ + fun foo() { + println(";") + println(); + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/kotlin/DiktatStepTest.java b/testlib/src/test/java/com/diffplug/spotless/kotlin/DiktatStepTest.java new file mode 100644 index 0000000000..e201dabb66 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/kotlin/DiktatStepTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 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.kotlin; + +import static com.diffplug.spotless.FileSignature.signAsList; + +import java.io.File; +import java.util.Collections; + +import org.junit.Test; + +import com.diffplug.spotless.*; + +public class DiktatStepTest extends ResourceHarness { + + @Test + public void behavior() throws Exception { + FormatterStep step = DiktatStep.create(TestProvisioner.mavenCentral()); + StepHarness.forStep(step) + .testResourceException("kotlin/diktat/Unsolvable.kt", assertion -> { + assertion.isInstanceOf(AssertionError.class); + assertion.hasMessage("There are 2 unfixed errors:" + + System.lineSeparator() + "Error on line: 1, column: 1 cannot be fixed automatically" + + System.lineSeparator() + "[FILE_NAME_INCORRECT] file name is incorrect - it should end with .kt extension and be in PascalCase: testlib" + + System.lineSeparator() + "Error on line: 1, column: 1 cannot be fixed automatically" + + System.lineSeparator() + "[FILE_NAME_MATCH_CLASS] file name is incorrect - it should match with the class described in it if there is the only one class declared: testlib vs Unsolvable"); + }); + } + + @Test + public void behaviorConf() throws Exception { + + String configPath = "src/main/kotlin/diktat-analysis.yml"; + File conf = setFile(configPath).toResource("kotlin/diktat/diktat-analysis.yml"); + FileSignature config = signAsList(conf); + + FormatterStep step = DiktatStep.create("0.3.0", TestProvisioner.mavenCentral(), Collections.emptyMap(), config); + StepHarness.forStep(step) + .testResourceException("kotlin/diktat/Unsolvable.kt", assertion -> { + assertion.isInstanceOf(AssertionError.class); + assertion.hasMessage("There are 2 unfixed errors:" + + System.lineSeparator() + "Error on line: 1, column: 1 cannot be fixed automatically" + + System.lineSeparator() + "[FILE_NAME_INCORRECT] file name is incorrect - it should end with .kt extension and be in PascalCase: testlib" + + System.lineSeparator() + "Error on line: 1, column: 1 cannot be fixed automatically" + + System.lineSeparator() + "[FILE_NAME_MATCH_CLASS] file name is incorrect - it should match with the class described in it if there is the only one class declared: testlib vs Unsolvable"); + }); + } + +}