Repository: cqfn/diKTat (kotlin)

Issue:
Diktat should check indentation inside string templates in {}
```kotlin
"Text: ${
expression
}"
```

`expression` should be indented one step more

Pull request description:
## What's done:
  * Added test
  * Fixed bug

This pull request closes #758 

## Actions checklist
* [ ] Implemented Rule, added Warnings
* [X] Added tests on checks
* [X] Added tests on fixers
* [ ] Updated diktat-analysis.yml
* [ ] Updated available-rules.md

Patch:
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt
index 1e8218d90b..bad5240b9d 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter3/files/IndentationRule.kt
@@ -33,6 +33,9 @@ import com.pinterest.ktlint.core.ast.ElementType.FILE
 import com.pinterest.ktlint.core.ast.ElementType.LBRACE
 import com.pinterest.ktlint.core.ast.ElementType.LBRACKET
 import com.pinterest.ktlint.core.ast.ElementType.LITERAL_STRING_TEMPLATE_ENTRY
+import com.pinterest.ktlint.core.ast.ElementType.LONG_STRING_TEMPLATE_ENTRY
+import com.pinterest.ktlint.core.ast.ElementType.LONG_TEMPLATE_ENTRY_END
+import com.pinterest.ktlint.core.ast.ElementType.LONG_TEMPLATE_ENTRY_START
 import com.pinterest.ktlint.core.ast.ElementType.LPAR
 import com.pinterest.ktlint.core.ast.ElementType.RBRACE
 import com.pinterest.ktlint.core.ast.ElementType.RBRACKET
@@ -56,6 +59,7 @@ import org.jetbrains.kotlin.psi.psiUtil.parents
 import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
 import org.jetbrains.kotlin.psi.psiUtil.startOffset
 import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult
+import kotlin.math.abs
 
 /**
  * Rule that checks indentation. The following general rules are checked:
@@ -180,10 +184,15 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
         }
 
         val expectedIndent = checkResult?.expectedIndent ?: indentError.expected
-        if (checkResult?.adjustNext == true) {
+        if (checkResult?.adjustNext == true && astNode.parents().none { it.elementType == LONG_STRING_TEMPLATE_ENTRY }) {
             val exceptionInitiatorNode = astNode.getExceptionalIndentInitiator()
             context.addException(exceptionInitiatorNode, expectedIndent - indentError.expected, checkResult.includeLastChild)
         }
+
+        if (astNode.treeParent.elementType == LONG_STRING_TEMPLATE_ENTRY && indentError.expected != indentError.actual) {
+            context.addException(astNode.treeParent, abs(indentError.expected - indentError.actual), false)
+        }
+
         if (checkResult?.isCorrect != true && expectedIndent != indentError.actual) {
             WRONG_INDENTATION.warnAndFix(configRules, emitWarn, isFixMode, "expected $expectedIndent but was ${indentError.actual}",
                 whiteSpace.startOffset + whiteSpace.text.lastIndexOf('\n') + 1, whiteSpace.node) {
@@ -328,15 +337,15 @@ class IndentationRule(configRules: List<RulesConfig>) : DiktatRule(
 
     companion object {
         const val INDENT_SIZE = 4
-        private val increasingTokens = listOf(LPAR, LBRACE, LBRACKET)
-        private val decreasingTokens = listOf(RPAR, RBRACE, RBRACKET)
+        private val increasingTokens = listOf(LPAR, LBRACE, LBRACKET, LONG_TEMPLATE_ENTRY_START)
+        private val decreasingTokens = listOf(RPAR, RBRACE, RBRACKET, LONG_TEMPLATE_ENTRY_END)
         private val matchingTokens = increasingTokens.zip(decreasingTokens)
     }
 }
 
 /**
  * @property expected expected indentation as a number of spaces
- * @property actual actial indentation as a number of spaces
+ * @property actual actual indentation as a number of spaces
  */
 internal data class IndentationError(val expected: Int, val actual: Int)
 
diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt
index 875df6755c..35b62e78aa 100644
--- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt
+++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/utils/indentation/Checkers.kt
@@ -6,6 +6,7 @@ package org.cqfn.diktat.ruleset.utils.indentation
 
 import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationError
 import org.cqfn.diktat.ruleset.rules.chapter3.files.lastIndent
+import org.cqfn.diktat.ruleset.utils.hasParent
 
 import com.pinterest.ktlint.core.ast.ElementType.ARROW
 import com.pinterest.ktlint.core.ast.ElementType.AS_KEYWORD
@@ -24,10 +25,12 @@ import com.pinterest.ktlint.core.ast.ElementType.IS_EXPRESSION
 import com.pinterest.ktlint.core.ast.ElementType.KDOC_END
 import com.pinterest.ktlint.core.ast.ElementType.KDOC_LEADING_ASTERISK
 import com.pinterest.ktlint.core.ast.ElementType.KDOC_SECTION
+import com.pinterest.ktlint.core.ast.ElementType.LONG_STRING_TEMPLATE_ENTRY
 import com.pinterest.ktlint.core.ast.ElementType.LPAR
 import com.pinterest.ktlint.core.ast.ElementType.OPERATION_REFERENCE
 import com.pinterest.ktlint.core.ast.ElementType.REFERENCE_EXPRESSION
 import com.pinterest.ktlint.core.ast.ElementType.SAFE_ACCESS
+import com.pinterest.ktlint.core.ast.ElementType.STRING_TEMPLATE
 import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_LIST
 import com.pinterest.ktlint.core.ast.ElementType.THEN
 import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT
@@ -194,16 +197,25 @@ internal class DotCallChecker(config: IndentationConfig) : CustomIndentationChec
         return false
     }
 
+    private fun ASTNode.isFromStringTemplate(): Boolean =
+            hasParent(LONG_STRING_TEMPLATE_ENTRY)
+
     @Suppress("ComplexMethod")
     override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? {
         whiteSpace.nextSibling.node
             .takeIf { nextNode ->
-                nextNode.isDotBeforeCallOrReference() ||
-                        nextNode.elementType == OPERATION_REFERENCE && nextNode.firstChildNode.elementType.let {
-                            it == ELVIS || it == IS_EXPRESSION || it == AS_KEYWORD || it == AS_SAFE
-                        } || nextNode.isCommentBeforeDot()
+                (nextNode.isDotBeforeCallOrReference() ||
+                        nextNode.elementType == OPERATION_REFERENCE && nextNode.firstChildNode.elementType.let { type ->
+                            type == ELVIS || type == IS_EXPRESSION || type == AS_KEYWORD || type == AS_SAFE
+                        } || nextNode.isCommentBeforeDot()) && whiteSpace.parents.none { it.node.elementType == LONG_STRING_TEMPLATE_ENTRY }
             }
-            ?.let {
+            ?.let { node ->
+                if (node.isFromStringTemplate()) {
+                    val template = node.parents().takeWhile { it.elementType != STRING_TEMPLATE }.last()
+                    return CheckResult.from(indentError.actual, indentError.expected +
+                            (if (configuration.extendedIndentBeforeDot) 2 else 1) * configuration.indentationSize, true)
+                }
+
                 // we need to get indent before the first expression in calls chain
                 return CheckResult.from(indentError.actual, (whiteSpace.run {
                     parents.takeWhile { it is KtDotQualifiedExpression || it is KtSafeQualifiedExpression }.lastOrNull() ?: this


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt
index 5d803c7965..d73159b3ca 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleWarnTest.kt
@@ -606,5 +606,30 @@ class IndentationRuleWarnTest : LintTestBase(::IndentationRule) {
         )
     }
 
+    @Test
+    @Tag(WarningNames.WRONG_INDENTATION)
+    fun `should trigger on string templates starting with new line`() {
+        lintMethod(
+            """
+                |fun foo(some: String) {
+                |    fun bar() {
+                |        val a = "${'$'}{
+                |        expression
+                |            .foo()
+                |            .bar()
+                |        }"
+                |    }
+                |    
+                |    val b = "${'$'}{ foo().bar() }"
+                |}
+                |
+            """.trimMargin(),
+            LintError(4, 1, ruleId, warnText(12, 8), true),
+            LintError(5, 1, ruleId, warnText(16, 12), true),
+            LintError(6, 1, ruleId, warnText(16, 12), true),
+            rulesConfigList = rulesConfigList
+        )
+    }
+
     private fun warnText(expected: Int, actual: Int) = "${WRONG_INDENTATION.warnText()} expected $expected but was $actual"
 }
diff --git a/diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Expected.kt b/diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Expected.kt
index 53f92028ba..d1bca4541b 100644
--- a/diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Expected.kt
+++ b/diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Expected.kt
@@ -32,5 +32,19 @@ data class Example(val field1: Type1,
         else
             foobaz()
     }
+
+    fun some() {
+        val a = "${
+            foo().bar()
+        }"
+
+        val b = "${baz().foo()}"
+
+        val c = "${
+            expression
+                .foo()
+                .bar()
+        }"
+    }
 }
 
diff --git a/diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Test.kt b/diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Test.kt
index 0174a58413..2eb1ab279e 100644
--- a/diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Test.kt
+++ b/diktat-rules/src/test/resources/test/paragraph3/indentation/IndentationFull1Test.kt
@@ -32,4 +32,18 @@ fun foo(
         else
         foobaz()
     }
+
+    fun some() {
+        val a = "${
+        foo().bar()
+        }"
+
+        val b = "${baz().foo()}"
+
+        val c = "${
+        expression
+            .foo()
+            .bar()
+        }"
+    }
 }