diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt index fec961d5da..faf785c462 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt @@ -22,7 +22,6 @@ 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.editorconfig.MAX_LINE_LENGTH_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.indent -import com.pinterest.ktlint.rule.engine.core.api.isPartOf 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 @@ -111,10 +110,7 @@ public class ArgumentListWrappingRule : // skip lambda arguments node.treeParent?.elementType != FUNCTION_LITERAL && // skip if number of arguments is big (we assume it with a magic number of 8) - node.children().count { it.elementType == VALUE_ARGUMENT } <= 8 && - // skip if part of a value argument list. It depends on the situation whether it is better to wrap the arguments in the list - // or the operators in the binary expression - !node.isPartOf(BINARY_EXPRESSION) + node.children().count { it.elementType == VALUE_ARGUMENT } <= 8 ) { // each argument should be on a separate line if // - at least one of the arguments is diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BinaryExpressionWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BinaryExpressionWrappingRule.kt index dfb2c362c6..204d9e59c6 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BinaryExpressionWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BinaryExpressionWrappingRule.kt @@ -2,35 +2,42 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.rule.engine.core.api.ElementType.BINARY_EXPRESSION import com.pinterest.ktlint.rule.engine.core.api.ElementType.CALL_EXPRESSION +import com.pinterest.ktlint.rule.engine.core.api.ElementType.CONDITION import com.pinterest.ktlint.rule.engine.core.api.ElementType.ELVIS import com.pinterest.ktlint.rule.engine.core.api.ElementType.EQ import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN +import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUNCTION_LITERAL import com.pinterest.ktlint.rule.engine.core.api.ElementType.LAMBDA_ARGUMENT +import com.pinterest.ktlint.rule.engine.core.api.ElementType.LAMBDA_EXPRESSION +import com.pinterest.ktlint.rule.engine.core.api.ElementType.LBRACE import com.pinterest.ktlint.rule.engine.core.api.ElementType.LONG_STRING_TEMPLATE_ENTRY import com.pinterest.ktlint.rule.engine.core.api.ElementType.OPERATION_REFERENCE import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY -import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT_LIST +import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACE +import com.pinterest.ktlint.rule.engine.core.api.ElementType.VALUE_ARGUMENT import com.pinterest.ktlint.rule.engine.core.api.IndentConfig import com.pinterest.ktlint.rule.engine.core.api.Rule -import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule -import com.pinterest.ktlint.rule.engine.core.api.Rule.VisitorModifier.RunAfterRule.Mode.REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED import com.pinterest.ktlint.rule.engine.core.api.RuleId import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint.Status.EXPERIMENTAL +import com.pinterest.ktlint.rule.engine.core.api.children import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.editorconfig.MAX_LINE_LENGTH_PROPERTY import com.pinterest.ktlint.rule.engine.core.api.firstChildLeafOrSelf -import com.pinterest.ktlint.rule.engine.core.api.indent +import com.pinterest.ktlint.rule.engine.core.api.isCodeLeaf +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.lastChildLeafOrSelf import com.pinterest.ktlint.rule.engine.core.api.leavesOnLine -import com.pinterest.ktlint.rule.engine.core.api.nextCodeSibling +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.noNewLineInClosedRange import com.pinterest.ktlint.rule.engine.core.api.parent import com.pinterest.ktlint.rule.engine.core.api.prevLeaf import com.pinterest.ktlint.rule.engine.core.api.prevSibling +import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceAfterMe import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe import com.pinterest.ktlint.ruleset.standard.StandardRule import org.jetbrains.kotlin.com.intellij.lang.ASTNode @@ -50,10 +57,6 @@ public class BinaryExpressionWrappingRule : INDENT_STYLE_PROPERTY, MAX_LINE_LENGTH_PROPERTY, ), - visitorModifiers = - setOf( - RunAfterRule(ARGUMENT_LIST_WRAPPING_RULE_ID, REGARDLESS_WHETHER_RUN_AFTER_RULE_IS_LOADED_OR_DISABLED), - ), ), Rule.Experimental { private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG @@ -74,11 +77,11 @@ public class BinaryExpressionWrappingRule : emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { when (node.elementType) { - BINARY_EXPRESSION -> visitExpression(node, emit, autoCorrect) + BINARY_EXPRESSION -> visitBinaryExpression(node, emit, autoCorrect) } } - private fun visitExpression( + private fun visitBinaryExpression( node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, autoCorrect: Boolean, @@ -101,15 +104,87 @@ public class BinaryExpressionWrappingRule : true, ) if (autoCorrect) { - expression.upsertWhitespaceBeforeMe(expression.indent().plus(indentConfig.indent)) + expression.upsertWhitespaceBeforeMe(indentConfig.childIndentOf(expression)) + } + } + + node + .takeIf { it.treeParent.elementType == VALUE_ARGUMENT } + ?.takeIf { it.causesMaxLineLengthToBeExceeded() } + ?.let { expression -> + emit( + expression.startOffset, + "Line is exceeding max line length. Break line before expression", + true, + ) + if (autoCorrect) { + expression.upsertWhitespaceBeforeMe(indentConfig.childIndentOf(expression)) } } + // When left hand side is a call expression which causes the max line length to be exceeded then first wrap that expression + node + .children() + .firstOrNull { !it.isCodeLeaf() } + ?.takeIf { it.elementType == CALL_EXPRESSION } + ?.takeIf { it.causesMaxLineLengthToBeExceeded() } + ?.let { callExpression -> visitCallExpression(callExpression, emit, autoCorrect) } + + // The remainder (operation reference plus right hand side) might still cause the max line length to be exceeded node - .findChildByType(OPERATION_REFERENCE) + .takeIf { node.lastChildNode.causesMaxLineLengthToBeExceeded() || node.isPartOfConditionExceedingMaxLineLength() } + ?.findChildByType(OPERATION_REFERENCE) ?.let { operationReference -> visitOperationReference(operationReference, emit, autoCorrect) } } + private fun ASTNode.isPartOfConditionExceedingMaxLineLength() = + // Checks that when binary expression itself fits on the line, but the closing parenthesis or opening brace does not fit. + // // Suppose that X is the last possible character on the + // // line X + // if (leftHandSideExpression && rightHandSideExpression) { + treeParent + .takeIf { it.elementType == CONDITION } + ?.lastChildLeafOrSelf() + ?.nextLeaf { it.isWhiteSpaceWithNewline() } + ?.prevLeaf() + ?.causesMaxLineLengthToBeExceeded() + ?: false + + private fun visitCallExpression( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean, + ) { + node + .takeIf { it.elementType == CALL_EXPRESSION } + ?.takeIf { it.treeParent.elementType == BINARY_EXPRESSION } + ?.let { callExpression -> + // Breaking the lambda expression has priority over breaking value arguments + callExpression + .findChildByType(LAMBDA_ARGUMENT) + ?.findChildByType(LAMBDA_EXPRESSION) + ?.findChildByType(FUNCTION_LITERAL) + ?.let { functionLiteral -> + functionLiteral + .findChildByType(LBRACE) + ?.let { lbrace -> + emit(lbrace.startOffset + 1, "Newline expected after '{'", true) + if (autoCorrect) { + lbrace.upsertWhitespaceAfterMe(indentConfig.childIndentOf(lbrace.treeParent)) + } + } + functionLiteral + .findChildByType(RBRACE) + ?.let { rbrace -> + emit(rbrace.startOffset, "Newline expected before '}'", true) + if (autoCorrect) { + rbrace.upsertWhitespaceBeforeMe(indentConfig.siblingIndentOf(node.treeParent)) + } + } + } + } + } + private fun visitOperationReference( node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, @@ -117,68 +192,54 @@ public class BinaryExpressionWrappingRule : ) { node .takeIf { it.elementType == OPERATION_REFERENCE } - ?.takeIf { it.treeParent.elementType == BINARY_EXPRESSION } + ?.takeUnless { + // Allow: + // val foo = "string too long to fit on the line" + + // "more text" + it.nextSibling().isWhiteSpaceWithNewline() + }?.takeIf { it.treeParent.elementType == BINARY_EXPRESSION } ?.takeIf { binaryExpression -> // Ignore binary expression inside raw string literals. Raw string literals are allowed to exceed max-line-length. Wrapping // (each) binary expression inside such a literal seems to create more chaos than it resolves. binaryExpression.parent { it.elementType == LONG_STRING_TEMPLATE_ENTRY } == null - }?.takeIf { it.isOnLineExceedingMaxLineLength() } - ?.let { operationReference -> - if (node.isCallExpressionFollowedByLambdaArgument() || cannotBeWrappedAtOperationReference(operationReference)) { - // Wrapping after operation reference might not be the best place in case of a call expression or just won't work as - // the left hand side still does not fit on a single line - val offset = - operationReference - .prevLeaf { it.isWhiteSpaceWithNewline() } - ?.let { previousNewlineNode -> - previousNewlineNode.startOffset + - previousNewlineNode.text.indexOfLast { it == '\n' } + - 1 + }?.let { operationReference -> + if (operationReference.firstChildNode.elementType == ELVIS) { + operationReference + .prevLeaf { it.isWhiteSpace() } + .takeUnless { it.isWhiteSpaceWithNewline() } + ?.let { + // Wrapping after the elvis operator leads to violating the 'chain-wrapping' rule, so it must wrapped itself + emit(operationReference.startOffset, "Line is exceeding max line length. Break line before '?:'", true) + if (autoCorrect) { + operationReference.upsertWhitespaceBeforeMe(indentConfig.childIndentOf(operationReference)) } - ?: operationReference.startOffset - emit(offset, "Line is exceeding max line length", false) + } } else { operationReference .nextSibling() ?.let { nextSibling -> emit( nextSibling.startOffset, - "Line is exceeding max line length. Break line after operator in binary expression", + "Line is exceeding max line length. Break line after '${operationReference.text}' in binary expression", true, ) if (autoCorrect) { - nextSibling.upsertWhitespaceBeforeMe(operationReference.indent().plus(indentConfig.indent)) + nextSibling.upsertWhitespaceBeforeMe(indentConfig.childIndentOf(operationReference)) } } } } } - private fun ASTNode.isCallExpressionFollowedByLambdaArgument() = - parent { it.elementType == VALUE_ARGUMENT_LIST } - ?.takeIf { it.treeParent.elementType == CALL_EXPRESSION } - ?.nextCodeSibling() - .let { it?.elementType == LAMBDA_ARGUMENT } - - private fun cannotBeWrappedAtOperationReference(operationReference: ASTNode) = - if (operationReference.firstChildNode.elementType == ELVIS) { - true - } else { - operationReference - .takeUnless { it.nextCodeSibling()?.elementType == BINARY_EXPRESSION } - ?.let { - val stopAtOperationReferenceLeaf = operationReference.firstChildLeafOrSelf() - maxLineLength <= - it - .leavesOnLine() - .takeWhile { leaf -> leaf != stopAtOperationReferenceLeaf } - .lengthWithoutNewlinePrefix() - } - ?: false - } - private fun ASTNode.isOnLineExceedingMaxLineLength() = leavesOnLine().lengthWithoutNewlinePrefix() > maxLineLength + private fun ASTNode.causesMaxLineLengthToBeExceeded() = + lastChildLeafOrSelf().let { lastChildLeaf -> + leavesOnLine() + .takeWhile { it.prevLeaf() != lastChildLeaf } + .lengthWithoutNewlinePrefix() + } > maxLineLength + private fun Sequence.lengthWithoutNewlinePrefix() = joinToString(separator = "") { it.text } .dropWhile { it == '\n' } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRuleTest.kt index 52744a7e6c..02106b257e 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRuleTest.kt @@ -843,22 +843,46 @@ class ArgumentListWrappingRuleTest { } @Test - fun `Given a property assignment with a binary expression for which the left hand side operator is a function call`() { + fun `Given a property assignment with a binary expression for which the left hand side operator is a function call then binary expression wrapping takes precedence`() { val code = """ // $MAX_LINE_LENGTH_MARKER $EOL_CHAR val foo1 = foobar(foo * bar) + "foo" - val foo2 = foobar("foo", foo * bar) + "foo" - val foo3 = foobar("fooo", foo * bar) + "foo" + val foo2 = foobar(foo * bar) + "fooo" + val foo3 = foobar("fooooooo", bar) + "foo" + val foo4 = foobar("foooooooo", bar) + "foo" + val foo5 = foobar("fooooooooo", bar) + "foo" + val foo6 = foobar("foooo", foo * bar) + "foo" + val foo7 = foobar("fooooooooooo", foo * bar) + "foo" + """.trimIndent() + val formattedCode = + """ + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + val foo1 = foobar(foo * bar) + "foo" + val foo2 = + foobar(foo * bar) + "fooo" + val foo3 = + foobar("fooooooo", bar) + "foo" + val foo4 = + foobar("foooooooo", bar) + "foo" + val foo5 = + foobar("fooooooooo", bar) + + "foo" + val foo6 = + foobar("foooo", foo * bar) + + "foo" + val foo7 = + foobar( + "fooooooooooo", + foo * bar + ) + + "foo" """.trimIndent() argumentListWrappingRuleAssertThat(code) .setMaxLineLength() .addAdditionalRuleProvider { BinaryExpressionWrappingRule() } .addAdditionalRuleProvider { WrappingRule() } - // Although the argument-list-wrapping runs before binary-expression-wrapping, it may not wrap the argument values of a - // function call in case that call is part of a binary expression. It might be better to break the line at the operation - // reference instead. - .hasNoLintViolationsExceptInAdditionalRules() + .isFormattedAs(formattedCode) } @Test @@ -919,4 +943,43 @@ class ArgumentListWrappingRuleTest { // When ValueArgumentCommentRule is not loaded or enabled argumentListWrappingRuleAssertThat(code).hasNoLintViolations() } + + @Test + fun `Issue 2462 - Given a call expression with value argument list inside a binary expression, then first wrap the binary expression`() { + val code = + """ + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + fun foo() { + every { foo.bar(bazbazbazbazbazbazbazbazbaz) } returns bar + } + """.trimIndent() + val formattedCode = + """ + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + fun foo() { + every { + foo.bar(bazbazbazbazbazbazbazbazbaz) + } returns bar + } + """.trimIndent() + argumentListWrappingRuleAssertThat(code) + .setMaxLineLength() + .addAdditionalRuleProvider { BinaryExpressionWrappingRule() } + .addAdditionalRuleProvider { WrappingRule() } + // Lint violations from BinaryExpressionWrappingRule and WrappingRule are reported during linting only. When formatting, the + // wrapping of the braces of the function literal by the BinaryExpressionWrapping prevents those violations from occurring. + .hasLintViolationsForAdditionalRule( + LintViolation(3, 12, "Newline expected after '{'"), + LintViolation(3, 12, "Missing newline after \"{\""), + LintViolation(3, 50, "Newline expected before '}'"), + // Lint violation below only occurs during linting. Resolving violations above, prevents the next violation from occurring + LintViolation(3, 59, "Line is exceeding max line length. Break line after 'returns' in binary expression"), + ) + // The lint violation below is only reported during lint. When formatting, the violation above is resolved first, and as a + // result this violation will no longer occur. + .hasLintViolations( + LintViolation(3, 21, "Argument should be on a separate line (unless all arguments can fit a single line)"), + LintViolation(3, 48, "Missing newline before \")\""), + ).isFormattedAs(formattedCode) + } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BinaryExpressionWrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BinaryExpressionWrappingRuleTest.kt index 6a811593d9..1a1b08cad2 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BinaryExpressionWrappingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BinaryExpressionWrappingRuleTest.kt @@ -30,7 +30,7 @@ class BinaryExpressionWrappingRuleTest { LintViolation(2, 11, "Line is exceeding max line length. Break line between assignment and expression"), // Next violation only happens during linting. When formatting the violation does not occur because fix of previous // violation prevent that the remainder of the line exceeds the maximum - LintViolation(2, 36, "Line is exceeding max line length. Break line after operator in binary expression"), + LintViolation(2, 36, "Line is exceeding max line length. Break line after '&&' in binary expression"), ).isFormattedAs(formattedCode) } @@ -51,7 +51,7 @@ class BinaryExpressionWrappingRuleTest { """.trimIndent() binaryExpressionWrappingRuleAssertThat(code) .setMaxLineLength() - .hasLintViolation(3, 30, "Line is exceeding max line length. Break line after operator in binary expression") + .hasLintViolation(3, 30, "Line is exceeding max line length. Break line after '&&' in binary expression") .isFormattedAs(formattedCode) } @@ -74,7 +74,7 @@ class BinaryExpressionWrappingRuleTest { """.trimIndent() binaryExpressionWrappingRuleAssertThat(code) .setMaxLineLength() - .hasLintViolation(3, 37, "Line is exceeding max line length. Break line after operator in binary expression") + .hasLintViolation(3, 37, "Line is exceeding max line length. Break line after '&&' in binary expression") .isFormattedAs(formattedCode) } @@ -97,7 +97,7 @@ class BinaryExpressionWrappingRuleTest { LintViolation(2, 13, "Line is exceeding max line length. Break line between assignment and expression"), // Next violation only happens during linting. When formatting the violation does not occur because fix of previous // violation prevent that the remainder of the line exceeds the maximum - LintViolation(2, 38, "Line is exceeding max line length. Break line after operator in binary expression"), + LintViolation(2, 38, "Line is exceeding max line length. Break line after '&&' in binary expression"), ).isFormattedAs(formattedCode) } @@ -118,7 +118,7 @@ class BinaryExpressionWrappingRuleTest { """.trimIndent() binaryExpressionWrappingRuleAssertThat(code) .setMaxLineLength() - .hasLintViolation(3, 30, "Line is exceeding max line length. Break line after operator in binary expression") + .hasLintViolation(3, 30, "Line is exceeding max line length. Break line after '&&' in binary expression") .isFormattedAs(formattedCode) } @@ -145,7 +145,8 @@ class BinaryExpressionWrappingRuleTest { """.trimIndent() binaryExpressionWrappingRuleAssertThat(code) .setMaxLineLength() - .hasLintViolation(3, 34, "Line is exceeding max line length. Break line after operator in binary expression") + .addAdditionalRuleProvider { ConditionWrappingRule() } + .hasLintViolation(3, 34, "Line is exceeding max line length. Break line after '&&' in binary expression") .isFormattedAs(formattedCode) } @@ -174,12 +175,8 @@ class BinaryExpressionWrappingRuleTest { binaryExpressionWrappingRuleAssertThat(code) .setMaxLineLength() .hasLintViolations( - // When linting, a violation is reported for each operation reference. While when formatting, the nested binary expression - // is evaluated (working from outside to inside). After wrapping an outer binary expression, the inner binary expressions - // are evaluated and only wrapped again at the operation reference when needed. - LintViolation(3, 1, "Line is exceeding max line length", canBeAutoCorrected = false), - LintViolation(3, 35, "Line is exceeding max line length. Break line after operator in binary expression"), - LintViolation(3, 63, "Line is exceeding max line length. Break line after operator in binary expression"), + LintViolation(3, 63, "Line is exceeding max line length. Break line after '||' in binary expression"), + LintViolation(3, 94, "Line is exceeding max line length. Break line after '&&' in binary expression"), ).isFormattedAs(formattedCode) } @@ -195,27 +192,43 @@ class BinaryExpressionWrappingRuleTest { """.trimIndent() binaryExpressionWrappingRuleAssertThat(code) .setMaxLineLength() - .hasLintViolationWithoutAutoCorrect(3, 1, "Line is exceeding max line length") + .hasNoLintViolations() } @Test - fun `Given a binary expression for which wrapping of the operator reference would still violates the max-line-length`() { + fun `Given a binary expression containing an elvis operator`() { val code = """ // $MAX_LINE_LENGTH_MARKER $EOL_CHAR - val foo1 = foo() ?: "fooooooooooo" + + val foo1 = foo() ?: "foooooooooooooooooo" + "bar" // Do not remove blank line below, it is relevant as both the newline of the blank line and the indent before property foo2 have to be accounted for - val foo2 = foo() ?: "foooooooooo" + + val foo2 = foo() ?: "foooooooooooooooooo" + + "bar" + """.trimIndent() + val formattedCode = + """ + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + val foo1 = + foo() + ?: "foooooooooooooooooo" + + "bar" + // Do not remove blank line below, it is relevant as both the newline of the blank line and the indent before property foo2 have to be accounted for + + val foo2 = + foo() + ?: "foooooooooooooooooo" + "bar" """.trimIndent() binaryExpressionWrappingRuleAssertThat(code) .setMaxLineLength() .hasLintViolations( - LintViolation(2, 1, "Line is exceeding max line length", canBeAutoCorrected = false), LintViolation(2, 12, "Line is exceeding max line length. Break line between assignment and expression"), - ) + LintViolation(2, 18, "Line is exceeding max line length. Break line before '?:'"), + LintViolation(6, 12, "Line is exceeding max line length. Break line between assignment and expression"), + LintViolation(6, 18, "Line is exceeding max line length. Break line before '?:'"), + ).isFormattedAs(formattedCode) } @Test @@ -234,34 +247,64 @@ class BinaryExpressionWrappingRuleTest { } @Test - fun `Given a value argument containing a binary expression`() { + fun `Given a value argument containing a binary expression which causes the line to exceeds tbe maximum line length`() { val code = """ // $MAX_LINE_LENGTH_MARKER $EOL_CHAR - val foobar = Foo(bar(1 * 2 * 3)) + val foobar1 = Foo(1 * 2 * 3 * 4) + val foobar2 = Foo(bar(1 * 2 * 3)) + val foobar3 = Foo(bar("bar" + "bazzzzzzzzzzz")) + val foobar4 = Foo(bar("bar" + "bazzzzzzzzzzzz")) """.trimIndent() val formattedCode = """ // $MAX_LINE_LENGTH_MARKER $EOL_CHAR - val foobar = Foo( + val foobar1 = Foo( + 1 * 2 * 3 * 4 + ) + val foobar2 = Foo( bar(1 * 2 * 3) ) + val foobar3 = Foo( + bar( + "bar" + "bazzzzzzzzzzz" + ) + ) + val foobar4 = Foo( + bar( + "bar" + + "bazzzzzzzzzzzz" + ) + ) """.trimIndent() binaryExpressionWrappingRuleAssertThat(code) .addAdditionalRuleProvider { ArgumentListWrappingRule() } + .addAdditionalRuleProvider { IndentationRule() } .addAdditionalRuleProvider { WrappingRule() } .addRequiredRuleProviderDependenciesFrom(StandardRuleSetProvider()) .setMaxLineLength() .hasLintViolations( // Although violations below are reported by the Linter, they will not be enforced by the formatter. After the // ArgumentListWrapping rule has wrapped the argument, there is no more need to wrap the expression as well. - LintViolation(2, 25, "Line is exceeding max line length. Break line after operator in binary expression"), - LintViolation(2, 29, "Line is exceeding max line length. Break line after operator in binary expression"), + LintViolation(4, 23, "Line is exceeding max line length. Break line before expression"), + LintViolation(4, 30, "Line is exceeding max line length. Break line after '+' in binary expression"), + LintViolation(5, 23, "Line is exceeding max line length. Break line before expression"), + LintViolation(5, 30, "Line is exceeding max line length. Break line after '+' in binary expression"), ).hasLintViolationsForAdditionalRules( - LintViolation(2, 18, "Argument should be on a separate line (unless all arguments can fit a single line)"), - LintViolation(2, 22, "Argument should be on a separate line (unless all arguments can fit a single line)"), - LintViolation(2, 31, "Missing newline before \")\""), + LintViolation(2, 19, "Argument should be on a separate line (unless all arguments can fit a single line)"), LintViolation(2, 32, "Missing newline before \")\""), + LintViolation(3, 19, "Argument should be on a separate line (unless all arguments can fit a single line)"), + LintViolation(3, 23, "Argument should be on a separate line (unless all arguments can fit a single line)"), + LintViolation(3, 32, "Missing newline before \")\""), + LintViolation(3, 33, "Missing newline before \")\""), + LintViolation(4, 19, "Argument should be on a separate line (unless all arguments can fit a single line)"), + LintViolation(4, 23, "Argument should be on a separate line (unless all arguments can fit a single line)"), + LintViolation(4, 46, "Missing newline before \")\""), + LintViolation(4, 47, "Missing newline before \")\""), + LintViolation(5, 19, "Argument should be on a separate line (unless all arguments can fit a single line)"), + LintViolation(5, 23, "Argument should be on a separate line (unless all arguments can fit a single line)"), + LintViolation(5, 47, "Missing newline before \")\""), + LintViolation(5, 48, "Missing newline before \")\""), ).isFormattedAs(formattedCode) } @@ -296,12 +339,21 @@ class BinaryExpressionWrappingRuleTest { } @Test - fun `Given a call expression followed by lambda argument`() { + fun `Given a call expression containing an binary expression value argument, followed by a lambda on the same line, then wrap the lambda`() { val code = """ - // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR fun foo() { - require(bar != "bar") { "some longgggggggggggggggggg message" } + require(bar != "barrrrrrrr") { "some longgggggggggggggggggg message" } + } + """.trimIndent() + val formattedCode = + """ + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + fun foo() { + require(bar != "barrrrrrrr") { + "some longgggggggggggggggggg message" + } } """.trimIndent() binaryExpressionWrappingRuleAssertThat(code) @@ -309,7 +361,33 @@ class BinaryExpressionWrappingRuleTest { .addAdditionalRuleProvider { ArgumentListWrappingRule() } .addAdditionalRuleProvider { WrappingRule() } .addRequiredRuleProviderDependenciesFrom(StandardRuleSetProvider()) - .hasLintViolationWithoutAutoCorrect(3, 1, "Line is exceeding max line length") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given an elvis expression exceeding the line length`() { + val code = + """ + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + val foo = foobar ?: throw UnsupportedOperationException("foobar") + """.trimIndent() + val formattedCode = + """ + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + val foo = + foobar + ?: throw UnsupportedOperationException( + "foobar" + ) + """.trimIndent() + binaryExpressionWrappingRuleAssertThat(code) + .setMaxLineLength() + .addAdditionalRuleProvider { ArgumentListWrappingRule() } + .addAdditionalRuleProvider { MaxLineLengthRule() } + .hasLintViolations( + LintViolation(2, 11, "Line is exceeding max line length. Break line between assignment and expression", true), + LintViolation(2, 18, "Line is exceeding max line length. Break line before '?:'", true), + ).isFormattedAs(formattedCode) } @Test @@ -321,12 +399,46 @@ class BinaryExpressionWrappingRuleTest { bar ?: throw UnsupportedOperationException("foobar") """.trimIndent() + binaryExpressionWrappingRuleAssertThat(code) + .setMaxLineLength() + .addAdditionalRuleProvider { MaxLineLengthRule() } + .hasLintViolationForAdditionalRule(4, 56, "Exceeded max line length (55)", false) + .hasNoLintViolationsExceptInAdditionalRules() + } + + @Test + fun `Issue 2462 - Given a call expression with value argument list inside a binary expression, then first wrap the binary expression`() { + val code = + """ + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + fun foo() { + every { foo.bar(bazbazbazbazbazbazbazbazbaz) } returns bar + } + """.trimIndent() + val formattedCode = + """ + // $MAX_LINE_LENGTH_MARKER $EOL_CHAR + fun foo() { + every { + foo.bar(bazbazbazbazbazbazbazbazbaz) + } returns bar + } + """.trimIndent() binaryExpressionWrappingRuleAssertThat(code) .setMaxLineLength() .addAdditionalRuleProvider { ArgumentListWrappingRule() } .addAdditionalRuleProvider { WrappingRule() } - .addAdditionalRuleProvider { MaxLineLengthRule() } - .addRequiredRuleProviderDependenciesFrom(StandardRuleSetProvider()) - .hasLintViolationWithoutAutoCorrect(4, 1, "Line is exceeding max line length") + // Lint violations from ArgumentListWrappingRule and WrappingRule are reported during linting only. When formatting, the + // wrapping of the braces of the function literal by the BinaryExpressionWrapping prevents those violations from occurring. + .hasLintViolationsForAdditionalRule( + LintViolation(3, 12, "Missing newline after \"{\""), + LintViolation(3, 21, "Argument should be on a separate line (unless all arguments can fit a single line)"), + LintViolation(3, 48, "Missing newline before \")\""), + ).hasLintViolations( + LintViolation(3, 12, "Newline expected after '{'"), + LintViolation(3, 50, "Newline expected before '}'"), + // Lint violation below only occurs during linting. Resolving violations above, prevents the next violation from occurring + LintViolation(3, 59, "Line is exceeding max line length. Break line after 'returns' in binary expression"), + ).isFormattedAs(formattedCode) } }