From 47668357ec422e853311ac83e09b5cc4bf18f20f Mon Sep 17 00:00:00 2001 From: Andrey Shcheglov Date: Fri, 29 Jul 2022 16:31:57 +0000 Subject: [PATCH] Properly format expressions wrapped *before* a binary operator (#1486) ### What's done: * Now, the indentation in binary expressions wrapped *before* a binary operator or an infix function is also controlled with `extendedIndentAfterOperators`. * The above is also true for `as` and `as?` operators. * The only exclusion is the Elvis operator (`?:`). * Fixes #1340. --- .../rules/chapter2/kdoc/KdocMethods.kt | 2 +- .../ruleset/rules/chapter4/SmartCastRule.kt | 2 +- .../cqfn/diktat/ruleset/utils/AstNodeUtils.kt | 2 +- .../ruleset/utils/indentation/Checkers.kt | 21 +- .../chapter3/spaces/IndentationRuleFixTest.kt | 35 ++ .../spaces/IndentationRuleTestResources.kt | 531 ++++++++++++++++++ .../spaces/IndentationRuleWarnTest.kt | 47 +- 7 files changed, 634 insertions(+), 6 deletions(-) diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/KdocMethods.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/KdocMethods.kt index f9c1abc302..24e7dfe288 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/KdocMethods.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter2/kdoc/KdocMethods.kt @@ -170,7 +170,7 @@ class KdocMethods(configRules: List) : DiktatRule( val isReferenceExpressionWithSameName = node.findAllDescendantsWithSpecificType(REFERENCE_EXPRESSION).map { it.text }.contains((node.psi as KtFunction).name) val hasReturnKdoc = kdocTags != null && kdocTags.hasKnownKdocTag(KDocKnownTag.RETURN) return (hasExplicitNotUnitReturnType || isFunWithExpressionBody && !hasExplicitUnitReturnType && hasNotExpressionBodyTypes) - && !hasReturnKdoc && !isReferenceExpressionWithSameName + && !hasReturnKdoc && !isReferenceExpressionWithSameName } private fun getExplicitlyThrownExceptions(node: ASTNode): Set { diff --git a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter4/SmartCastRule.kt b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter4/SmartCastRule.kt index 45bae7aa8b..943fc6678d 100644 --- a/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter4/SmartCastRule.kt +++ b/diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/chapter4/SmartCastRule.kt @@ -126,7 +126,7 @@ class SmartCastRule(configRules: List) : DiktatRule( val list: MutableList = mutableListOf() asExpr.forEach { asCall -> if (asCall.node.findParentNodeWithSpecificType(IF) - == it.node.findParentNodeWithSpecificType(IF)) { + == it.node.findParentNodeWithSpecificType(IF)) { list.add(asCall) } } 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 348da4a767..a6c6fdf8dd 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 @@ -930,7 +930,7 @@ private fun ASTNode.hasExplicitIt(): Boolean { val parameterList = findChildByType(ElementType.FUNCTION_LITERAL) ?.findChildByType(ElementType.VALUE_PARAMETER_LIST) ?.psi - as KtParameterList? + as KtParameterList? return parameterList?.parameters ?.any { it.name == "it" } ?: false 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 99ebd2458d..e869c7247f 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 @@ -14,6 +14,7 @@ import com.pinterest.ktlint.core.ast.ElementType.ARROW import com.pinterest.ktlint.core.ast.ElementType.AS_KEYWORD import com.pinterest.ktlint.core.ast.ElementType.AS_SAFE import com.pinterest.ktlint.core.ast.ElementType.BINARY_EXPRESSION +import com.pinterest.ktlint.core.ast.ElementType.BINARY_WITH_TYPE import com.pinterest.ktlint.core.ast.ElementType.BLOCK_COMMENT import com.pinterest.ktlint.core.ast.ElementType.BODY import com.pinterest.ktlint.core.ast.ElementType.CALL_EXPRESSION @@ -39,6 +40,7 @@ import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT_LIST import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE +import com.pinterest.ktlint.core.ast.children import com.pinterest.ktlint.core.ast.nextCodeSibling import com.pinterest.ktlint.core.ast.prevSibling import org.jetbrains.kotlin.com.intellij.lang.ASTNode @@ -138,7 +140,17 @@ internal class ValueParameterListChecker(configuration: IndentationConfig) : Cus internal class ExpressionIndentationChecker(configuration: IndentationConfig) : CustomIndentationChecker(configuration) { override fun checkNode(whiteSpace: PsiWhiteSpace, indentError: IndentationError): CheckResult? = when { - whiteSpace.parent.node.elementType == BINARY_EXPRESSION && whiteSpace.prevSibling.node.elementType == OPERATION_REFERENCE -> { + whiteSpace.parent.node.elementType in sequenceOf(BINARY_EXPRESSION, BINARY_WITH_TYPE) && + whiteSpace.immediateSiblings().any { sibling -> + /* + * We're looking for an operation reference, including + * `as` and `as?` (`AS_SAFE`), but excluding `?:` (`ELVIS`), + * because there's a separate flag for Elvis expressions + * in IDEA (`CONTINUATION_INDENT_IN_ELVIS`). + */ + sibling.node.elementType == OPERATION_REFERENCE && + sibling.node.children().firstOrNull()?.elementType != ELVIS + } -> { val parentIndent = whiteSpace.parentIndent() ?: indentError.expected val expectedIndent = parentIndent + IndentationAmount.valueOf(configuration.extendedIndentAfterOperators) CheckResult.from(indentError.actual, expectedIndent, true) @@ -308,3 +320,10 @@ internal fun PsiElement.parentIndent(): Int? = parentsWithSelf .firstOrNull() ?.text ?.lastIndent() + +/** + * @return the sequence of immediate siblings (the previous and the next one), + * excluding `null`'s. + */ +private fun PsiElement.immediateSiblings(): Sequence = + sequenceOf(prevSibling, nextSibling).filterNotNull() 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 1359a90e93..81ab97c7f8 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 @@ -11,6 +11,7 @@ import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.withCust 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.expressionsWrappedBeforeOperator import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.parenthesesSurroundedInfixExpressions import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.whitespaceInStringLiterals import org.cqfn.diktat.ruleset.constants.Warnings.WRONG_INDENTATION @@ -225,6 +226,40 @@ class IndentationRuleFixTest : FixTestBase("test/paragraph3/indentation", } } + /** + * See [#1340](https://github.com/saveourtool/diktat/issues/1340). + */ + @Nested + @TestMethodOrder(DisplayName::class) + inner class `Expressions wrapped before operator` { + @ParameterizedTest(name = "$EXTENDED_INDENT_AFTER_OPERATORS = {0}") + @ValueSource(booleans = [false, true]) + @Tag(WarningNames.WRONG_INDENTATION) + fun `should be properly indented`(extendedIndentAfterOperators: Boolean, @TempDir tempDir: Path) { + val defaultConfig = IndentationConfig(NEWLINE_AT_END to false) + val customConfig = defaultConfig.withCustomParameters(EXTENDED_INDENT_AFTER_OPERATORS to extendedIndentAfterOperators) + + lintMultipleMethods( + expressionsWrappedBeforeOperator[extendedIndentAfterOperators].assertNotNull(), + tempDir = tempDir, + rulesConfigList = customConfig.asRulesConfigList()) + } + + @ParameterizedTest(name = "$EXTENDED_INDENT_AFTER_OPERATORS = {0}") + @ValueSource(booleans = [false, true]) + @Tag(WarningNames.WRONG_INDENTATION) + fun `should be reformatted if mis-indented`(extendedIndentAfterOperators: Boolean, @TempDir tempDir: Path) { + val defaultConfig = IndentationConfig(NEWLINE_AT_END to false) + val customConfig = defaultConfig.withCustomParameters(EXTENDED_INDENT_AFTER_OPERATORS to extendedIndentAfterOperators) + + lintMultipleMethods( + actualContent = expressionsWrappedBeforeOperator[!extendedIndentAfterOperators].assertNotNull(), + expectedContent = expressionsWrappedBeforeOperator[extendedIndentAfterOperators].assertNotNull(), + tempDir = tempDir, + rulesConfigList = customConfig.asRulesConfigList()) + } + } + /** * See [#1409](https://github.com/saveourtool/diktat/issues/1409). */ 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 f7ddad9dcb..24f95506da 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 @@ -605,6 +605,29 @@ internal object IndentationRuleTestResources { | 2 to | 3)) """.trimMargin(), + + """ + |fun f() { + | g(42 as + | Integer) + |} + """.trimMargin(), + + """ + |fun f() { + | g("" as? + | String?) + |} + """.trimMargin(), + + """ + |fun f() { + | // The dot-qualified expression is always single-indented. + | "" + | .length as + | Int + |} + """.trimMargin(), ) /** @@ -842,6 +865,29 @@ internal object IndentationRuleTestResources { | 2 to | 3)) """.trimMargin(), + + """ + |fun f() { + | g(42 as + | Integer) + |} + """.trimMargin(), + + """ + |fun f() { + | g("" as? + | String?) + |} + """.trimMargin(), + + """ + |fun f() { + | // The dot-qualified expression is always single-indented. + | "" + | .length as + | Int + |} + """.trimMargin(), ) /** @@ -853,6 +899,491 @@ internal object IndentationRuleTestResources { false to expressionsWrappedAfterOperatorSingleIndent, true to expressionsWrappedAfterOperatorContinuationIndent) + /** + * Expressions wrapped before an operator or an infix function, single + * indent (`extendedIndentAfterOperators` is **off**). + * + * When adding new code fragments to this list, be sure to also add their + * counterparts (preserving order) to [expressionsWrappedBeforeOperatorContinuationIndent]. + * + * See [#1340](https://github.com/saveourtool/diktat/issues/1340). + * + * @see expressionsWrappedBeforeOperatorContinuationIndent + */ + @Language("kotlin") + private val expressionsWrappedBeforeOperatorSingleIndent = arrayOf( + """ + |fun f() { + | systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE + | or View.SYSTEM_UI_FLAG_FULLSCREEN + | or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) + |} + """.trimMargin(), + + """ + |val systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE + | or View.SYSTEM_UI_FLAG_FULLSCREEN + | or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) + """.trimMargin(), + + """ + |const val FOO = 1 + | + |const val BAR = 2 + | + |const val BAZ = 4 + | + |fun acceptInteger(arg: Int) = Unit + | + |fun main() { + | acceptInteger(FOO or BAR or BAZ or FOO or BAR or BAZ + | or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ + | or FOO or BAR or BAZ) + |} + """.trimMargin(), + + """ + |const val TRUE = true + | + |const val FALSE = false + | + |fun acceptBoolean(arg: Boolean) = Unit + | + |fun f() { + | acceptBoolean(TRUE + | || FALSE + | || TRUE) + |} + """.trimMargin(), + + """ + |val c = (3 + | + 2) + """.trimMargin(), + + """ + |infix fun Int.combineWith(that: Int) = this + that + | + |fun f() { + | val x = (1 + | combineWith 2 + | combineWith 3 + | combineWith 4 + | combineWith 5 + | combineWith 6) + |} + """.trimMargin(), + + """ + |fun f(i1: Int, i2: Int, i3: Int): Int { + | if (i2 > 0 + | && i3 < 0) { + | return 2 + | } + | return 0 + |} + """.trimMargin(), + + """ + |val value1 = (1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |val value2 = + | (1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value3 = identity(1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value4 = identity( + | 1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value5 = + | identity(1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |/** + | * Line breaks: + | * + | * 1. before the expression body (`=`), + | * 2. before the effective function arguments, and + | * 3. on each infix function call ([to]). + | */ + |val value6 = + | identity( + | 1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value7 = identity(identity(1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value8 = identity(identity( + | 1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value9 = + | identity(identity(1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value10 = + | identity(identity( + | 1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |// Same as above, but using a custom getter instead of an explicit initializer. + |val value11 + | get() = + | (1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |// Same as above, but using a custom getter instead of an explicit initializer. + |val value12 + | get() = + | identity(1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |// Same as above, but using a custom getter instead of an explicit initializer. + |val value13 + | get() = + | identity(identity(1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |fun f() { + | g(42 + | as Integer) + |} + """.trimMargin(), + + """ + |fun f() { + | g("" + | as? String?) + |} + """.trimMargin(), + + """ + |fun f() { + | // The dot-qualified expression is always single-indented. + | "" + | .length + | as Int + |} + """.trimMargin(), + ) + + /** + * Expressions wrapped before an operator or an infix function, continuation + * indent (`extendedIndentAfterOperators` is **on**). + * + * When adding new code fragments to this list, be sure to also add their + * counterparts (preserving order) to [expressionsWrappedBeforeOperatorSingleIndent]. + * + * See [#1340](https://github.com/saveourtool/diktat/issues/1340). + * + * @see expressionsWrappedBeforeOperatorSingleIndent + */ + @Language("kotlin") + private val expressionsWrappedBeforeOperatorContinuationIndent = arrayOf( + """ + |fun f() { + | systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE + | or View.SYSTEM_UI_FLAG_FULLSCREEN + | or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) + |} + """.trimMargin(), + + """ + |val systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE + | or View.SYSTEM_UI_FLAG_FULLSCREEN + | or View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) + """.trimMargin(), + + """ + |const val FOO = 1 + | + |const val BAR = 2 + | + |const val BAZ = 4 + | + |fun acceptInteger(arg: Int) = Unit + | + |fun main() { + | acceptInteger(FOO or BAR or BAZ or FOO or BAR or BAZ + | or FOO or BAR or BAZ or FOO or BAR or BAZ or FOO or BAR or BAZ + | or FOO or BAR or BAZ) + |} + """.trimMargin(), + + """ + |const val TRUE = true + | + |const val FALSE = false + | + |fun acceptBoolean(arg: Boolean) = Unit + | + |fun f() { + | acceptBoolean(TRUE + | || FALSE + | || TRUE) + |} + """.trimMargin(), + + """ + |val c = (3 + | + 2) + """.trimMargin(), + + """ + |infix fun Int.combineWith(that: Int) = this + that + | + |fun f() { + | val x = (1 + | combineWith 2 + | combineWith 3 + | combineWith 4 + | combineWith 5 + | combineWith 6) + |} + """.trimMargin(), + + """ + |fun f(i1: Int, i2: Int, i3: Int): Int { + | if (i2 > 0 + | && i3 < 0) { + | return 2 + | } + | return 0 + |} + """.trimMargin(), + + """ + |val value1 = (1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |val value2 = + | (1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value3 = identity(1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value4 = identity( + | 1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value5 = + | identity(1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |/** + | * Line breaks: + | * + | * 1. before the expression body (`=`), + | * 2. before the effective function arguments, and + | * 3. on each infix function call ([to]). + | */ + |val value6 = + | identity( + | 1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value7 = identity(identity(1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value8 = identity(identity( + | 1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value9 = + | identity(identity(1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |val value10 = + | identity(identity( + | 1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |// Same as above, but using a custom getter instead of an explicit initializer. + |val value11 + | get() = + | (1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |// Same as above, but using a custom getter instead of an explicit initializer. + |val value12 + | get() = + | identity(1 + | to 2 + | to 3) + """.trimMargin(), + + """ + |fun identity(t: T): T = t + | + |// Same as above, but using a custom getter instead of an explicit initializer. + |val value13 + | get() = + | identity(identity(1 + | to 2 + | to 3)) + """.trimMargin(), + + """ + |fun f() { + | g(42 + | as Integer) + |} + """.trimMargin(), + + """ + |fun f() { + | g("" + | as? String?) + |} + """.trimMargin(), + + """ + |fun f() { + | // The dot-qualified expression is always single-indented. + | "" + | .length + | as Int + |} + """.trimMargin(), + ) + + /** + * Expressions wrapped before an operator or an infix function. + * + * See [#1340](https://github.com/saveourtool/diktat/issues/1340). + */ + val expressionsWrappedBeforeOperator = mapOf( + false to expressionsWrappedBeforeOperatorSingleIndent, + true to expressionsWrappedBeforeOperatorContinuationIndent) + /** * Parenthesized expressions, `extendedIndentForExpressionBodies` is **off**, * `extendedIndentAfterOperators` is **on**. 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 4de250a065..00dedf277c 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 @@ -11,6 +11,7 @@ import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestMixin.withCust 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.expressionsWrappedBeforeOperator import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.ifExpressions import org.cqfn.diktat.ruleset.chapter3.spaces.IndentationRuleTestResources.parenthesesSurroundedInfixExpressions import org.cqfn.diktat.ruleset.constants.Warnings.WRONG_INDENTATION @@ -329,8 +330,8 @@ class IndentationRuleWarnTest : LintTestBase(::IndentationRule) { | | bar | .baz() - | as Baz - | as? Baz + | as Baz + | as? Baz |} | """.trimMargin() @@ -909,6 +910,48 @@ class IndentationRuleWarnTest : LintTestBase(::IndentationRule) { } } + /** + * See [#1340](https://github.com/saveourtool/diktat/issues/1340). + */ + @Nested + @TestMethodOrder(DisplayName::class) + inner class `Expressions wrapped before operator` { + @ParameterizedTest(name = "$EXTENDED_INDENT_AFTER_OPERATORS = {0}") + @ValueSource(booleans = [false, true]) + @Tag(WarningNames.WRONG_INDENTATION) + fun `should be properly indented`(extendedIndentAfterOperators: Boolean) { + val defaultConfig = IndentationConfig(NEWLINE_AT_END to false) + val customConfig = defaultConfig.withCustomParameters(EXTENDED_INDENT_AFTER_OPERATORS to extendedIndentAfterOperators) + + lintMultipleMethods( + expressionsWrappedBeforeOperator[extendedIndentAfterOperators].assertNotNull(), + lintErrors = emptyArray(), + customConfig.asRulesConfigList() + ) + } + + @ParameterizedTest(name = "$EXTENDED_INDENT_AFTER_OPERATORS = {0}") + @ValueSource(booleans = [false, true]) + @Tag(WarningNames.WRONG_INDENTATION) + fun `should be reported if mis-indented`(extendedIndentAfterOperators: Boolean) { + val defaultConfig = IndentationConfig(NEWLINE_AT_END to false) + val customConfig = defaultConfig.withCustomParameters(EXTENDED_INDENT_AFTER_OPERATORS to extendedIndentAfterOperators) + + assertSoftly { softly -> + expressionsWrappedBeforeOperator[!extendedIndentAfterOperators].assertNotNull().forEach { code -> + softly.assertThat(lintResult(code, customConfig.asRulesConfigList())) + .describedAs("lint result for ${code.describe()}") + .isNotEmpty + .hasSizeBetween(1, 5).allSatisfy(Consumer { lintError -> + assertThat(lintError.ruleId).describedAs("ruleId").isEqualTo(ruleId) + assertThat(lintError.canBeAutoCorrected).describedAs("canBeAutoCorrected").isTrue + assertThat(lintError.detail).matches(warnTextRegex) + }) + } + } + } + } + /** * See [#1409](https://github.com/saveourtool/diktat/issues/1409). */