Skip to content

Commit

Permalink
Diktat is a strict coding standard for Kotlin and a custom set of rul…
Browse files Browse the repository at this point in the history
…es for detecting code smells, code style issues and bugs.

https://github.com/cqfn/diKTat/blob/master/README.md
  • Loading branch information
Cheshiriks committed Feb 4, 2021
1 parent cd84982 commit e5c1efb
Show file tree
Hide file tree
Showing 19 changed files with 864 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}} |',
Expand Down Expand Up @@ -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: |
Expand Down
182 changes: 182 additions & 0 deletions lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* 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, String configPath) {
return create(versionDiktat, provisioner, Collections.emptyMap(), configPath);
}

public static FormatterStep create(String versionDiktat, Provisioner provisioner, Map<String, String> userData, String configPath) {
return create(versionDiktat, provisioner, false, userData, configPath);
}

public static FormatterStep createForScript(String versionDiktat, Provisioner provisioner, Map<String, String> userData, String configPath) {
return create(versionDiktat, provisioner, true, userData, configPath);
}

public static FormatterStep create(String versionDiktat, Provisioner provisioner, boolean isScript, Map<String, String> userData, String configPath) {
Objects.requireNonNull(versionDiktat, "versionDiktat");
Objects.requireNonNull(provisioner, "provisioner");
return FormatterStep.createLazy(NAME,
() -> new DiktatStep.State(versionDiktat, provisioner, isScript, userData, configPath),
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 String configPath;
private final String pkg;
private final String pkgKtlint;
final JarState jar;
private final TreeMap<String, String> userData;

State(String versionDiktat, Provisioner provisioner, boolean isScript, Map<String, String> userData, String configPath) throws IOException {

HashSet<String> 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.configPath = configPath;
}

FormatterFunc createFormat() throws Exception {

ClassLoader classLoader = jar.getClassLoader();

// first, we get the diktat rules
if (configPath != null) {
System.setProperty("diktat.config.path", configPath);
}

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<Object> 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;
}

}

}
13 changes: 13 additions & 0 deletions plugin-gradle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,19 @@ spotless {
ktfmt('0.15').dropboxStyle() // version and dropbox style are optional
```

<a name="applying-diktat-to-kotlin-files"></a>

### 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")
```

<a name="applying-scalafmt-to-scala-files"></a>

## Scala
Expand Down
1 change: 1 addition & 0 deletions plugin-gradle/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ if (version.endsWith('-SNAPSHOT')) {
'eclipse',
'ktlint',
'ktfmt',
'diktat',
'tsfmt',
'prettier',
'scalafmt',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.gradle.api.tasks.SourceSet;

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;
Expand Down Expand Up @@ -122,6 +123,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 String configPath;

DiktatFormatExtension(String version, String configPath) {
this.version = version;
this.configPath = configPath;
addStep(createStep());
}

public void withConfig(String path) {
// Specify the path to the configuration file
this.configPath = path;
replaceStep(createStep());
}

private FormatterStep createStep() {
return DiktatStep.create(version, provisioner(), configPath);
}
}

/** 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,6 +23,7 @@

import com.diffplug.common.collect.ImmutableSortedMap;
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;
Expand Down Expand Up @@ -104,6 +105,47 @@ 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, Collections.emptyMap(), null);
}

public DiktatFormatExtension diktat() {
return diktat(DiktatStep.defaultVersionDiktat());
}

public class DiktatFormatExtension {

private final String version;
private String configPath;
private Map<String, String> userData;

DiktatFormatExtension(String version, Map<String, String> config, String configPath) {
this.version = version;
this.userData = config;
this.configPath = configPath;
addStep(createStep());
}

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

public void withConfig(String path) {
// Specify the path to the configuration file
this.configPath = path;
replaceStep(createStep());
}

private FormatterStep createStep() {
return DiktatStep.createForScript(version, provisioner(), userData, configPath);
}
}

@Override
protected void setupTask(SpotlessTask task) {
if (target == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit e5c1efb

Please sign in to comment.