Repository: analysis-dev/diktat (kotlin)

Issue:
Package name fixing incorrect with file-level directives
Package name rule incorrectly adds package directive to the following code:

```
@file:Suppress("CONSTANT_UPPERCASE")

// FixMe: we need to use package name here when https://github.com/gradle/gradle/issues/9270 will be fixed
// now it is just a workaround

object Versions {
    const val KOTLIN = "1.4.10"
    const val JUNIT = "5.7.1"
    const val OKIO = "3.0.0-alpha.1"
    const val SERIALIZATION = "1.1.0"
}

```

To reproduce use:
` ./ktlint -R diktat-0.5.2.jar --disabled_rules=standard -F --debug "Test.kt"`

Pull request description:
### What's done:
* Fix bug with incorrect package name fixing when there is exist file annotation
* Add tests

Closes #875

Patch:
diff --git a/README.md b/README.md
index e413764651..dafdee4b57 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ Main features of diktat are the following:
     # another option is "brew install ktlint"
     ```
    
-2. Load diKTat manually: [here](https://github.com/cqfn/diKTat/releases/download/v0.5.2/diktat.jar)
+2. Load diKTat manually: [here](https://github.com/cqfn/diKTat/releases/download/v0.5.2/diktat-0.5.2.jar)
 
    **OR** use curl:
    ```bash
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter1/PackageNaming.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter1/PackageNaming.kt
index 63f8202f38..924e5c84b0 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter1/PackageNaming.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter1/PackageNaming.kt
@@ -12,10 +12,16 @@ import org.cqfn.diktat.ruleset.constants.Warnings.PACKAGE_NAME_MISSING
 import org.cqfn.diktat.ruleset.rules.DiktatRule
 import org.cqfn.diktat.ruleset.utils.*
 
+import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT
 import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION
+import com.pinterest.ktlint.core.ast.ElementType.EOL_COMMENT
+import com.pinterest.ktlint.core.ast.ElementType.FILE_ANNOTATION_LIST
 import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
+import com.pinterest.ktlint.core.ast.ElementType.KDOC
 import com.pinterest.ktlint.core.ast.ElementType.PACKAGE_DIRECTIVE
 import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
+import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
+import com.pinterest.ktlint.core.ast.children
 import com.pinterest.ktlint.core.ast.isLeaf
 import org.jetbrains.kotlin.com.intellij.lang.ASTNode
 import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
@@ -218,8 +224,27 @@ class PackageNaming(configRules: List<RulesConfig>) : DiktatRule(
             ?: run {
                 // there is missing package statement in a file, so it will be created and inserted
                 val newPackageDirective = generatedPackageDirective.findLeafWithSpecificType(PACKAGE_DIRECTIVE)!!
-                packageDirectiveNode.treeParent.replaceChild(packageDirectiveNode, newPackageDirective)
-                newPackageDirective.treeParent.addChild(PsiWhiteSpaceImpl("\n\n"), newPackageDirective.treeNext)
+                val packageDirectiveParent = packageDirectiveNode.treeParent
+                // When package directive is missing in .kt file,
+                // the node is still present in the AST, and not always in a convenient place.
+                // E.g. `@file:Suppress("...") // comments`
+                // AST will be: FILE_ANNOTATION_LIST, PACKAGE_DIRECTIVE, WHITE_SPACE, EOL_COMMENT
+                // So, we can't just put new package directive in it's old place and rely on FileStructure rule
+                if (packageDirectiveNode != packageDirectiveParent.firstChildNode) {
+                    // We will insert new package directive node before first node, which is not in the following list
+                    val possibleTypesBeforePackageDirective = listOf(WHITE_SPACE, EOL_COMMENT, BLOCK_COMMENT, KDOC, PACKAGE_DIRECTIVE, FILE_ANNOTATION_LIST)
+                    val addBefore = packageDirectiveParent.children().first { it.elementType !in possibleTypesBeforePackageDirective }
+                    packageDirectiveParent.removeChild(packageDirectiveNode)
+                    packageDirectiveParent.addChild(newPackageDirective, addBefore)
+                    if (newPackageDirective.treePrev.elementType != WHITE_SPACE) {
+                        packageDirectiveParent.addChild(PsiWhiteSpaceImpl("\n"), newPackageDirective)
+                    }
+                } else {
+                    packageDirectiveParent.replaceChild(packageDirectiveNode, newPackageDirective)
+                }
+                if (newPackageDirective.treeNext.elementType != WHITE_SPACE) {
+                    packageDirectiveParent.addChild(PsiWhiteSpaceImpl("\n"), newPackageDirective.treeNext)
+                }
             }
     }
 
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/FileStructureRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/FileStructureRule.kt
index 82b4bad358..4bba67399a 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/FileStructureRule.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/FileStructureRule.kt
@@ -304,7 +304,7 @@ class FileStructureRule(configRules: List<RulesConfig>) : DiktatRule(
                 if (elementType == WHITE_SPACE && text.count { it == '\n' } != 2) {
                     FILE_NO_BLANK_LINE_BETWEEN_BLOCKS.warnAndFix(configRules, emitWarn, isFixMode, astNode.text.lines().first(),
                         astNode.startOffset, astNode) {
-                        (this as LeafPsiElement).replaceWithText("\n\n${text.replace("\n", "")}")
+                        (this as LeafPsiElement).parent.node.replaceChild(this, PsiWhiteSpaceImpl("\n\n${text.replace("\n", "")}"))
                     }
                 }
             }


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackageNamingWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackageNamingWarnTest.kt
index 1f700f71ce..674a17102c 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackageNamingWarnTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackageNamingWarnTest.kt
@@ -51,6 +51,26 @@ class PackageNamingWarnTest : LintTestBase(::PackageNaming) {
         )
     }
 
+    @Test
+    @Tag(WarningNames.PACKAGE_NAME_MISSING)
+    fun `missing package name with annotation (check)`() {
+        lintMethod(
+            """
+                @file:Suppress("CONSTANT_UPPERCASE")
+
+                import org.cqfn.diktat.a.b.c
+
+                /**
+                 * testComment
+                 */
+                class TestPackageName {  }
+
+            """.trimIndent(),
+            LintError(1, 37, ruleId, "${PACKAGE_NAME_MISSING.warnText()} $TEST_FILE_NAME", true),
+            rulesConfigList = rulesConfigList
+        )
+    }
+
     @Test
     @Tag(WarningNames.PACKAGE_NAME_INCORRECT_CASE)
     fun `package name should be in a lower case (check)`() {
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackagePathFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackagePathFixTest.kt
index ba7aba84af..5bd83cc205 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackagePathFixTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter1/PackagePathFixTest.kt
@@ -31,6 +31,32 @@ class PackagePathFixTest : FixTestBase(
         fixAndCompare("some/FixIncorrectExpected.kt", "some/FixIncorrectTest.kt")
     }
 
+    @Test
+    @Tag(WarningNames.PACKAGE_NAME_MISSING)
+    fun `fix missing package name with file annotation`() {
+        fixAndCompare("org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected.kt", "org/cqfn/diktat/some/name/FixMissingWithAnnotationTest.kt")
+    }
+
+    @Test
+    @Tag(WarningNames.PACKAGE_NAME_MISSING)
+    fun `fix missing package name with file annotation and comments`() {
+        fixAndCompare("org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected2.kt", "org/cqfn/diktat/some/name/FixMissingWithAnnotationTest2.kt")
+    }
+
+    @Test
+    @Tag(WarningNames.PACKAGE_NAME_MISSING)
+    fun `fix missing package name with file annotation and comments 2`() {
+        fixAndCompare("org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected3.kt", "org/cqfn/diktat/some/name/FixMissingWithAnnotationTest3.kt")
+    }
+
+    // If there is no import list in code, the node is still present in the AST, but without any whitespaces around
+    // So, this check covered case, when we manually add whitespace before package directive
+    @Test
+    @Tag(WarningNames.PACKAGE_NAME_MISSING)
+    fun `fix missing package name without import list`() {
+        fixAndCompare("org/cqfn/diktat/some/name/FixMissingWithoutImportExpected.kt", "org/cqfn/diktat/some/name/FixMissingWithoutImportTest.kt")
+    }
+
     @Test
     @Tag(WarningNames.PACKAGE_NAME_MISSING)
     fun `fix missing package name with a proper location without domain`() {
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleFixTest.kt
index 4b6d875b60..cd5a6c8862 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleFixTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/files/TopLevelOrderRuleFixTest.kt
@@ -4,6 +4,7 @@ import org.cqfn.diktat.ruleset.rules.chapter3.files.TopLevelOrderRule
 import org.cqfn.diktat.util.FixTestBase
 
 import generated.WarningNames
+import org.junit.jupiter.api.Disabled
 import org.junit.jupiter.api.Tag
 import org.junit.jupiter.api.Test
 
@@ -19,4 +20,12 @@ class TopLevelOrderRuleFixTest : FixTestBase("test/paragraph3/top_level", ::TopL
     fun `should fix top level order with comment`() {
         fixAndCompare("TopLevelWithCommentExpected.kt", "TopLevelWithCommentTest.kt")
     }
+
+    // FixMe: should be considered this case (swapped order of kdoc and package directive)
+    @Disabled("Isn't working yet")
+    @Test
+    @Tag(WarningNames.TOP_LEVEL_ORDER)
+    fun `should fix top level order with header kdoc`() {
+        fixAndCompare("TopLevelWithHeaderKdocExpected.kt", "TopLevelWithHeaderKdocTest.kt")
+    }
 }
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingExpected.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingExpected.kt
index 58da8cfa0f..c1a68822bb 100644
--- a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingExpected.kt
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingExpected.kt
@@ -1,5 +1,4 @@
 package org.cqfn.diktat.some.name
-
 import org.cqfn.diktat.ktlint.core.Rule
 
 class TestPackageName {
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected.kt
new file mode 100644
index 0000000000..92cccc5b65
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected.kt
@@ -0,0 +1,7 @@
+@file:Suppress("CONSTANT_UPPERCASE")
+
+package org.cqfn.diktat.some.name
+import org.cqfn.diktat.ktlint.core.Rule
+
+class TestPackageName {
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected2.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected2.kt
new file mode 100644
index 0000000000..77803de762
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected2.kt
@@ -0,0 +1,6 @@
+@file:Suppress("CONSTANT_UPPERCASE") // comment
+package org.cqfn.diktat.some.name
+import org.cqfn.diktat.ktlint.core.Rule
+
+class TestPackageName {
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected3.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected3.kt
new file mode 100644
index 0000000000..4194493d0f
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationExpected3.kt
@@ -0,0 +1,9 @@
+/**
+ * comment
+ */
+@file:Suppress("CONSTANT_UPPERCASE")
+package org.cqfn.diktat.some.name
+import org.cqfn.diktat.ktlint.core.Rule
+
+class TestPackageName {
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationTest.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationTest.kt
new file mode 100644
index 0000000000..57e4a1129a
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationTest.kt
@@ -0,0 +1,6 @@
+@file:Suppress("CONSTANT_UPPERCASE")
+
+import org.cqfn.diktat.ktlint.core.Rule
+
+class TestPackageName {
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationTest2.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationTest2.kt
new file mode 100644
index 0000000000..ad4353f719
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationTest2.kt
@@ -0,0 +1,5 @@
+@file:Suppress("CONSTANT_UPPERCASE") // comment
+import org.cqfn.diktat.ktlint.core.Rule
+
+class TestPackageName {
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationTest3.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationTest3.kt
new file mode 100644
index 0000000000..76c83cb771
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithAnnotationTest3.kt
@@ -0,0 +1,8 @@
+/**
+ * comment
+ */
+@file:Suppress("CONSTANT_UPPERCASE")
+import org.cqfn.diktat.ktlint.core.Rule
+
+class TestPackageName {
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithoutImportExpected.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithoutImportExpected.kt
new file mode 100644
index 0000000000..666790dfe6
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithoutImportExpected.kt
@@ -0,0 +1,7 @@
+@file:Suppress("CONSTANT_UPPERCASE")
+package org.cqfn.diktat.some.name
+
+val a = 5
+
+class TestPackageName {
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithoutImportTest.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithoutImportTest.kt
new file mode 100644
index 0000000000..34f61483b9
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/org/cqfn/diktat/some/name/FixMissingWithoutImportTest.kt
@@ -0,0 +1,5 @@
+@file:Suppress("CONSTANT_UPPERCASE")
+val a = 5
+
+class TestPackageName {
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/some/FixMissingExpected.kt b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/some/FixMissingExpected.kt
index 651271a2f7..c4d5be09e7 100644
--- a/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/some/FixMissingExpected.kt
+++ b/diktat-rules/src/test/resources/test/paragraph1/naming/package/src/main/kotlin/some/FixMissingExpected.kt
@@ -1,5 +1,4 @@
 package org.cqfn.diktat.some
-
 import org.cqfn.diktat.ktlint.core.Rule
 
 class TestPackageName {
diff --git a/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithHeaderKdocExpected.kt b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithHeaderKdocExpected.kt
new file mode 100644
index 0000000000..82534d239f
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithHeaderKdocExpected.kt
@@ -0,0 +1,9 @@
+/**
+ * Header Kdoc
+ */
+
+package test.paragraph3.top_level
+
+import org.cqfn.diktat.bar
+
+class A {}
diff --git a/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithHeaderKdocTest.kt b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithHeaderKdocTest.kt
new file mode 100644
index 0000000000..f7613bf749
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/top_level/TopLevelWithHeaderKdocTest.kt
@@ -0,0 +1,9 @@
+package test.paragraph3.top_level
+
+/**
+ * Header Kdoc
+ */
+
+import org.cqfn.diktat.bar
+
+class A {}