Repository: analysis-dev/diktat (kotlin)

Issue:
Add tests with .kts files
For now we focused exclusively on regular .kt files, but there also can be kts files, which have slightly different AST. E.g., AST of kts file can contain no `PACKAGE_DIRECTIVE` node, but `FileStructureRule` currently assumes that it will always be present.

Pull request description:
### What's done:
* Initial support

This pull request closes #266

Patch:
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/FileNaming.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/FileNaming.kt
index 8b6135d183..b1a4d5a58a 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/FileNaming.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/FileNaming.kt
@@ -7,6 +7,7 @@ import org.cqfn.diktat.ruleset.constants.Warnings.FILE_NAME_MATCH_CLASS
 import org.cqfn.diktat.ruleset.utils.getAllChildrenWithType
 import org.cqfn.diktat.ruleset.utils.getFilePath
 import org.cqfn.diktat.ruleset.utils.getFirstChildWithType
+import org.cqfn.diktat.ruleset.utils.isKotlinScript
 import org.cqfn.diktat.ruleset.utils.isPascalCase
 
 import com.pinterest.ktlint.core.Rule
@@ -39,8 +40,10 @@ class FileNaming(private val configRules: List<RulesConfig>) : Rule("file-naming
 
         if (node.elementType == FILE) {
             filePath = node.getFilePath()
-            checkFileNaming(node)
-            checkClassNameMatchesWithFile(node)
+            if (!filePath.isKotlinScript()) {
+                checkFileNaming(node)
+                checkClassNameMatchesWithFile(node)
+            }
         }
     }
 
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/PackageNaming.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/PackageNaming.kt
index 84888608bf..bd2499cc52 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/PackageNaming.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/PackageNaming.kt
@@ -60,7 +60,7 @@ class PackageNaming(private val configRules: List<RulesConfig>) : Rule("package-
             val realPackageName = calculateRealPackageName(filePath)
 
             // if node isLeaf - this means that there is no package name declared
-            if (node.isLeaf()) {
+            if (node.isLeaf() && !filePath.isKotlinScript()) {
                 warnAndFixMissingPackageName(node, realPackageName, filePath)
                 return
             }
@@ -129,7 +129,7 @@ class PackageNaming(private val configRules: List<RulesConfig>) : Rule("package-
             }
 
         // package name should start from a company's domain name
-        if (!isDomainMatches(wordsInPackageName)) {
+        if (wordsInPackageName.isNotEmpty() && !isDomainMatches(wordsInPackageName)) {
             PACKAGE_NAME_INCORRECT_PREFIX.warnAndFix(configRules, emitWarn, isFixMode, domainName,
                 wordsInPackageName[0].startOffset, wordsInPackageName[0]) {
                 val oldPackageName = wordsInPackageName.joinToString(PACKAGE_SEPARATOR) { it.text }
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/files/BlankLinesRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/files/BlankLinesRule.kt
index 38e5b92852..3486626c5e 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/files/BlankLinesRule.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/files/BlankLinesRule.kt
@@ -17,6 +17,7 @@ import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_LITERAL
 import com.pinterest.ktlint.core.ast.ElementType.LAMBDA_EXPRESSION
 import com.pinterest.ktlint.core.ast.ElementType.LBRACE
 import com.pinterest.ktlint.core.ast.ElementType.RBRACE
+import com.pinterest.ktlint.core.ast.ElementType.SCRIPT
 import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
 import org.jetbrains.kotlin.com.intellij.lang.ASTNode
 
@@ -46,7 +47,11 @@ class BlankLinesRule(private val configRules: List<RulesConfig>) : Rule("blank-l
     }
 
     private fun handleBlankLine(node: ASTNode) {
-        if (node.treeParent.elementType.let { it == BLOCK || it == CLASS_BODY || it == FUNCTION_LITERAL }) {
+        if (node.treeParent.let {
+            // kts files are parsed as a SCRIPT node containing BLOCK, therefore WHITE_SPACEs from these BLOCKS shouldn't be checked
+            it.elementType == BLOCK && it.treeParent?.elementType != SCRIPT ||
+                    it.elementType == CLASS_BODY || it.elementType == FUNCTION_LITERAL
+        }) {
             if ((node.treeNext.elementType == RBRACE) xor (node.treePrev.elementType == LBRACE)) {
                 // if both are present, this is not beginning or end
                 // if both are null, then this block is empty and is handled in another rule
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/StringUtils.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/StringUtils.kt
index 06ac3590e1..022d1c7913 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/StringUtils.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/StringUtils.kt
@@ -111,3 +111,10 @@ fun String.removePrefix(): String {
     }
     return this
 }
+
+/**
+ * Checks if [this] String is a name of a kotlin script file by checking whether file extension equals 'kts'
+ *
+ * @return true if this is a kotlin script file name, false otherwise
+ */
+fun String.isKotlinScript() = endsWith(".kts")
diff --git a/pom.xml b/pom.xml
index f2869f3ef5..c90dfa48ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -308,7 +308,7 @@
                             <classPath>${maven.compile.classpath}</classPath>
                             <languageVersion>1.4</languageVersion>
                             <jvmTarget>1.8</jvmTarget>
-                            <excludes>**/resources/**/*.kt</excludes>
+                            <excludes>**/resources/**/*.kt,**/resources/**/*.kts</excludes>
                         </configuration>
                     </execution>
                 </executions>


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSmokeTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSmokeTest.kt
index 5828452d55..e99af228b7 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSmokeTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/smoke/DiktatSmokeTest.kt
@@ -25,18 +25,14 @@ import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Tag
 import org.junit.jupiter.api.Test
 
-import java.io.File
-
 import kotlinx.serialization.encodeToString
 
-typealias ruleToConfig = Map<String, Map<String, String>>
+typealias ruleToConfig = Map<Warnings, Map<String, String>>
 
 /**
  * Test for [DiktatRuleSetProvider] in autocorrect mode as a whole. All rules are applied to a file.
  * Note: ktlint uses initial text from a file to calculate line and column from offset. Because of that line/col of unfixed errors
  * may change after some changes to text or other rules.
- *
- * fixme: run as a separate maven goal/module?
  */
 class DiktatSmokeTest : FixTestBase("test/smoke/src/main/kotlin",
     { DiktatRuleSetProvider(configFilePath) },
@@ -55,9 +51,9 @@ class DiktatSmokeTest : FixTestBase("test/smoke/src/main/kotlin",
                     rulesConfig.removeIf { it.name == warning.name }
                     rulesConfig.add(RulesConfig(warning.name, enabled = false, configuration = emptyMap()))
                 }
-                rulesToOverride.forEach { (name, configuration) ->
-                    rulesConfig.removeIf { it.name == name }
-                    rulesConfig.add(RulesConfig(name, enabled = true, configuration = configuration))
+                rulesToOverride.forEach { (warning, configuration) ->
+                    rulesConfig.removeIf { it.name == warning.name }
+                    rulesConfig.add(RulesConfig(warning.name, enabled = true, configuration = configuration))
                 }
             }
         createTempFile()
@@ -106,7 +102,7 @@ class DiktatSmokeTest : FixTestBase("test/smoke/src/main/kotlin",
     fun `smoke test #5`() {
         overrideRulesConfig(emptyList(),
             mapOf(
-                Warnings.HEADER_MISSING_OR_WRONG_COPYRIGHT.name to mapOf(
+                Warnings.HEADER_MISSING_OR_WRONG_COPYRIGHT to mapOf(
                     "isCopyrightMandatory" to "true",
                     "copyrightText" to """|Copyright 2018-2020 John Doe.
                                     |    Licensed under the Apache License, Version 2.0 (the "License");
@@ -146,13 +142,8 @@ class DiktatSmokeTest : FixTestBase("test/smoke/src/main/kotlin",
     @Test
     @Tag("DiktatRuleSetProvider")
     fun `smoke test #2`() {
-        // fixme: path shouldn't point to `target` directory
-        // val expectedFileAbsolutePath = Path.of(Path.of("target/test-classes/$resourceFilePath").toAbsolutePath().toString(), "Example2Test.kt_copy")
-
-        val expectedFileAbsolutePath = File("target/test-classes/$resourceFilePath/Example2Test.kt_copy").absolutePath
         fixAndCompare("Example2Expected.kt", "Example2Test.kt")
         unfixedLintErrors.assertEquals(
-            LintError(1, 1, "$DIKTAT_RULE_SET_ID:file-naming", "${FILE_NAME_INCORRECT.warnText()} Example2Test.kt_copy", true),  // todo this is a false one
             LintError(1, 1, "$DIKTAT_RULE_SET_ID:header-comment", "${HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE.warnText()} there are 2 declared classes and/or objects", false),
             LintError(14, 17, "$DIKTAT_RULE_SET_ID:comments", "${Warnings.COMMENTED_OUT_CODE.warnText()} private class Test : RuntimeException()", false)
         )
@@ -163,8 +154,7 @@ class DiktatSmokeTest : FixTestBase("test/smoke/src/main/kotlin",
     fun `smoke test #1`() {
         fixAndCompare("Example1Expected.kt", "Example1Test.kt")
         unfixedLintErrors.assertEquals(
-            LintError(1, 1, "$DIKTAT_RULE_SET_ID:file-naming", "${FILE_NAME_INCORRECT.warnText()} Example1Test.kt_copy", true),  // todo this is a false one
-            LintError(1, 1, "$DIKTAT_RULE_SET_ID:file-naming", "${FILE_NAME_MATCH_CLASS.warnText()} Example1Test.kt_copy vs Example", true),  // todo this is a false one
+            LintError(1, 1, "$DIKTAT_RULE_SET_ID:file-naming", "${FILE_NAME_MATCH_CLASS.warnText()} Example1Test.kt vs Example", true),
             LintError(1, 1, "$DIKTAT_RULE_SET_ID:kdoc-formatting", "${KDOC_NO_EMPTY_TAGS.warnText()} @return", false),
             LintError(3, 6, "$DIKTAT_RULE_SET_ID:kdoc-comments", "${MISSING_KDOC_TOP_LEVEL.warnText()} Example", false),
             LintError(3, 26, "$DIKTAT_RULE_SET_ID:kdoc-comments", "${MISSING_KDOC_CLASS_ELEMENTS.warnText()} isValid", false),
@@ -179,6 +169,25 @@ class DiktatSmokeTest : FixTestBase("test/smoke/src/main/kotlin",
         )
     }
 
+    @Test
+    @Tag("DiktatRuleSetProvider")
+    fun `smoke test with kts files`() {
+        overrideRulesConfig(
+            listOf(
+                HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE  // because build.gradle.kts doesn't need extra comments, and this rule can be manually disabled if needed
+            ),
+            mapOf(
+                Warnings.WRONG_INDENTATION to mapOf(
+                    "newlineAtEnd" to "false",
+                    "extendedIndentOfParameters" to "false",
+                )
+            )
+        )  // so that trailing newline isn't checked
+        // file name is `gradle_` so that IDE doesn't suggest to import gradle project
+        fixAndCompare("../../../build.gradle_.kts", "../../../build.gradle_.kts")
+        Assertions.assertTrue(unfixedLintErrors.isEmpty())
+    }
+
     companion object {
         private const val DEFAULT_CONFIG_PATH = "../diktat-analysis.yml"
         private val unfixedLintErrors: MutableList<LintError> = mutableListOf()
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt
index 08ab2f3603..bc4098575f 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/LintTestBase.kt
@@ -30,6 +30,7 @@ open class LintTestBase(private val ruleSupplier: (rulesConfigList: List<RulesCo
         KtLint.lint(
             KtLint.Params(
                 fileName = actualFileName,
+                script = actualFileName.endsWith("kts"),
                 text = code,
                 ruleSets = listOf(DiktatRuleSetProvider4Test(ruleSupplier,
                     rulesConfigList ?: this.rulesConfigList).get()),
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt
index 6d83f26a9e..4fbc326d14 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt
@@ -80,9 +80,10 @@ internal fun format(ruleSetProviderRef: (rulesConfigList: List<RulesConfig>?) ->
             KtLint.Params(
                 text = text,
                 ruleSets = listOf(ruleSetProviderRef.invoke(rulesConfigList).get()),
-                fileName = fileName,
+                fileName = fileName.removeSuffix("_copy"),
+                script = fileName.removeSuffix("_copy").endsWith("kts"),
                 cb = cb,
-                userData = mapOf("file_path" to fileName)
+                userData = mapOf("file_path" to fileName.removeSuffix("_copy"))
             )
         )
 
diff --git a/diktat-rules/src/test/resources/test/smoke/build.gradle_.kts b/diktat-rules/src/test/resources/test/smoke/build.gradle_.kts
new file mode 100644
index 0000000000..649b9b7d6a
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/smoke/build.gradle_.kts
@@ -0,0 +1,36 @@
+import org.cqfn.diktat.generation.docs.generateAvailableRules
+import org.cqfn.diktat.generation.docs.generateCodeStyle
+import org.cqfn.diktat.generation.docs.generateFullDoc
+import org.cqfn.diktat.generation.docs.generateRulesMapping
+
+plugins {
+    kotlin("jvm") version "1.4.21"
+    id("org.cqfn.diktat.diktat-gradle-plugin") version "0.1.7"
+}
+
+repositories {
+    jcenter()
+    mavenCentral()
+    maven { url = uri("https://example.com") }
+}
+
+tasks.register("generateAvailableRules") {
+    dependsOn("generateRulesMapping")
+    doFirst {
+        generateAvailableRules(rootDir, file("$rootDir/../wp"))
+    }
+}
+
+tasks.register("updateDocumentation") {
+    dependsOn(
+        "generateRulesMapping",
+        "generateAvailableRules",
+        "generateFullDoc",
+        "generateCodeStyle"
+    )
+}
+
+diktat {
+    debug = true
+    inputs = files("buildSrc/**/*.kt", "*.kts")
+}