diff --git a/CHANGELOG.md b/CHANGELOG.md index e1460ff7a9..9266dec834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Do not force blank line before function in right hand side of assignment `blank-line-before-declaration` [#2260](https://github.com/pinterest/ktlint/issue/2260) * Ignore override of function in rule `function-naming` [#2271](https://github.com/pinterest/ktlint/issue/2271) * Do not replace function body having a return statement only in case the return statement contains an intermediate exit point 'function-expression-body' [#2269](https://github.com/pinterest/ktlint/issue/2269) +* Prevent wrapping of nested multiline binary expression before operation reference as it results in a compilation error `multiline-expression-wrapping` [#2286](https://github.com/pinterest/ktlint/issue/2286) ### Changed diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressionWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressionWrappingRule.kt index e5b79bdca6..e42612f564 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressionWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressionWrappingRule.kt @@ -23,6 +23,8 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType.RPAR import com.pinterest.ktlint.rule.engine.core.api.ElementType.SAFE_ACCESS_EXPRESSION import com.pinterest.ktlint.rule.engine.core.api.ElementType.TRY import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT +import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST +import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_PARAMETER_LIST import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHEN import com.pinterest.ktlint.rule.engine.core.api.IndentConfig import com.pinterest.ktlint.rule.engine.core.api.IndentConfig.Companion.DEFAULT_INDENT_CONFIG @@ -36,11 +38,13 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPER import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithoutNewline import com.pinterest.ktlint.rule.engine.core.api.lastChildLeafOrSelf import com.pinterest.ktlint.rule.engine.core.api.leavesIncludingSelf import com.pinterest.ktlint.rule.engine.core.api.nextLeaf +import com.pinterest.ktlint.rule.engine.core.api.nextSibling import com.pinterest.ktlint.rule.engine.core.api.prevCodeLeaf import com.pinterest.ktlint.rule.engine.core.api.prevCodeSibling import com.pinterest.ktlint.rule.engine.core.api.prevLeaf @@ -108,11 +112,39 @@ public class MultilineExpressionWrappingRule : emit(node.startOffset, "A multiline expression should start on a new line", true) if (autoCorrect) { node.upsertWhitespaceBeforeMe(indentConfig.siblingIndentOf(node)) - node - .lastChildLeafOrSelf() - .nextLeaf { !it.isWhiteSpaceWithoutNewline() && !it.isPartOfComment() && it.elementType != COMMA } - ?.takeIf { !it.isWhiteSpaceWithNewline() } - ?.upsertWhitespaceBeforeMe(indentConfig.siblingIndentOf(node)) + val leafOnSameLineAfterMultilineExpression = + node + .lastChildLeafOrSelf() + .nextLeaf { !it.isWhiteSpaceWithoutNewline() && !it.isPartOfComment() } + ?.takeIf { !it.isWhiteSpaceWithNewline() } + when { + leafOnSameLineAfterMultilineExpression == null -> Unit + + leafOnSameLineAfterMultilineExpression.treeParent.elementType == OPERATION_REFERENCE -> { + // When binary expressions are wrapped, each binary expression for itself is checked whether it is a + // multiline expression. So there is no need to check whether wrapping after the operation reference is + // needed + Unit + } + + leafOnSameLineAfterMultilineExpression.elementType == COMMA && + (leafOnSameLineAfterMultilineExpression.treeParent.elementType == VALUE_ARGUMENT_LIST || + leafOnSameLineAfterMultilineExpression.treeParent.elementType == VALUE_PARAMETER_LIST + ) -> { + // Keep comma on same line as multiline expression: + // foo( + // fooBar + // .filter { it.bar }, + // ) + leafOnSameLineAfterMultilineExpression + .nextLeaf() + ?.upsertWhitespaceBeforeMe(indentConfig.siblingIndentOf(node)) + } + + else -> { + leafOnSameLineAfterMultilineExpression.upsertWhitespaceBeforeMe(indentConfig.siblingIndentOf(node)) + } + } } } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressionWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressionWrappingRuleTest.kt index 017cb4a7e6..1f163b0fe0 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressionWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressionWrappingRuleTest.kt @@ -5,6 +5,7 @@ import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue import com.pinterest.ktlint.test.KtLintAssertThat import com.pinterest.ktlint.test.LintViolation import com.pinterest.ktlint.test.MULTILINE_STRING_QUOTE +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -12,7 +13,7 @@ class MultilineExpressionWrappingRuleTest { private val multilineExpressionWrappingRuleAssertThat = KtLintAssertThat.assertThatRule { MultilineExpressionWrappingRule() } @Nested - inner class `Given a function call using a named parameter` { + inner class `Given a function call using a named argument` { @Test fun `Given value argument for a named parameter in a function with a multiline dot qualified expression on the same line as the assignment`() { val code = @@ -160,7 +161,7 @@ class MultilineExpressionWrappingRuleTest { } @Nested - inner class `Given a function call using an unnamed parameter` { + inner class `Given a function call using an unnamed argument` { @Test fun `Given value argument in a function with a multiline binary expression on the same line as the assignment`() { val code = @@ -297,6 +298,31 @@ class MultilineExpressionWrappingRuleTest { } } + @Test + fun `Given a declaration with parameter having a default value which is a multiline expression then keep trailing comma after the parameter`() { + val code = + """ + fun foo( + val string: String = barFoo + .count { it == "bar" }, + val int: Int + ) + """.trimIndent() + val formattedCode = + """ + fun foo( + val string: String = + barFoo + .count { it == "bar" }, + val int: Int + ) + """.trimIndent() + multilineExpressionWrappingRuleAssertThat(code) + .addAdditionalRuleProvider { IndentationRule() } + .hasLintViolation(2, 26, "A multiline expression should start on a new line") + .isFormattedAs(formattedCode) + } + @Test fun `Given a return statement with a multiline expression then do not reformat as it would result in a compilation error`() { val code = @@ -830,4 +856,65 @@ class MultilineExpressionWrappingRuleTest { .hasLintViolation(1, 11, "A multiline expression should start on a new line") .isFormattedAs(formattedCode) } + + @Test + fun `Issue 2286 - `() { + val code = + """ + val foo = foo() + bar1 { + "bar1" + } + + bar2 { + "bar2" + } + """.trimIndent() + val formattedCode = + """ + val foo = + foo() + + bar1 { + "bar1" + } + + bar2 { + "bar2" + } + """.trimIndent() + multilineExpressionWrappingRuleAssertThat(code) + .addAdditionalRuleProvider { IndentationRule() } + .hasLintViolations( + LintViolation(1, 11, "A multiline expression should start on a new line"), + LintViolation(1, 19, "A multiline expression should start on a new line"), + ).isFormattedAs(formattedCode) + } + + @Disabled + @Test + fun `Issue 2286 - xx `() { + val code = + """ + val foo = foo() + bar1 { + "bar1" + } + "bar3" + + bar2 { + "bar2" + } + """.trimIndent() + val formattedCode = + """ + val foo = + foo() + + bar1 { + "bar1" + } + + bar2 { + "bar2" + } + """.trimIndent() + multilineExpressionWrappingRuleAssertThat(code) + .addAdditionalRuleProvider { IndentationRule() } + .hasLintViolations( + LintViolation(1, 11, "A multiline expression should start on a new line"), + LintViolation(1, 19, "A multiline expression should start on a new line"), + ).isFormattedAs(formattedCode) + } }