Repository: analysis-dev/diktat (kotlin)

Issue:
NPE in LONG_LINE rule on Binary Expressions
The file: RxKotlin/src/main/kotlin/io/reactivex/rxjava3/kotlin/Observables.kt from https://github.com/ReactiveX/RxKotlin

fails due to Binary Expression (String Concatenation) with 

```

[ERROR] 2021-06-10 16:34:37 Internal error has occurred in rule [line-length]. Please make an issue on this bug at https://github.com/cqfn/diKTat/.
                       As a workaround you can disable these inspections in yml config: <[LONG_LINE]>.
                       Root cause of the problem is in [/mnt/d/RxKotlin/src/main/kotlin/io/reactivex/rxjava3/kotlin/Observables.kt] file.
                       Node, where the problem appeared:
                       kotlin.FILE: "@file:Suppress("unused", "HasPlatformType")

java.lang.NullPointerException
        at org.cqfn.diktat.ruleset.rules.chapter3.LineLength.fixBinaryExpression(LineLength.kt:252)
        at org.cqfn.diktat.ruleset.rules.chapter3.LineLength.fixError(LineLength.kt:233)

```

Pull request description:
### What's done:
* Fix bug with NPE: we didn't consider, that offset changes in course of fixes, so it's break the logic sometimes
* Add additional check for long comments: if there is only one white space, which is located after `//` - we shouldn't fix split it
* Add tests

Patch:
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/LineLength.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/LineLength.kt
index 5ea3e4402c..0981c113bf 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/LineLength.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/LineLength.kt
@@ -91,7 +91,12 @@ class LineLength(configRules: List<RulesConfig>) : DiktatRule(
                     LONG_LINE.warnAndFix(configRules, emitWarn, isFixMode,
                         "max line length ${configuration.lineLength}, but was ${line.length}",
                         offset + node.startOffset, node, fixableType != LongLineFixableCases.None) {
+                        // we should keep in mind, that in the course of fixing we change the offset
+                        val textLenBeforeFix = node.textLength
                         fixError(fixableType)
+                        val textLenAfterFix = node.textLength
+                        // offset for all next nodes changed to this delta
+                        offset += (textLenAfterFix - textLenBeforeFix)
                     }
                 }
             }
@@ -137,13 +142,14 @@ class LineLength(configRules: List<RulesConfig>) : DiktatRule(
      */
     private fun checkStringTemplate(node: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases {
         val leftOffset = positionByOffset(node.startOffset).second
-        val difference = configuration.lineLength.toInt() - leftOffset
-        // case when new line should be inserted after `+`. Example: "first" + "second"
-        if (difference > node.text.length) {
-            return LongLineFixableCases.BinaryExpression(node.treeParent)
-        }
         val delimiterIndex = node.text.substring(0, configuration.lineLength.toInt() - leftOffset).lastIndexOf(' ')
         if (delimiterIndex == -1) {
+            // we can't split this string, however may be we can move it entirely:
+            // case when new line should be inserted after `+`. Example: "first" + "second"
+            node.treeParent.findChildByType(OPERATION_REFERENCE)?.let {
+                return LongLineFixableCases.BinaryExpression(node.treeParent)
+            }
+            // can't fix this case
             return LongLineFixableCases.None
         }
         // minus 2 here as we are inserting ` +` and we don't want it to exceed line length
@@ -161,7 +167,9 @@ class LineLength(configRules: List<RulesConfig>) : DiktatRule(
     private fun checkComment(wrongNode: ASTNode, configuration: LineLengthConfiguration): LongLineFixableCases {
         val leftOffset = positionByOffset(wrongNode.startOffset).second
         val indexLastSpace = wrongNode.text.substring(0, configuration.lineLength.toInt() - leftOffset).lastIndexOf(' ')
-        if (indexLastSpace == -1) {
+        // index == -1 indicates, that we didn't find any possible way to split this comment
+        // index == 2 indicates, that we found the white space after `//`, and shouldn't fix it
+        if (indexLastSpace == -1 || indexLastSpace == 2) {
             return LongLineFixableCases.None
         }
         return LongLineFixableCases.Comment(wrongNode, indexLastSpace)


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/LineLengthFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/LineLengthFixTest.kt
index e0f317ae40..6325ed139d 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/LineLengthFixTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/LineLengthFixTest.kt
@@ -30,6 +30,11 @@ class LineLengthFixTest : FixTestBase("test/paragraph3/long_line", ::LineLength)
         fixAndCompare("LongLineCommentExpected2.kt", "LongLineCommentTest2.kt", rulesConfigListDefaultLineLength)
     }
 
+    @Test
+    fun `should fix long string template while some fix is already done`() {
+        fixAndCompare("LongStringTemplateExpected.kt", "LongStringTemplateTest.kt", rulesConfigListDefaultLineLength)
+    }
+
     @Test
     fun `should fix long binary expression`() {
         fixAndCompare("LongLineExpressionExpected.kt", "LongLineExpressionTest.kt", rulesConfigListLineLength)
diff --git a/diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixExpected.kt b/diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixExpected.kt
index e6b32e1a9c..ddc30f0694 100644
--- a/diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixExpected.kt
+++ b/diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixExpected.kt
@@ -1,6 +1,8 @@
 package org.cqfn.diktat.resources.paragraph3.longline
 
 class veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong {
+    // looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg
+    //looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg
     val s = "d" +
 " s d d d d ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
 }
diff --git a/diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixTest.kt b/diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixTest.kt
index 407a2a61d3..286bc4e997 100644
--- a/diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixTest.kt
+++ b/diktat-rules/src/test/resources/test/paragraph3/long_line/LongExpressionNoFixTest.kt
@@ -1,5 +1,7 @@
 package org.cqfn.diktat.resources.paragraph3.longline
 
 class veryLoooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong {
+    // looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg
+    //looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg
     val s = "d s d d d d ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
 }
diff --git a/diktat-rules/src/test/resources/test/paragraph3/long_line/LongStringTemplateExpected.kt b/diktat-rules/src/test/resources/test/paragraph3/long_line/LongStringTemplateExpected.kt
new file mode 100644
index 0000000000..b15925bf85
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/long_line/LongStringTemplateExpected.kt
@@ -0,0 +1,12 @@
+object Observables {
+    val someCode = 15 // looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg
+    // Some
+// looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong line
+    @Deprecated("New type inference algorithm in Kotlin 1.4 makes this method obsolete. Method will be removed in" +
+" future RxKotlin release.",
+        replaceWith = ReplaceWith("Observable.combineLatest(source1, source2, source3, source4, combineFunction)", "io.reactivex.Observable"),
+        level = DeprecationLevel.WARNING)
+    @CheckReturnValue
+    @SchedulerSupport(SchedulerSupport.NONE)
+    inline fun <T1 : Any, T2 : Any, T3 : Any, T4 : Any, R : Any> combineLatest() {}
+}
diff --git a/diktat-rules/src/test/resources/test/paragraph3/long_line/LongStringTemplateTest.kt b/diktat-rules/src/test/resources/test/paragraph3/long_line/LongStringTemplateTest.kt
new file mode 100644
index 0000000000..bff4d2ad64
--- /dev/null
+++ b/diktat-rules/src/test/resources/test/paragraph3/long_line/LongStringTemplateTest.kt
@@ -0,0 +1,10 @@
+object Observables {
+    val someCode = 15 // looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooongggggggggggggggggggggggggg
+    // Some looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong line
+    @Deprecated("New type inference algorithm in Kotlin 1.4 makes this method obsolete. Method will be removed in future RxKotlin release.",
+        replaceWith = ReplaceWith("Observable.combineLatest(source1, source2, source3, source4, combineFunction)", "io.reactivex.Observable"),
+        level = DeprecationLevel.WARNING)
+    @CheckReturnValue
+    @SchedulerSupport(SchedulerSupport.NONE)
+    inline fun <T1 : Any, T2 : Any, T3 : Any, T4 : Any, R : Any> combineLatest() {}
+}