Repository: analysis-dev/diktat (kotlin)

Issue:
Kdoc-formatting failure
**Diktat version:** 0.5.2

```
[ERROR] 2021-05-08 23:21:19 Internal error has occurred in kdoc-formatting. Please make an issue on this bug at https://github.com/cqfn/diKTat/.
java.lang.NullPointerException: Cannot invoke "org.jetbrains.kotlin.com.intellij.psi.impl.source.codeStyle.IndentHelper.getIndent(org.jetbrains.kotlin.com.intellij.psi.PsiFile, org.jet
brains.kotlin.com.intellij.lang.ASTNode)" because the return value of "org.jetbrains.kotlin.com.intellij.psi.impl.source.codeStyle.IndentHelper.getInstance()" is null
        at org.jetbrains.kotlin.com.intellij.psi.impl.source.codeStyle.CodeEditUtil.saveWhitespacesInfo(CodeEditUtil.java:108)
        at org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.ChangeUtil.saveIndentationToCopy(ChangeUtil.java:104)
        at org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.ChangeUtil.copyLeafWithText(ChangeUtil.java:80)
        at org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement.replaceWithText(LeafElement.java:153)
        at org.cqfn.diktat.ruleset.utils.AstNodeUtilsKt.leaveExactlyNumNewLines(AstNodeUtils.kt:522)
        at org.cqfn.diktat.ruleset.utils.AstNodeUtilsKt.leaveOnlyOneNewLine(AstNodeUtils.kt:515)
        at org.cqfn.diktat.ruleset.rules.chapter2.kdoc.KdocFormatting$checkEmptyLinesBetweenBasicTags$1$1.invoke(KdocFormatting.kt:272)
        at org.cqfn.diktat.ruleset.rules.chapter2.kdoc.KdocFormatting$checkEmptyLinesBetweenBasicTags$1$1.invoke(KdocFormatting.kt:62)
        at org.cqfn.diktat.ruleset.constants.Warnings.warnAndFix(Warnings.kt:258)
        at org.cqfn.diktat.ruleset.constants.Warnings.warnAndFix$default(Warnings.kt:206)
        at org.cqfn.diktat.ruleset.rules.chapter2.kdoc.KdocFormatting.checkEmptyLinesBetweenBasicTags(KdocFormatting.kt:270)
        at org.cqfn.diktat.ruleset.rules.chapter2.kdoc.KdocFormatting.logic(KdocFormatting.kt:88)
        at org.cqfn.diktat.ruleset.rules.DiktatRule.visit(DiktatRule.kt:46)
        at com.pinterest.ktlint.core.KtLint$format$1.invoke(KtLint.kt:271)
        at com.pinterest.ktlint.core.KtLint$format$1.invoke(KtLint.kt:23)
        at com.pinterest.ktlint.core.KtLint$visitor$2$2.invoke(KtLint.kt:212)
        at com.pinterest.ktlint.core.KtLint$visitor$2$2.invoke(KtLint.kt:23)
        at com.pinterest.ktlint.core.ast.PackageKt.visit(package.kt:224)
        at com.pinterest.ktlint.core.ast.PackageKt.visit(package.kt:225)
        at com.pinterest.ktlint.core.ast.PackageKt.visit(package.kt:225)
        at com.pinterest.ktlint.core.ast.PackageKt.visit(package.kt:225)
        at com.pinterest.ktlint.core.ast.PackageKt.visit(package.kt:225)
        at com.pinterest.ktlint.core.KtLint$visitor$2.invoke(KtLint.kt:211)
        at com.pinterest.ktlint.core.KtLint$visitor$2.invoke(KtLint.kt:23)
        at com.pinterest.ktlint.core.KtLint.format(KtLint.kt:264)
        at com.pinterest.ktlint.internal.FileUtilsKt.formatFile(FileUtils.kt:79)
        at com.pinterest.ktlint.KtlintCommandLine.process(Main.kt:331)
        at com.pinterest.ktlint.KtlintCommandLine.access$process(Main.kt:116)
        at com.pinterest.ktlint.KtlintCommandLine$lintFiles$2$1.call(Main.kt:257)
        at com.pinterest.ktlint.KtlintCommandLine$lintFiles$2$1.call(Main.kt:116)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
        at java.base/java.lang.Thread.run(Thread.java:832)
[ERROR] 2021-05-08 23:21:19 As a workaround you can disable these inspections in yml config: [KDOC_CONTAINS_DATE_OR_AUTHOR, KDOC_EMPTY_KDOC, KDOC_NEWLINES_BEFORE_BASIC_TAGS, KDOC_NO_DE
PRECATED_TAG, KDOC_NO_EMPTY_TAGS, KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS, KDOC_NO_NEWLINE_AFTER_SPECIAL_TAGS, KDOC_WRONG_SPACES_AFTER_TAG, KDOC_WRONG_TAGS_ORDER]

```
**Reproducer:**
```
package com.akuleshov7.ktoml.parsers.node

import com.akuleshov7.ktoml.error
import com.akuleshov7.ktoml.exceptions.InternalAstException
import com.akuleshov7.ktoml.exceptions.InternalParsingException
import com.akuleshov7.ktoml.exceptions.TomlParsingException
import com.akuleshov7.ktoml.parsingError



class TomlFile : TomlNode("rootNode", 0) {
    override val name = "rootNode"

    override fun getNeighbourNodes() =
        throw InternalAstException("Invalid call to getNeighbourNodes() for TomlFile node")

    /**
     * This method recursively finds child toml table with the proper name in AST.
     * Stops processing if AST is broken and it has more than one table with the searched name on the same level.
     * @param searchedTableName - the table name that is expected to be found in the list of children of this node
     * @param searchedLevel - the level of nested child node that is searched level (indexed from 1)
     *
     * For example: findTableInAstByName("a.d", 2) will find [a.d] table in the following tree:
     *     a
     *    /  \
     *   a.c a.d
     *        \
     *        a.d.e
     */
    fun findTableInAstByName(searchedTableName: String, searchedLevel: Int): TomlTable? {
    }
}
```

Pull request:
### What's done:
* Updated logic
* Added tests

We were treating any blank line in KDoc tag's body as an error and were trying to remove it, while empty lines *between* tag's lines are valid.

### TODO:
* [x] example with trailing blank line in tag's body

This pull request closes #868

Patch:
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/KdocFormatting.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/KdocFormatting.kt
index 0fabddb676..0425cbfaad 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/KdocFormatting.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/KdocFormatting.kt
@@ -20,8 +20,10 @@ import org.cqfn.diktat.ruleset.utils.getAllChildrenWithType
 import org.cqfn.diktat.ruleset.utils.getFirstChildWithType
 import org.cqfn.diktat.ruleset.utils.getIdentifierName
 import org.cqfn.diktat.ruleset.utils.hasChildMatching
+import org.cqfn.diktat.ruleset.utils.hasTrailingNewlineInTagBody
 import org.cqfn.diktat.ruleset.utils.kDocTags
 import org.cqfn.diktat.ruleset.utils.leaveOnlyOneNewLine
+import org.cqfn.diktat.ruleset.utils.reversedChildren
 
 import com.pinterest.ktlint.core.ast.ElementType
 import com.pinterest.ktlint.core.ast.ElementType.KDOC
@@ -39,7 +41,6 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement
 import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement
 import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
 import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
-import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet
 import org.jetbrains.kotlin.kdoc.parser.KDocKnownTag
 import org.jetbrains.kotlin.kdoc.psi.impl.KDocTag
 import org.jetbrains.kotlin.lexer.KtTokens
@@ -238,11 +239,9 @@ class KdocFormatting(configRules: List<RulesConfig>) : DiktatRule(
                         previousTag.addChild(treePrev.clone() as ASTNode, null)
                         previousTag.addChild(this.clone() as ASTNode, null)
                     }
-                        ?: run {
-                            firstBasicTag.node.applyToPrevSibling(KDOC_LEADING_ASTERISK) {
-                                treeParent.addChild(treePrev.clone() as ASTNode, this)
-                                treeParent.addChild(this.clone() as ASTNode, treePrev)
-                            }
+                        ?: firstBasicTag.node.applyToPrevSibling(KDOC_LEADING_ASTERISK) {
+                            treeParent.addChild(treePrev.clone() as ASTNode, this)
+                            treeParent.addChild(this.clone() as ASTNode, treePrev)
                         }
                 } else {
                     firstBasicTag.node.apply {
@@ -256,27 +255,26 @@ class KdocFormatting(configRules: List<RulesConfig>) : DiktatRule(
     }
 
     private fun checkEmptyLinesBetweenBasicTags(basicTags: List<KDocTag>) {
-        val tagsWithRedundantEmptyLines = basicTags.dropLast(1).filterNot { tag ->
+        val tagsWithRedundantEmptyLines = basicTags.dropLast(1).filter { tag ->
             val nextWhiteSpace = tag.node.nextSibling { it.elementType == WHITE_SPACE }
-            val noEmptyKdocLines = tag
-                .node
-                .getChildren(TokenSet.create(KDOC_LEADING_ASTERISK))
-                .filter { it.treeNext == null || it.treeNext.elementType == WHITE_SPACE }
-                .count() == 0
-            nextWhiteSpace?.text?.count { it == '\n' } == 1 && noEmptyKdocLines
+            // either there is a trailing blank line in tag's body OR there are empty lines right after this tag
+            tag.hasTrailingNewlineInTagBody() || nextWhiteSpace?.text?.count { it == '\n' } != 1
         }
 
         tagsWithRedundantEmptyLines.forEach { tag ->
             KDOC_NO_NEWLINES_BETWEEN_BASIC_TAGS.warnAndFix(configRules, emitWarn, isFixMode,
                 "@${tag.name}", tag.startOffset, tag.node) {
-                tag.node.nextSibling { it.elementType == WHITE_SPACE }?.leaveOnlyOneNewLine()
-                // the first asterisk before tag is not included inside KDOC_TAG node
-                // we look for the second and take its previous which should be WHITE_SPACE with newline
-                tag
-                    .node
-                    .getAllChildrenWithType(KDOC_LEADING_ASTERISK)
-                    .firstOrNull()
-                    ?.let { tag.node.removeRange(it.treePrev, null) }
+                if (tag.hasTrailingNewlineInTagBody()) {
+                    // if there is a blank line in tag's body, we remove it and everything after it, so that the next white space is kept in place
+                    // we look for the last LEADING_ASTERISK and take its previous node which should be WHITE_SPACE with newline
+                    tag.node.reversedChildren()
+                        .takeWhile { it.elementType == WHITE_SPACE || it.elementType == KDOC_LEADING_ASTERISK }
+                        .firstOrNull { it.elementType == KDOC_LEADING_ASTERISK }
+                        ?.let { tag.node.removeRange(it.treePrev, null) }
+                } else {
+                    // otherwise we remove redundant blank lines from white space node after tag
+                    tag.node.nextSibling { it.elementType == WHITE_SPACE }?.leaveOnlyOneNewLine()
+                }
             }
         }
     }
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
index 6b6359f8b5..e1f292178e 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/AstNodeUtils.kt
@@ -115,6 +115,19 @@ fun ASTNode.isCorrect() = this.findAllDescendantsWithSpecificType(TokenType.ERRO
 fun ASTNode.getAllChildrenWithType(elementType: IElementType): List<ASTNode> =
         this.getChildren(null).filter { it.elementType == elementType }
 
+/**
+ * Generates a sequence of this ASTNode's children in reversed order
+ *
+ * @return a reevrsed sequence of children
+ */
+fun ASTNode.reversedChildren(): Sequence<ASTNode> = sequence {
+    var node = lastChildNode
+    while (node != null) {
+        yield(node)
+        node = node.treePrev
+    }
+}
+
 /**
  * Replaces the [beforeNode] of type [WHITE_SPACE] with the node with specified [text]
  *
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/KdocUtils.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/KdocUtils.kt
index 37ba57eb2a..177be2abf5 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/KdocUtils.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/KdocUtils.kt
@@ -7,6 +7,7 @@ package org.cqfn.diktat.ruleset.utils
 import com.pinterest.ktlint.core.ast.ElementType
 import com.pinterest.ktlint.core.ast.ElementType.KDOC_SECTION
 import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
+import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline
 import com.pinterest.ktlint.core.ast.prevSibling
 import org.jetbrains.kotlin.com.intellij.lang.ASTNode
 import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement
@@ -33,6 +34,23 @@ fun ASTNode.kDocTags(): List<KDocTag> {
 fun Iterable<KDocTag>.hasKnownKdocTag(knownTag: KDocKnownTag): Boolean =
         this.find { it.knownTag == knownTag } != null
 
+/**
+ * Checks for trailing newlines in tag's body. Handles cases, when there is no leading asterisk on an empty line:
+ * ```
+ * * @param param
+ *
+ * * @return
+ * ```
+ * as well as usual simple cases.
+ *
+ * @return true if there is a trailing newline
+ */
+fun KDocTag.hasTrailingNewlineInTagBody() = node.lastChildNode.isWhiteSpaceWithNewline() ||
+        node.reversedChildren()
+            .takeWhile { it.elementType == WHITE_SPACE || it.elementType == ElementType.KDOC_LEADING_ASTERISK }
+            .firstOrNull { it.elementType == ElementType.KDOC_LEADING_ASTERISK }
+            ?.takeIf { it.treeNext == null || it.treeNext.elementType == WHITE_SPACE } != null
+
 /**
  * This method inserts a new tag into KDoc before specified another tag, aligning it with the rest of this KDoc
  *


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 a766625e9c..90cbdd62b3 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
@@ -259,6 +259,12 @@ class DiktatSmokeTest : FixTestBase("test/smoke/src/main/kotlin",
         )
     }
 
+    @Test
+    @Tag("DiktatRuleSetProvider")
+    fun `regression - should correctly handle tags with empty lines`() {
+        fixAndCompare("KdocFormattingMultilineTagsExpected.kt", "KdocFormattingMultilineTagsTest.kt")
+    }
+
     companion object {
         private const val DEFAULT_CONFIG_PATH = "../diktat-analysis.yml"
         private val unfixedLintErrors: MutableList<LintError> = mutableListOf()
diff --git a/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/KdocFormattingMultilineTagsExpected.kt b/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/KdocFormattingMultilineTagsExpected.kt
new file mode 100644
index 0000000000..d62b70bbe7
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/KdocFormattingMultilineTagsExpected.kt
@@ -0,0 +1,22 @@
+package org.cqfn.diktat
+
+/**
+ * @param bar lorem ipsum
+ *
+ * dolor sit amet
+ * @return
+ */
+fun foo1(bar: Bar): Baz {
+    // placeholder
+}
+
+/**
+ * @param bar lorem ipsum
+ *
+ * dolor sit amet
+ * @return
+ */
+fun foo2(bar: Bar): Baz {
+    // placeholder
+}
+
diff --git a/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/KdocFormattingMultilineTagsTest.kt b/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/KdocFormattingMultilineTagsTest.kt
new file mode 100644
index 0000000000..820e8a3c1d
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/smoke/src/main/kotlin/KdocFormattingMultilineTagsTest.kt
@@ -0,0 +1,18 @@
+/**
+ * @param bar lorem ipsum
+ *
+ * dolor sit amet
+ */
+fun foo1(bar: Bar): Baz {
+    // placeholder
+}
+
+/**
+ * @param bar lorem ipsum
+ *
+ * dolor sit amet
+ *
+ */
+fun foo2(bar: Bar): Baz {
+    // placeholder
+}