Skip to content

Commit 76de19b

Browse files
committed
Diktat is a strict coding standard for Kotlin and a custom set of rules for detecting code smells, code style issues and bugs.
https://github.com/cqfn/diKTat/blob/master/README.md
1 parent cd84982 commit 76de19b

File tree

19 files changed

+851
-0
lines changed

19 files changed

+851
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}}
6060
extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
6161
lib('kotlin.KtLintStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |',
6262
lib('kotlin.KtfmtStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
63+
lib('kotlin.DiktatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
6364
lib('markdown.FreshMarkStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |',
6465
lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
6566
lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |',
@@ -95,6 +96,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}}
9596
| [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
9697
| [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :+1: | :white_large_square: |
9798
| [`kotlin.KtfmtStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtfmtStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
99+
| [`kotlin.DiktatStep`](lib/src/main/java/com/diffplug/spotless/kotlin/DiktatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
98100
| [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: |
99101
| [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
100102
| [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: |
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright 2021 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.diffplug.spotless.kotlin;
18+
19+
import com.diffplug.spotless.*;
20+
21+
import java.io.IOException;
22+
import java.io.Serializable;
23+
import java.lang.reflect.Constructor;
24+
import java.lang.reflect.InvocationTargetException;
25+
import java.lang.reflect.Method;
26+
import java.lang.reflect.Proxy;
27+
import java.util.*;
28+
29+
/** Wraps up [diktat](https://github.com/cqfn/diKTat) as a FormatterStep. */
30+
public class DiktatStep {
31+
32+
// prevent direct instantiation
33+
private DiktatStep() {}
34+
35+
private static final String DEFAULT_VERSION = "0.4.0";
36+
static final String NAME = "diktat";
37+
static final String PACKAGE_DIKTAT = "org.cqfn.diktat";
38+
static final String PACKAGE_KTLINT = "com.pinterest.ktlint";
39+
static final String MAVEN_COORDINATE = PACKAGE_DIKTAT + ":diktat-rules:";
40+
41+
public static String defaultVersionDiktat() {
42+
return DEFAULT_VERSION;
43+
}
44+
45+
public static FormatterStep create(Provisioner provisioner) {
46+
return create(defaultVersionDiktat(), provisioner);
47+
}
48+
49+
public static FormatterStep create(String versionDiktat, Provisioner provisioner) {
50+
return create(versionDiktat, provisioner, Collections.emptyMap(), null);
51+
}
52+
53+
public static FormatterStep create(String versionDiktat, Provisioner provisioner, String configPath) {
54+
return create(versionDiktat, provisioner, Collections.emptyMap(), configPath);
55+
}
56+
57+
public static FormatterStep create(String versionDiktat, Provisioner provisioner, Map<String, String> userData, String configPath) {
58+
return create(versionDiktat, provisioner, false, userData, configPath);
59+
}
60+
61+
public static FormatterStep createForScript(String versionDiktat, Provisioner provisioner, Map<String, String> userData, String configPath) {
62+
return create(versionDiktat, provisioner, true, userData, configPath);
63+
}
64+
65+
public static FormatterStep create(String versionDiktat, Provisioner provisioner, boolean isScript, Map<String, String> userData, String configPath) {
66+
Objects.requireNonNull(versionDiktat, "versionDiktat");
67+
Objects.requireNonNull(provisioner, "provisioner");
68+
return FormatterStep.createLazy(NAME,
69+
() -> new DiktatStep.State(versionDiktat, provisioner, isScript, userData, configPath),
70+
DiktatStep.State::createFormat);
71+
}
72+
73+
static final class State implements Serializable {
74+
75+
private static final long serialVersionUID = 1L;
76+
77+
/** Are the files being linted Kotlin script files. */
78+
private final boolean isScript;
79+
private final String configPath;
80+
private final String pkg;
81+
private final String pkgKtlint;
82+
final JarState jar;
83+
private final TreeMap<String, String> userData;
84+
85+
State(String versionDiktat, Provisioner provisioner, boolean isScript, Map<String, String> userData, String configPath) throws IOException {
86+
87+
HashSet<String> pkgSet = new HashSet<>();
88+
pkgSet.add(MAVEN_COORDINATE + versionDiktat);
89+
90+
this.userData = new TreeMap<>(userData);
91+
this.pkg = PACKAGE_DIKTAT;
92+
this.pkgKtlint = PACKAGE_KTLINT;
93+
this.jar = JarState.from(pkgSet, provisioner);
94+
this.isScript = isScript;
95+
this.configPath = configPath;
96+
}
97+
98+
FormatterFunc createFormat() throws Exception {
99+
100+
ClassLoader classLoader = jar.getClassLoader();
101+
102+
// first, we get the diktat rules
103+
if (configPath != null) {
104+
System.setProperty("diktat.config.path", configPath);
105+
}
106+
107+
Class<?> ruleSetProviderClass = classLoader.loadClass(pkg + ".ruleset.rules.DiktatRuleSetProvider");
108+
Object diktatRuleSet = ruleSetProviderClass.getMethod("get").invoke(ruleSetProviderClass.newInstance());
109+
Iterable<?> ruleSets = Collections.singletonList(diktatRuleSet );
110+
111+
// next, we create an error callback which throws an assertion error when the format is bad
112+
Class<?> function2Interface = classLoader.loadClass("kotlin.jvm.functions.Function2");
113+
Class<?> lintErrorClass = classLoader.loadClass(pkgKtlint + ".core.LintError");
114+
Method detailGetter = lintErrorClass.getMethod("getDetail");
115+
Method lineGetter = lintErrorClass.getMethod("getLine");
116+
Method colGetter = lintErrorClass.getMethod("getCol");
117+
118+
// grab the KtLint singleton
119+
Class<?> ktlintClass = classLoader.loadClass(pkgKtlint + ".core.KtLint");
120+
Object ktlint = ktlintClass.getDeclaredField("INSTANCE").get(null);
121+
122+
Class<?> paramsClass = classLoader.loadClass(pkgKtlint + ".core.KtLint$Params");
123+
// and its constructor
124+
Constructor<?> constructor = paramsClass.getConstructor(
125+
/* fileName, nullable */ String.class,
126+
/* text */ String.class,
127+
/* ruleSets */ Iterable.class,
128+
/* userData */ Map.class,
129+
/* callback */ function2Interface,
130+
/* script */ boolean.class,
131+
/* editorConfigPath, nullable */ String.class,
132+
/* debug */ boolean.class);
133+
Method formatterMethod = ktlintClass.getMethod("format", paramsClass);
134+
FormatterFunc.NeedsFile formatterFunc = (input, file) -> {
135+
ArrayList<Object> errors = new ArrayList<>();
136+
137+
Object formatterCallback = Proxy.newProxyInstance(classLoader, new Class[]{function2Interface},
138+
(proxy, method, args) -> {
139+
Object lintError = args[0]; //ktlint.core.LintError
140+
boolean corrected = (Boolean) args[1];
141+
if (!corrected) {
142+
errors.add(lintError);
143+
}
144+
return null;
145+
});
146+
147+
userData.put("file_path", file.getAbsolutePath());
148+
try {
149+
Object params = constructor.newInstance(
150+
/* fileName, nullable */ file.getName(),
151+
/* text */ input,
152+
/* ruleSets */ ruleSets,
153+
/* userData */ userData,
154+
/* callback */ formatterCallback,
155+
/* script */ isScript,
156+
/* editorConfigPath, nullable */ null,
157+
/* debug */ false);
158+
String result = (String) formatterMethod.invoke(ktlint, params);
159+
if (!errors.isEmpty()) {
160+
StringBuilder error = new StringBuilder("");
161+
error.append("There are ").append(errors.size()).append(" unfixed errors:");
162+
for (Object er : errors) {
163+
String detail = (String) detailGetter.invoke(er);
164+
int line = (Integer) lineGetter.invoke(er);
165+
int col = (Integer) colGetter.invoke(er);
166+
167+
error.append(System.lineSeparator()).append("Error on line: ").append(line).append(", column: ").append(col).append(" cannot be fixed automatically")
168+
.append(System.lineSeparator()).append(detail);
169+
}
170+
throw new AssertionError(error);
171+
}
172+
return result;
173+
} catch (InvocationTargetException e) {
174+
throw ThrowingEx.unwrapCause(e);
175+
}
176+
};
177+
178+
return formatterFunc;
179+
}
180+
181+
}
182+
183+
}

plugin-gradle/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,19 @@ spotless {
288288
ktfmt('0.15').dropboxStyle() // version and dropbox style are optional
289289
```
290290

291+
<a name="applying-diktat-to-kotlin-files"></a>
292+
293+
### diktat
294+
295+
[homepage](https://github.com/cqfn/diKTat). [changelog](https://github.com/cqfn/diKTat/releases). You can provide configuration path manually as `configPath`.
296+
297+
```kotlin
298+
spotless {
299+
kotlin {
300+
// version and configPath are both optional
301+
diktat('0.4.0').configPath("full/path/to/diktat-analysis.yml")
302+
```
303+
291304
<a name="applying-scalafmt-to-scala-files"></a>
292305

293306
## Scala

plugin-gradle/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ if (version.endsWith('-SNAPSHOT')) {
6969
'eclipse',
7070
'ktlint',
7171
'ktfmt',
72+
'diktat',
7273
'tsfmt',
7374
'prettier',
7475
'scalafmt',

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinExtension.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
import javax.inject.Inject;
2525

26+
import com.diffplug.spotless.kotlin.DiktatStep;
27+
2628
import org.gradle.api.GradleException;
2729
import org.gradle.api.file.FileCollection;
2830
import org.gradle.api.plugins.JavaPluginConvention;
@@ -122,6 +124,38 @@ private FormatterStep createStep() {
122124
}
123125
}
124126

127+
/** Adds the specified version of [diktat](https://github.com/cqfn/diKTat). */
128+
public DiktatFormatExtension diktat(String version) {
129+
Objects.requireNonNull(version);
130+
return new DiktatFormatExtension(version, null);
131+
}
132+
133+
public DiktatFormatExtension diktat() {
134+
return diktat(DiktatStep.defaultVersionDiktat());
135+
}
136+
137+
public class DiktatFormatExtension {
138+
139+
private final String version;
140+
private String configPath;
141+
142+
DiktatFormatExtension(String version, String configPath) {
143+
this.version = version;
144+
this.configPath = configPath;
145+
addStep(createStep());
146+
}
147+
148+
public void withConfig(String path) {
149+
// Specify the path to the configuration file
150+
this.configPath = path;
151+
replaceStep(createStep());
152+
}
153+
154+
private FormatterStep createStep() {
155+
return DiktatStep.create(version, provisioner(), configPath);
156+
}
157+
}
158+
125159
/** If the user hasn't specified the files yet, we'll assume he/she means all of the kotlin files. */
126160
@Override
127161
protected void setupTask(SpotlessTask task) {

plugin-gradle/src/main/java/com/diffplug/gradle/spotless/KotlinGradleExtension.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import com.diffplug.common.collect.ImmutableSortedMap;
2525
import com.diffplug.spotless.FormatterStep;
26+
import com.diffplug.spotless.kotlin.DiktatStep;
2627
import com.diffplug.spotless.kotlin.KtLintStep;
2728
import com.diffplug.spotless.kotlin.KtfmtStep;
2829
import com.diffplug.spotless.kotlin.KtfmtStep.Style;
@@ -104,6 +105,47 @@ private FormatterStep createStep() {
104105
}
105106
}
106107

108+
/** Adds the specified version of [diktat](https://github.com/cqfn/diKTat). */
109+
public DiktatFormatExtension diktat(String version) {
110+
Objects.requireNonNull(version, "version");
111+
return new DiktatFormatExtension(version, Collections.emptyMap(), null);
112+
}
113+
114+
public DiktatFormatExtension diktat() {
115+
return diktat(DiktatStep.defaultVersionDiktat());
116+
}
117+
118+
public class DiktatFormatExtension {
119+
120+
private final String version;
121+
private String configPath;
122+
private Map<String, String> userData;
123+
124+
DiktatFormatExtension(String version, Map<String, String> config, String configPath) {
125+
this.version = version;
126+
this.userData = config;
127+
this.configPath = configPath;
128+
addStep(createStep());
129+
}
130+
131+
public void userData(Map<String, String> userData) {
132+
// Copy the map to a sorted map because up-to-date checking is based on binary-equals of the serialized
133+
// representation.
134+
this.userData = ImmutableSortedMap.copyOf(userData);
135+
replaceStep(createStep());
136+
}
137+
138+
public void withConfig(String path) {
139+
// Specify the path to the configuration file
140+
this.configPath = path;
141+
replaceStep(createStep());
142+
}
143+
144+
private FormatterStep createStep() {
145+
return DiktatStep.createForScript(version, provisioner(), userData, configPath);
146+
}
147+
}
148+
107149
@Override
108150
protected void setupTask(SpotlessTask task) {
109151
if (target == null) {

plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinExtensionTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ public void integration() throws IOException {
4646
assertFile("src/main/kotlin/basic.kt").sameAsResource("kotlin/ktlint/basic.clean");
4747
}
4848

49+
@Test
50+
public void integrationDiktat() throws IOException {
51+
setFile("build.gradle").toLines(
52+
"plugins {",
53+
" id 'org.jetbrains.kotlin.jvm' version '1.4.30'",
54+
" id 'com.diffplug.spotless'",
55+
"}",
56+
"repositories { mavenCentral() }",
57+
"spotless {",
58+
" kotlin {",
59+
" diktat()",
60+
" }",
61+
"}");
62+
setFile("src/main/kotlin/com/example/Main.kt").toResource("kotlin/diktat/main.dirty");
63+
gradleRunner().withArguments("spotlessApply").build();
64+
assertFile("src/main/kotlin/com/example/Main.kt").sameAsResource("kotlin/diktat/main.clean");
65+
}
66+
4967
@Test
5068
public void integrationKtfmt() throws IOException {
5169
// ktfmt's dependency, google-java-format 1.8 requires a minimum of JRE 11+.

plugin-gradle/src/test/java/com/diffplug/gradle/spotless/KotlinGradleExtensionTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,25 @@ public void integration_default() throws IOException {
7575
assertFile("configuration.gradle.kts").sameAsResource("kotlin/ktlint/basic.clean");
7676
}
7777

78+
@Test
79+
public void integration_default_diktat() throws IOException {
80+
setFile("build.gradle").toLines(
81+
"plugins {",
82+
" id 'org.jetbrains.kotlin.jvm' version '1.4.30'",
83+
" id 'com.diffplug.spotless'",
84+
"}",
85+
"repositories { mavenCentral() }",
86+
"spotless {",
87+
" kotlinGradle {",
88+
" diktat()",
89+
" }",
90+
"}");
91+
setFile("configuration.gradle.kts").toResource("kotlin/diktat/basic.dirty");
92+
BuildResult result = gradleRunner().withArguments("spotlessApply").buildAndFail();
93+
assertThat(result.getOutput()).contains("[HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE] files that contain multiple "
94+
+ "or no classes should contain description of what is inside of this file: there are 0 declared classes and/or objects");
95+
}
96+
7897
@Test
7998
public void integration_pinterest() throws IOException {
8099
setFile("build.gradle").toLines(

0 commit comments

Comments
 (0)