Repository: analysis-dev/diktat (kotlin)

Issue:
`WRONG_INDENTATION`: aligned chained method calls reported as false positives
Consider this code example:

```kotlin
fun LocalDateTime.updateTime(
    hour: Int? = null,
    minute: Int? = null,
    second: Int? = null,
): LocalDateTime = withHour(hour ?: getHour())
    .withMinute(minute ?: getMinute())
    .withSecond(second ?: getSecond())
```

![1336](https://user-images.githubusercontent.com/73111822/171164029-d62b0667-75e1-4117-8ac3-4a4d31efefa6.svg)

Chained method calls in lines 6-7 are normally aligned (e. g.: IDEA does so). Yet, DiKTat reports this as an error, suggesting the following formatting. `extendedIndentBeforeDot` **on** (the default):

```kotlin
fun LocalDateTime.updateTime(
    hour: Int? = null,
    minute: Int? = null,
    second: Int? = null,
): LocalDateTime = withHour(hour ?: getHour())
        .withMinute(minute ?: getMinute())
                .withSecond(second ?: getSecond())
```

&mdash; and `extendedIndentBeforeDot` **off**:

```kotlin
fun LocalDateTime.updateTime(
    hour: Int? = null,
    minute: Int? = null,
    second: Int? = null,
): LocalDateTime = withHour(hour ?: getHour())
    .withMinute(minute ?: getMinute())
        .withSecond(second ?: getSecond())
```

The corresponding IDEA code style flag is `CONTINUATION_INDENT_FOR_CHAINED_CALLS`, **on** by default.

Pull request description:
### What's done:

 * This fixes #1336

## Which rule and warnings did you add?

## Actions checklist
* [X] Added tests on checks
* [X] Added tests on fixers

Patch:
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 5ab0f78d31..7d8457cdee 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
@@ -213,18 +213,33 @@ internal class DotCallChecker(config: IndentationConfig) : CustomIndentationChec
                     } || nextNode.isCommentBeforeDot()) && whiteSpace.parents.none { it.node.elementType == LONG_STRING_TEMPLATE_ENTRY }
             }
             ?.let { node ->
+                val indentIncrement = (if (configuration.extendedIndentBeforeDot) 2 else 1) * configuration.indentationSize
                 if (node.isFromStringTemplate()) {
                     return CheckResult.from(indentError.actual, indentError.expected +
-                        (if (configuration.extendedIndentBeforeDot) 2 else 1) * configuration.indentationSize, true)
+                        indentIncrement, true)
                 }
 
                 // we need to get indent before the first expression in calls chain
-                return CheckResult.from(indentError.actual, (whiteSpace.run {
+                /*-
+                 * If the parent indent (the one before a `DOT_QUALIFIED_EXPRESSION`
+                 * or a `SAFE_ACCESS_EXPRESSION`) is `null`, then use 0 as the
+                 * fallback value.
+                 *
+                 * If `indentError.expected` is used as a fallback (pre-1.2.2
+                 * behaviour), this breaks chained dot-qualified or safe-access
+                 * expressions (see #1336), e.g.:
+                 *
+                 * ```kotlin
+                 * val a = first()
+                 *     .second()
+                 *     .third()
+                 * ```
+                 */
+                val parentIndent = whiteSpace.run {
                     parents.takeWhile { it is KtDotQualifiedExpression || it is KtSafeQualifiedExpression }.lastOrNull() ?: this
-                }
-                    .parentIndent()
-                    ?: indentError.expected) +
-                    (if (configuration.extendedIndentBeforeDot) 2 else 1) * configuration.indentationSize, true)
+                }.parentIndent() ?: 0
+                val expectedIndent = parentIndent + indentIncrement
+                return CheckResult.from(indentError.actual, expectedIndent, true)
             }
         return null
     }


Test patch:
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt
index 2117f95773..9e744752b9 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleFixTest.kt
@@ -8,6 +8,7 @@ import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.assertNo
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.describe
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.extendedIndent
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.withCustomParameters
+import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.dotQualifiedExpressions
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.expressionBodyFunctions
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.expressionsWrappedAfterOperator
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.parenthesesSurroundedInfixExpressions
@@ -248,4 +249,38 @@ class IndentationRuleFixTest : FixTestBase("test/paragraph3/indentation",
                 rulesConfigList = customConfig.asRulesConfigList())
         }
     }
+
+    /**
+     * See [#1336](https://github.com/saveourtool/diktat/issues/1336).
+     */
+    @Nested
+    @TestMethodOrder(DisplayName::class)
+    inner class `Dot- and safe-qualified expressions` {
+        @ParameterizedTest(name = "extendedIndentBeforeDot = {0}")
+        @ValueSource(booleans = [false, true])
+        @Tag(WarningNames.WRONG_INDENTATION)
+        fun `should be properly indented`(extendedIndentBeforeDot: Boolean, @TempDir tempDir: Path) {
+            val defaultConfig = IndentationConfig("newlineAtEnd" to false)
+            val customConfig = defaultConfig.withCustomParameters("extendedIndentBeforeDot" to extendedIndentBeforeDot)
+
+            lintMultipleMethods(
+                dotQualifiedExpressions[extendedIndentBeforeDot].assertNotNull(),
+                tempDir = tempDir,
+                rulesConfigList = customConfig.asRulesConfigList())
+        }
+
+        @ParameterizedTest(name = "extendedIndentBeforeDot = {0}")
+        @ValueSource(booleans = [false, true])
+        @Tag(WarningNames.WRONG_INDENTATION)
+        fun `should be reformatted if mis-indented`(extendedIndentBeforeDot: Boolean, @TempDir tempDir: Path) {
+            val defaultConfig = IndentationConfig("newlineAtEnd" to false)
+            val customConfig = defaultConfig.withCustomParameters("extendedIndentBeforeDot" to extendedIndentBeforeDot)
+
+            lintMultipleMethods(
+                actualContent = dotQualifiedExpressions[!extendedIndentBeforeDot].assertNotNull(),
+                expectedContent = dotQualifiedExpressions[extendedIndentBeforeDot].assertNotNull(),
+                tempDir = tempDir,
+                rulesConfigList = customConfig.asRulesConfigList())
+        }
+    }
 }
diff --git a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestResources.kt b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestResources.kt
index 9cacd40b54..277b96917f 100644
--- a/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestResources.kt
+++ b/diktat-rules/src/test/kotlin/org/cqfn/diktat/ruleset/chapter3/spaces/IndentationRuleTestResources.kt
@@ -979,6 +979,161 @@ internal object IndentationRuleTestResources {
         false to parenthesesSurroundedInfixExpressionsSingleIndent,
         true to parenthesesSurroundedInfixExpressionsContinuationIndent)
 
+    /**
+     * Dot-qualified and safe-access expressions, single indent
+     * (`extendedIndentBeforeDot` is **off**).
+     *
+     * When adding new code fragments to this list, be sure to also add their
+     * counterparts (preserving order) to
+     * [dotQualifiedExpressionsContinuationIndent].
+     *
+     * See [#1336](https://github.com/saveourtool/diktat/issues/1336).
+     *
+     * @see dotQualifiedExpressionsContinuationIndent
+     */
+    @Language("kotlin")
+    val dotQualifiedExpressionsSingleIndent = arrayOf(
+        """
+        |fun LocalDateTime.updateTime(
+        |    hour: Int? = null,
+        |    minute: Int? = null,
+        |    second: Int? = null,
+        |): LocalDateTime = withHour(hour ?: getHour())
+        |    .withMinute(minute ?: getMinute())
+        |    .withSecond(second ?: getSecond())
+        """.trimMargin(),
+
+        """
+        |fun f() {
+        |    first()
+        |        .second()
+        |        .third()
+        |}
+        """.trimMargin(),
+
+        """
+        |val a = first()
+        |    .second()
+        |    .third()
+        """.trimMargin(),
+
+        """
+        |val b = first()
+        |    ?.second()
+        |    ?.third()
+        """.trimMargin(),
+
+        """
+        |fun f1() = first()
+        |    .second()
+        |    .third()
+        """.trimMargin(),
+
+        """
+        |fun f2() =
+        |    first()
+        |        .second()
+        |        .third()
+        """.trimMargin(),
+
+        """
+        |fun f3() = g(first()
+        |    .second()
+        |    .third()
+        |    .fourth())
+        """.trimMargin(),
+
+        """
+        |fun f4() = g(
+        |    first()
+        |        .second()
+        |        .third()
+        |        .fourth())
+        """.trimMargin(),
+    )
+
+    /**
+     * Dot-qualified and safe-access expressions, continuation indent
+     * (`extendedIndentBeforeDot` is **on**).
+     *
+     * When adding new code fragments to this list, be sure to also add their
+     * counterparts (preserving order) to
+     * [dotQualifiedExpressionsSingleIndent].
+     *
+     * See [#1336](https://github.com/saveourtool/diktat/issues/1336).
+     *
+     * @see dotQualifiedExpressionsSingleIndent
+     */
+    @Language("kotlin")
+    val dotQualifiedExpressionsContinuationIndent = arrayOf(
+        """
+        |fun LocalDateTime.updateTime(
+        |    hour: Int? = null,
+        |    minute: Int? = null,
+        |    second: Int? = null,
+        |): LocalDateTime = withHour(hour ?: getHour())
+        |        .withMinute(minute ?: getMinute())
+        |        .withSecond(second ?: getSecond())
+        """.trimMargin(),
+
+        """
+        |fun f() {
+        |    first()
+        |            .second()
+        |            .third()
+        |}
+        """.trimMargin(),
+
+        """
+        |val a = first()
+        |        .second()
+        |        .third()
+        """.trimMargin(),
+
+        """
+        |val b = first()
+        |        ?.second()
+        |        ?.third()
+        """.trimMargin(),
+
+        """
+        |fun f1() = first()
+        |        .second()
+        |        .third()
+        """.trimMargin(),
+
+        """
+        |fun f2() =
+        |    first()
+        |            .second()
+        |            .third()
+        """.trimMargin(),
+
+        """
+        |fun f3() = g(first()
+        |        .second()
+        |        .third()
+        |        .fourth())
+        """.trimMargin(),
+
+        """
+        |fun f4() = g(
+        |    first()
+        |            .second()
+        |            .third()
+        |            .fourth())
+        """.trimMargin(),
+    )
+
+    /**
+     * Dot-qualified and safe-access expressions.
+     *
+     * See [#1336](https://github.com/saveourtool/diktat/issues/1336).
+     */
+    val dotQualifiedExpressions = mapOf(
+        false to dotQualifiedExpressionsSingleIndent,
+        true to dotQualifiedExpressionsContinuationIndent)
+
     @Language("kotlin")
     @Suppress("COMMENT_WHITE_SPACE")
     private val ifExpressionsSingleIndent = arrayOf(
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 2892cfde57..fdda961e50 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
@@ -8,6 +8,7 @@ import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.asSequen
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.assertNotNull
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.describe
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.withCustomParameters
+import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.dotQualifiedExpressions
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.expressionBodyFunctions
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.expressionsWrappedAfterOperator
 import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.ifExpressions
@@ -15,6 +16,7 @@ import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.pare
 import org.cqfn.diktat.ruleset.constants.Warnings.WRONG_INDENTATION
 import org.cqfn.diktat.ruleset.rules.chapter3.files.IndentationRule
 import org.cqfn.diktat.util.LintTestBase
+
 import com.pinterest.ktlint.core.LintError
 import generated.WarningNames
 import org.assertj.core.api.AbstractSoftAssertions
@@ -29,6 +31,7 @@ import org.junit.jupiter.api.TestMethodOrder
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.ValueSource
 import org.opentest4j.MultipleFailuresError
+
 import java.util.function.Consumer
 
 @Suppress("LargeClass")
@@ -936,6 +939,48 @@ class IndentationRuleWarnTest : LintTestBase(::IndentationRule) {
         }
     }
 
+    /**
+     * See [#1336](https://github.com/saveourtool/diktat/issues/1336).
+     */
+    @Nested
+    @TestMethodOrder(DisplayName::class)
+    inner class `Dot- and safe-qualified expressions` {
+        @ParameterizedTest(name = "extendedIndentBeforeDot = {0}")
+        @ValueSource(booleans = [false, true])
+        @Tag(WarningNames.WRONG_INDENTATION)
+        fun `should be properly indented`(extendedIndentBeforeDot: Boolean) {
+            val defaultConfig = IndentationConfig("newlineAtEnd" to false)
+            val customConfig = defaultConfig.withCustomParameters("extendedIndentBeforeDot" to extendedIndentBeforeDot)
+
+            lintMultipleMethods(
+                dotQualifiedExpressions[extendedIndentBeforeDot].assertNotNull(),
+                lintErrors = emptyArray(),
+                customConfig.asRulesConfigList()
+            )
+        }
+
+        @ParameterizedTest(name = "extendedIndentBeforeDot = {0}")
+        @ValueSource(booleans = [false, true])
+        @Tag(WarningNames.WRONG_INDENTATION)
+        fun `should be reported if mis-indented`(extendedIndentBeforeDot: Boolean) {
+            val defaultConfig = IndentationConfig("newlineAtEnd" to false)
+            val customConfig = defaultConfig.withCustomParameters("extendedIndentBeforeDot" to extendedIndentBeforeDot)
+
+            assertSoftly { softly ->
+                dotQualifiedExpressions[!extendedIndentBeforeDot].assertNotNull().forEach { code ->
+                    softly.assertThat(lintResult(code, customConfig.asRulesConfigList()))
+                        .describedAs("lint result for ${code.describe()}")
+                        .isNotEmpty
+                        .hasSizeBetween(2, 3).allSatisfy(Consumer { lintError ->
+                            assertThat(lintError.ruleId).describedAs("ruleId").isEqualTo(ruleId)
+                            assertThat(lintError.canBeAutoCorrected).describedAs("canBeAutoCorrected").isTrue
+                            assertThat(lintError.detail).matches(warnTextRegex)
+                        })
+                }
+            }
+        }
+    }
+
     @Nested
     @TestMethodOrder(DisplayName::class)
     inner class `If expressions` {