From 60499e047fcd842a7fe9b6e85b7c415b7ba481ca Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Mon, 7 Mar 2022 20:16:13 +0100 Subject: [PATCH] Move wrapping logic from IndentationRule to new WrappingRule Wrapping logic belong in an own rule, so it can be disabled without disabling the indentation logic. The WrappingRule contains a simplified indentation logic. Whenever a linebreak is inserted, it determines the indentation based on the current indentation of the parent node regardless whether this node is indented correctly. It is the responsibility of the IndentationRule, which runs later than the WrappingRule to fix the indentations. Spec files used in the unit tests have been split. Some parts are moved to new spec files specific for the WrappingRule. Other parts are moved to real unit tests. And on some spec files, both the wrapping and indentation rules are run together. Those files need further refactoring. The RuleExtension now also allows the diffFileLint and diffFileFormat to run on a list of rules. Spec files which are used for linting with multiple rules must specify the rule-id in the expectations. The rule-id now optionally can also be specified in those expectation when the file is checked by only one rule. However, in case the detail property of the LintError contains a ":" then the rule-id *must* be specified as it is used as delimiter. Closes #835 --- CHANGELOG.md | 1 + .../spacing-around-double-colon/lint.kt.spec | 22 +- .../ruleset/standard/IndentationRule.kt | 360 +---- .../standard/StandardRuleSetProvider.kt | 3 +- .../ktlint/ruleset/standard/WrappingRule.kt | 439 ++++++ .../ruleset/standard/IndentationRuleTest.kt | 131 +- .../ruleset/standard/WrappingRuleTest.kt | 1380 +++++++++++++++++ .../spec/chain-wrapping/lint.kt.spec | 16 +- .../format-binary-expression-expected.kt.spec | 11 +- .../indent/format-binary-expression.kt.spec | 17 +- .../spec/indent/format-eq-expected.kt.spec | 5 - .../resources/spec/indent/format-eq.kt.spec | 3 - .../resources/spec/indent/format-kdoc.kt.spec | 3 +- ...at-raw-string-trim-indent-expected.kt.spec | 8 - .../format-raw-string-trim-indent.kt.spec | 5 - .../indent/format-supertype-expected.kt.spec | 67 - .../spec/indent/format-supertype.kt.spec | 62 - .../spec/indent/lint-argument-list.kt.spec | 14 +- .../spec/indent/lint-supertype.kt.spec | 9 +- .../spec/indent/lint-when-expression.kt.spec | 5 +- .../format-argument-list-expected.kt.spec | 0 .../format-argument-list.kt.spec | 0 .../format-parameter-list-expected.kt.spec | 18 + .../wrapping/format-parameter-list.kt.spec | 10 + .../format-supertype-expected.kt.spec | 35 + .../spec/wrapping/format-supertype.kt.spec | 35 + .../pinterest/ktlint/test/RuleExtension.kt | 43 +- 27 files changed, 2049 insertions(+), 653 deletions(-) create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRule.kt create mode 100644 ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRuleTest.kt rename ktlint-ruleset-standard/src/test/resources/spec/{indent => wrapping}/format-argument-list-expected.kt.spec (100%) rename ktlint-ruleset-standard/src/test/resources/spec/{indent => wrapping}/format-argument-list.kt.spec (100%) create mode 100644 ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-parameter-list-expected.kt.spec create mode 100644 ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-parameter-list.kt.spec create mode 100644 ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-supertype-expected.kt.spec create mode 100644 ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-supertype.kt.spec diff --git a/CHANGELOG.md b/CHANGELOG.md index 76eba2c677..745c1accca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Changed - Print the rule id always in the PlainReporter ([#1121](https://github.com/pinterest/ktlint/issues/1121)) +- All wrapping logic is moved from the `indent` rule to the new rule `wrapping` (as part of the `standard` ruleset). In case you currently have disabled the `indent` rule, you may want to reconsider whether this is still necessary or that you also want to disable the new `wrapping` rule to keep the status quo. Both rules can be run independent of each other. ([#835](https://github.com/pinterest/ktlint/issues/835)) ### Removed diff --git a/ktlint-ruleset-experimental/src/test/resources/spec/spacing-around-double-colon/lint.kt.spec b/ktlint-ruleset-experimental/src/test/resources/spec/spacing-around-double-colon/lint.kt.spec index b2b0ba03f2..d15a91f1d7 100644 --- a/ktlint-ruleset-experimental/src/test/resources/spec/spacing-around-double-colon/lint.kt.spec +++ b/ktlint-ruleset-experimental/src/test/resources/spec/spacing-around-double-colon/lint.kt.spec @@ -34,14 +34,14 @@ fun main() { } // expect -// 3:19:Unexpected spacing before "::" -// 4:21:Unexpected spacing after "::" -// 5:20:Unexpected spacing around "::" -// 6:21:Unexpected spacing after "::" -// 10:45:Unexpected spacing after "::" -// 11:42:Unexpected spacing before "::" -// 12:44:Unexpected spacing around "::" -// 16:45:Unexpected spacing after "::" -// 23:70:Unexpected spacing around "::" -// 28:11:Unexpected spacing after "::" -// 33:20:Unexpected spacing before "::" +// 3:19:double-colon-spacing:Unexpected spacing before "::" +// 4:21:double-colon-spacing:Unexpected spacing after "::" +// 5:20:double-colon-spacing:Unexpected spacing around "::" +// 6:21:double-colon-spacing:Unexpected spacing after "::" +// 10:45:double-colon-spacing:Unexpected spacing after "::" +// 11:42:double-colon-spacing:Unexpected spacing before "::" +// 12:44:double-colon-spacing:Unexpected spacing around "::" +// 16:45:double-colon-spacing:Unexpected spacing after "::" +// 23:70:double-colon-spacing:Unexpected spacing around "::" +// 28:11:double-colon-spacing:Unexpected spacing after "::" +// 33:20:double-colon-spacing:Unexpected spacing before "::" diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt index 698c40c780..1a15ee7d3f 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRule.kt @@ -7,7 +7,6 @@ import com.pinterest.ktlint.core.IndentConfig import com.pinterest.ktlint.core.IndentConfig.IndentStyle.SPACE import com.pinterest.ktlint.core.IndentConfig.IndentStyle.TAB import com.pinterest.ktlint.core.Rule -import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION import com.pinterest.ktlint.core.ast.ElementType.ARROW import com.pinterest.ktlint.core.ast.ElementType.BINARY_EXPRESSION import com.pinterest.ktlint.core.ast.ElementType.BINARY_WITH_TYPE @@ -17,7 +16,6 @@ import com.pinterest.ktlint.core.ast.ElementType.BY_KEYWORD import com.pinterest.ktlint.core.ast.ElementType.CALL_EXPRESSION import com.pinterest.ktlint.core.ast.ElementType.CLOSING_QUOTE import com.pinterest.ktlint.core.ast.ElementType.COLON -import com.pinterest.ktlint.core.ast.ElementType.COMMA import com.pinterest.ktlint.core.ast.ElementType.CONDITION import com.pinterest.ktlint.core.ast.ElementType.DELEGATED_SUPER_TYPE_ENTRY import com.pinterest.ktlint.core.ast.ElementType.DOT_QUALIFIED_EXPRESSION @@ -33,14 +31,12 @@ import com.pinterest.ktlint.core.ast.ElementType.KDOC 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_START -import com.pinterest.ktlint.core.ast.ElementType.LAMBDA_EXPRESSION 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.LPAR import com.pinterest.ktlint.core.ast.ElementType.LT -import com.pinterest.ktlint.core.ast.ElementType.OBJECT_LITERAL import com.pinterest.ktlint.core.ast.ElementType.OPEN_QUOTE import com.pinterest.ktlint.core.ast.ElementType.OPERATION_REFERENCE import com.pinterest.ktlint.core.ast.ElementType.PARENTHESIZED @@ -54,7 +50,6 @@ import com.pinterest.ktlint.core.ast.ElementType.SECONDARY_CONSTRUCTOR import com.pinterest.ktlint.core.ast.ElementType.SHORT_STRING_TEMPLATE_ENTRY import com.pinterest.ktlint.core.ast.ElementType.STRING_TEMPLATE import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_CALL_ENTRY -import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_ENTRY 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.TYPE_ARGUMENT_LIST @@ -62,8 +57,6 @@ import com.pinterest.ktlint.core.ast.ElementType.TYPE_CONSTRAINT_LIST import com.pinterest.ktlint.core.ast.ElementType.TYPE_PARAMETER_LIST import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT 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.WHEN_ENTRY import com.pinterest.ktlint.core.ast.ElementType.WHERE_KEYWORD import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE @@ -73,7 +66,6 @@ import com.pinterest.ktlint.core.ast.isPartOfComment import com.pinterest.ktlint.core.ast.isWhiteSpace import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline import com.pinterest.ktlint.core.ast.isWhiteSpaceWithoutNewline -import com.pinterest.ktlint.core.ast.nextCodeLeaf import com.pinterest.ktlint.core.ast.nextCodeSibling import com.pinterest.ktlint.core.ast.nextLeaf import com.pinterest.ktlint.core.ast.nextSibling @@ -81,9 +73,6 @@ import com.pinterest.ktlint.core.ast.parent import com.pinterest.ktlint.core.ast.prevCodeLeaf import com.pinterest.ktlint.core.ast.prevCodeSibling import com.pinterest.ktlint.core.ast.prevLeaf -import com.pinterest.ktlint.core.ast.prevSibling -import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe -import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe import com.pinterest.ktlint.core.ast.visit import com.pinterest.ktlint.core.initKtLintKLogger import com.pinterest.ktlint.ruleset.standard.IndentationRule.IndentContext.Block @@ -98,29 +87,23 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.psi.KtStringTemplateExpression -import org.jetbrains.kotlin.psi.KtSuperTypeList import org.jetbrains.kotlin.psi.psiUtil.leaves private val logger = KotlinLogging.logger {}.initKtLintKLogger() /** - * ktlint's rule that checks & corrects indentation. - * - * To keep things simple, we walk the AST twice: - * - 1st pass - insert missing newlines (e.g. between parentheses of a multi-line function call) - * - 2st pass - correct indentation + * Checks & correct indentation * * Current limitations: * - "all or nothing" (currently, rule can only be disabled for an entire file) */ -class IndentationRule : Rule( +public class IndentationRule : Rule( id = "indent", visitorModifiers = setOf( VisitorModifier.RunOnRootNodeOnly, VisitorModifier.RunAsLateAsPossible ) ) { - private companion object { private val lTokenSet = TokenSet.create(LPAR, LBRACE, LBRACKET, LT) private val rTokenSet = TokenSet.create(RPAR, RBRACE, RBRACKET, GT) @@ -151,342 +134,15 @@ class IndentationRule : Rule( if (indentConfig.disabled) { return } + reset() - logger.trace { "phase: rearrangement (auto correction ${if (autoCorrect) "on" else "off"})" } - // step 1: insert newlines (if/where needed) - var emitted = false - rearrange(node, autoCorrect) { offset, errorMessage, canBeAutoCorrected -> - emitted = true - emit(offset, errorMessage, canBeAutoCorrected) - } - if (emitted && autoCorrect) { - logger.trace { - "indenting:\n" + - node - .text - .split("\n") - .mapIndexed { i, v -> "\t${i + 1}: $v" } - .joinToString("\n") - } - } - reset() - logger.trace { "phase: indentation" } - // step 2: correct indentation indent(node, autoCorrect, emit) - // The expectedIndent should never be negative. If so, it is very likely that ktlint crashes at runtime when // autocorrecting is executed while no error occurs with linting only. Such errors often are not found in unit // tests, as the examples are way more simple than realistic code. assert(expectedIndent >= 0) } - private fun rearrange( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - node.visit { n -> - when (n.elementType) { - LPAR, LBRACE, LBRACKET -> rearrangeBlock(n, autoCorrect, emit) // TODO: LT - SUPER_TYPE_LIST -> rearrangeSuperTypeList(n, autoCorrect, emit) - VALUE_PARAMETER_LIST, VALUE_ARGUMENT_LIST -> rearrangeValueList(n, autoCorrect, emit) - ARROW -> rearrangeArrow(n, autoCorrect, emit) - WHITE_SPACE -> line += n.text.count { it == '\n' } - CLOSING_QUOTE -> rearrangeClosingQuote(n, autoCorrect, emit) - } - } - } - - private fun rearrangeBlock( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - val rElementType = matchingRToken[node.elementType] - var newlineInBetween = false - var parameterListInBetween = false - var numberOfArgs = 0 - var firstArg: ASTNode? = null - // matching ), ] or } - val r = node.nextSibling { - val isValueArgument = it.elementType == VALUE_ARGUMENT - val hasLineBreak = if (isValueArgument) it.hasLineBreak(LAMBDA_EXPRESSION, FUN) else it.hasLineBreak() - newlineInBetween = newlineInBetween || hasLineBreak - parameterListInBetween = parameterListInBetween || it.elementType == VALUE_PARAMETER_LIST - if (isValueArgument) { - numberOfArgs++ - firstArg = it - } - it.elementType == rElementType - }!! - if ( - !newlineInBetween || - // keep { p -> - // } - (node.elementType == LBRACE && parameterListInBetween) || - // keep ({ - // }) and (object : C { - // }) - ( - numberOfArgs == 1 && - firstArg?.firstChildNode?.elementType - ?.let { it == OBJECT_LITERAL || it == LAMBDA_EXPRESSION } == true - ) - ) { - return - } - if (!node.nextCodeLeaf()?.prevLeaf { - // Skip comments, whitespace, and empty nodes - !it.isPartOfComment() && - !it.isWhiteSpaceWithoutNewline() && - it.textLength > 0 - }.isWhiteSpaceWithNewline() && - // IDEA quirk: - // if (true && - // true - // ) { - // } - // instead of - // if ( - // true && - // true - // ) { - // } - node.treeNext?.elementType != CONDITION - ) { - requireNewlineAfterLeaf(node, autoCorrect, emit) - } - if (!r.prevLeaf().isWhiteSpaceWithNewline()) { - requireNewlineBeforeLeaf(r, autoCorrect, emit) - } - } - - private fun rearrangeSuperTypeList( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - val entries = (node.psi as KtSuperTypeList).entries - if ( - node.textContains('\n') && - entries.size > 1 && - // e.g. - // - // class A : B, C, - // D - // or - // class A : B, C({ - // }), D - // - // but not - // - // class A : B, C, D({ - // }) - !( - entries.dropLast(1).all { it.elementType == SUPER_TYPE_ENTRY } && - entries.last().elementType == SUPER_TYPE_CALL_ENTRY - ) - ) { - // put space after : - if (!node.prevLeaf().isWhiteSpaceWithNewline()) { - val colon = node.prevCodeLeaf()!! - if ( - !colon.prevLeaf().isWhiteSpaceWithNewline() && - colon.prevCodeLeaf().let { it?.elementType != RPAR || !it.prevLeaf().isWhiteSpaceWithNewline() } - ) { - requireNewlineAfterLeaf(colon, autoCorrect, emit) - } - } - // put entries on separate lines - // TODO: group emit()s below with the one above into one (similar to ParameterListWrappingRule) - for (c in node.children()) { - if (c.elementType == COMMA && !c.treeNext.isWhiteSpaceWithNewline()) { - requireNewlineAfterLeaf(c, autoCorrect, emit) - } - } - } - } - - private fun rearrangeValueList( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - for (c in node.children()) { - val hasLineBreak = when (c.elementType) { - VALUE_ARGUMENT -> c.hasLineBreak(LAMBDA_EXPRESSION, FUN) - VALUE_PARAMETER, ANNOTATION -> c.hasLineBreak() - else -> false - } - if (hasLineBreak) { - // rearrange - // - // a, b, value( - // ), c, d - // - // to - // - // a, b, - // value( - // ), - // c, d - - // insert \n in front of multi-line value - val prevSibling = c.prevSibling { it.elementType != WHITE_SPACE } - if ( - prevSibling?.elementType == COMMA && - !prevSibling.treeNext.isWhiteSpaceWithNewline() - ) { - requireNewlineAfterLeaf(prevSibling, autoCorrect, emit) - } - // insert \n after multi-line value - val nextSibling = c.nextSibling { it.elementType != WHITE_SPACE } - if ( - nextSibling?.elementType == COMMA && - !nextSibling.treeNext.isWhiteSpaceWithNewline() && - // value( - // ), // a comment - // c, d - nextSibling.treeNext?.treeNext?.psi !is PsiComment - ) { - requireNewlineAfterLeaf(nextSibling, autoCorrect, emit) - } - } - } - } - - private fun rearrangeClosingQuote( - n: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - val treeParent = n.treeParent - if (treeParent.elementType == STRING_TEMPLATE) { - val treeParentPsi = treeParent.psi as KtStringTemplateExpression - if (treeParentPsi.isMultiLine() && n.treePrev.text.isNotBlank()) { - // rewriting - // """ - // text - // _""".trimIndent() - // to - // """ - // text - // _ - // """.trimIndent() - emit( - n.startOffset, - "Missing newline before \"\"\"", - true - ) - if (autoCorrect) { - n as LeafPsiElement - n.rawInsertBeforeMe(LeafPsiElement(REGULAR_STRING_PART, "\n")) - } - logger.trace { "$line: " + (if (!autoCorrect) "would have " else "") + "inserted newline before (closing) \"\"\"" } - } - } - } - - private fun mustBeFollowedByNewline(node: ASTNode): Boolean { - // find EOL token (last token before \n) - // if token is in lTokenSet - // find matching rToken - // return true if there is no newline after the rToken - // return false - val p = node.treeParent - val nextCodeSibling = node.nextCodeSibling() // e.g. BINARY_EXPRESSION - var lToken = nextCodeSibling?.nextLeaf { it.isWhiteSpaceWithNewline() }?.prevCodeLeaf() - if (lToken != null && lToken.elementType !in lTokenSet) { - // special cases: - // x = y.f({ z -> - // }) - // x = y.f(0, 1, - // 2, 3) - lToken = lToken.prevLeaf { it.elementType in lTokenSet || it == node } - } - if (lToken != null && lToken.elementType in lTokenSet) { - val rElementType = matchingRToken[lToken.elementType] - val rToken = lToken.nextSibling { it.elementType == rElementType } - return rToken?.treeParent == lToken.treeParent - } - if (nextCodeSibling?.textContains('\n') == false) { - return true - } - return false - } - - private fun rearrangeArrow( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - val p = node.treeParent - if ( - // check - // `{ p -> ... }` - // and - // `when { m -> ... }` - // only - p.elementType.let { it != FUNCTION_LITERAL && it != WHEN_ENTRY } || - // ... and only if expression after -> spans multiple lines - !p.textContains('\n') || - // permit - // when { - // m -> 0 + d({ - // }) - // } - (p.elementType == WHEN_ENTRY && mustBeFollowedByNewline(node)) || - // permit - // when (this) { - // in 0x1F600..0x1F64F, // Emoticons - // 0x200D // Zero-width Joiner - // -> true - // } - (p.elementType == WHEN_ENTRY && node.prevLeaf()?.textContains('\n') == true) - ) { - return - } - if (!node.nextCodeLeaf()?.prevLeaf().isWhiteSpaceWithNewline()) { - requireNewlineAfterLeaf(node, autoCorrect, emit) - } - val r = node.nextSibling { it.elementType == RBRACE } ?: return - if (!r.prevLeaf().isWhiteSpaceWithNewline()) { - requireNewlineBeforeLeaf(r, autoCorrect, emit) - } - } - - private fun requireNewlineBeforeLeaf( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - emit( - node.startOffset - 1, - """Missing newline before "${node.text}"""", - true - ) - logger.trace { "$line: " + ((if (!autoCorrect) "would have " else "") + "inserted newline before ${node.text}") } - if (autoCorrect) { - (node.psi as LeafPsiElement).upsertWhitespaceBeforeMe("\n ") - } - } - - private fun requireNewlineAfterLeaf( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - emit( - node.startOffset + 1, - """Missing newline after "${node.text}"""", - true - ) - logger.trace { "$line: " + (if (!autoCorrect) "would have " else "") + "inserted newline after ${node.text}" } - if (autoCorrect) { - (node.psi as LeafPsiElement).upsertWhitespaceAfterMe("\n ") - } - } - private class IndentContext { private val exitAdj = mutableMapOf() val ignored = mutableSetOf() @@ -1256,16 +912,6 @@ class IndentationRule : Rule( return false } - private fun ASTNode.hasLineBreak(vararg ignoreElementTypes: IElementType): Boolean { - if (isWhiteSpaceWithNewline()) return true - return if (ignoreElementTypes.isEmpty()) { - textContains('\n') - } else { - elementType !in ignoreElementTypes && - children().any { c -> c.textContains('\n') && c.elementType !in ignoreElementTypes } - } - } - private fun ASTNode.containsMixedIndentationCharacters(): Boolean { assert((this.psi as KtStringTemplateExpression).isMultiLine()) val nonBlankLines = this diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index a48229fac9..1273bdfae3 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -36,6 +36,7 @@ public class StandardRuleSetProvider : RuleSetProvider { SpacingAroundOperatorsRule(), SpacingAroundParensRule(), SpacingAroundRangeOperatorRule(), - StringTemplateRule() + StringTemplateRule(), + WrappingRule() ) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRule.kt new file mode 100644 index 0000000000..4d34f1a2c0 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRule.kt @@ -0,0 +1,439 @@ +package com.pinterest.ktlint.ruleset.standard + +import com.pinterest.ktlint.core.EditorConfig.Companion.loadEditorConfig +import com.pinterest.ktlint.core.EditorConfig.Companion.loadIndentConfig +import com.pinterest.ktlint.core.IndentConfig +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION +import com.pinterest.ktlint.core.ast.ElementType.ARROW +import com.pinterest.ktlint.core.ast.ElementType.CLOSING_QUOTE +import com.pinterest.ktlint.core.ast.ElementType.COMMA +import com.pinterest.ktlint.core.ast.ElementType.CONDITION +import com.pinterest.ktlint.core.ast.ElementType.FUN +import com.pinterest.ktlint.core.ast.ElementType.FUNCTION_LITERAL +import com.pinterest.ktlint.core.ast.ElementType.GT +import com.pinterest.ktlint.core.ast.ElementType.LAMBDA_EXPRESSION +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.LPAR +import com.pinterest.ktlint.core.ast.ElementType.LT +import com.pinterest.ktlint.core.ast.ElementType.OBJECT_LITERAL +import com.pinterest.ktlint.core.ast.ElementType.RBRACE +import com.pinterest.ktlint.core.ast.ElementType.RBRACKET +import com.pinterest.ktlint.core.ast.ElementType.REGULAR_STRING_PART +import com.pinterest.ktlint.core.ast.ElementType.RPAR +import com.pinterest.ktlint.core.ast.ElementType.STRING_TEMPLATE +import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_CALL_ENTRY +import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_ENTRY +import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_LIST +import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT +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.WHEN_ENTRY +import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE +import com.pinterest.ktlint.core.ast.children +import com.pinterest.ktlint.core.ast.isPartOfComment +import com.pinterest.ktlint.core.ast.isWhiteSpaceWithNewline +import com.pinterest.ktlint.core.ast.isWhiteSpaceWithoutNewline +import com.pinterest.ktlint.core.ast.lineIndent +import com.pinterest.ktlint.core.ast.nextCodeLeaf +import com.pinterest.ktlint.core.ast.nextCodeSibling +import com.pinterest.ktlint.core.ast.nextLeaf +import com.pinterest.ktlint.core.ast.nextSibling +import com.pinterest.ktlint.core.ast.prevCodeLeaf +import com.pinterest.ktlint.core.ast.prevLeaf +import com.pinterest.ktlint.core.ast.prevSibling +import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe +import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe +import com.pinterest.ktlint.core.ast.visit +import com.pinterest.ktlint.core.initKtLintKLogger +import mu.KotlinLogging +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiComment +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType +import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet +import org.jetbrains.kotlin.psi.KtStringTemplateExpression +import org.jetbrains.kotlin.psi.KtSuperTypeList + +private val logger = KotlinLogging.logger {}.initKtLintKLogger() + +/** + * This rule inserts missing newlines (e.g. between parentheses of a multi-line function call). This logic previously + * was part of the IndentationRule (phase 1). + * + * Current limitations: + * - "all or nothing" (currently, rule can only be disabled for an entire file) + * - Whenever a linebreak is inserted, this rules assumes that the parent node it indented correctly. So the indentation + * is fixed with respect to indentation of the parent. This is just a simple best effort for the case that the + * indentation rule is not run. + */ +public class WrappingRule : Rule( + id = "wrapping", + visitorModifiers = setOf(VisitorModifier.RunOnRootNodeOnly) +) { + private companion object { + private val lTokenSet = TokenSet.create(LPAR, LBRACE, LBRACKET, LT) + private val rTokenSet = TokenSet.create(RPAR, RBRACE, RBRACKET, GT) + private val matchingRToken = + lTokenSet.types.zip( + rTokenSet.types + ).toMap() + } + + private var line = 1 + private lateinit var indentConfig: IndentConfig + + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + line = 1 + indentConfig = node.loadEditorConfig().loadIndentConfig() + node.visit { n -> // TODO: Check whether this visit can be removed like other rules. This would disabling the rule for blocks and lines + when (n.elementType) { + LPAR, LBRACE, LBRACKET -> rearrangeBlock(n, autoCorrect, emit) // TODO: LT + SUPER_TYPE_LIST -> rearrangeSuperTypeList(n, autoCorrect, emit) + VALUE_PARAMETER_LIST, VALUE_ARGUMENT_LIST -> rearrangeValueList(n, autoCorrect, emit) + ARROW -> rearrangeArrow(n, autoCorrect, emit) + WHITE_SPACE -> line += n.text.count { it == '\n' } + CLOSING_QUOTE -> rearrangeClosingQuote(n, autoCorrect, emit) + } + } + } + + private fun rearrangeBlock( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val rElementType = matchingRToken[node.elementType] + var newlineInBetween = false + var parameterListInBetween = false + var numberOfArgs = 0 + var firstArg: ASTNode? = null + // matching ), ] or } + val r = node.nextSibling { + val isValueArgument = it.elementType == VALUE_ARGUMENT + val hasLineBreak = if (isValueArgument) it.hasLineBreak(LAMBDA_EXPRESSION, FUN) else it.hasLineBreak() + newlineInBetween = newlineInBetween || hasLineBreak + parameterListInBetween = parameterListInBetween || it.elementType == VALUE_PARAMETER_LIST + if (isValueArgument) { + numberOfArgs++ + firstArg = it + } + it.elementType == rElementType + }!! + if ( + !newlineInBetween || + // keep { p -> + // } + (node.elementType == LBRACE && parameterListInBetween) || + // keep ({ + // }) and (object : C { + // }) + ( + numberOfArgs == 1 && + firstArg?.firstChildNode?.elementType + ?.let { it == OBJECT_LITERAL || it == LAMBDA_EXPRESSION } == true + ) + ) { + return + } + if (!node.nextCodeLeaf()?.prevLeaf { + // Skip comments, whitespace, and empty nodes + !it.isPartOfComment() && + !it.isWhiteSpaceWithoutNewline() && + it.textLength > 0 + }.isWhiteSpaceWithNewline() && + // IDEA quirk: + // if (true && + // true + // ) { + // } + // instead of + // if ( + // true && + // true + // ) { + // } + node.treeNext?.elementType != CONDITION + ) { + requireNewlineAfterLeaf(node, autoCorrect, emit) + } + if (!r.prevLeaf().isWhiteSpaceWithNewline()) { + requireNewlineBeforeLeaf(r, autoCorrect, emit, node.treeParent.lineIndent()) + } + } + + private fun rearrangeSuperTypeList( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val entries = (node.psi as KtSuperTypeList).entries + if ( + node.textContains('\n') && + entries.size > 1 && + // e.g. + // + // class A : B, C, + // D + // or + // class A : B, C({ + // }), D + // + // but not + // + // class A : B, C, D({ + // }) + !( + entries.dropLast(1).all { it.elementType == SUPER_TYPE_ENTRY } && + entries.last().elementType == SUPER_TYPE_CALL_ENTRY + ) + ) { + // put space after : + if (!node.prevLeaf().isWhiteSpaceWithNewline()) { + val colon = node.prevCodeLeaf()!! + if ( + !colon.prevLeaf().isWhiteSpaceWithNewline() && + colon.prevCodeLeaf().let { it?.elementType != RPAR || !it.prevLeaf().isWhiteSpaceWithNewline() } + ) { + requireNewlineAfterLeaf(colon, autoCorrect, emit, node.lineIndent() + indentConfig.indent) + } + } + // put entries on separate lines + // TODO: group emit()s below with the one above into one (similar to ParameterListWrappingRule) + for (c in node.children()) { + if (c.elementType == COMMA && !c.treeNext.isWhiteSpaceWithNewline()) { + requireNewlineAfterLeaf( + nodeAfterWhichNewlineIsRequired = c, + autoCorrect = autoCorrect, + emit = emit, + indent = node.lineIndent() + ) + } + } + } + } + + private fun rearrangeValueList( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + for (c in node.children()) { + val hasLineBreak = when (c.elementType) { + VALUE_ARGUMENT -> c.hasLineBreak(LAMBDA_EXPRESSION, FUN) + VALUE_PARAMETER, ANNOTATION -> c.hasLineBreak() + else -> false + } + if (hasLineBreak) { + // rearrange + // + // a, b, value( + // ), c, d + // + // to + // + // a, b, + // value( + // ), + // c, d + + // insert \n in front of multi-line value + val prevSibling = c.prevSibling { it.elementType != WHITE_SPACE } + if ( + prevSibling?.elementType == COMMA && + !prevSibling.treeNext.isWhiteSpaceWithNewline() + ) { + requireNewlineAfterLeaf(prevSibling, autoCorrect, emit) + } + // insert \n after multi-line value + val nextSibling = c.nextSibling { it.elementType != WHITE_SPACE } + if ( + nextSibling?.elementType == COMMA && + !nextSibling.treeNext.isWhiteSpaceWithNewline() && + // value( + // ), // a comment + // c, d + nextSibling.treeNext?.treeNext?.psi !is PsiComment + ) { + requireNewlineAfterLeaf(nextSibling, autoCorrect, emit) + } + } + } + } + + private fun rearrangeClosingQuote( + n: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val treeParent = n.treeParent + if (treeParent.elementType == STRING_TEMPLATE) { + val treeParentPsi = treeParent.psi as KtStringTemplateExpression + if (treeParentPsi.isMultiLine() && n.treePrev.text.isNotBlank()) { + // rewriting + // """ + // text + // _""".trimIndent() + // to + // """ + // text + // _ + // """.trimIndent() + emit( + n.startOffset, + "Missing newline before \"\"\"", + true + ) + if (autoCorrect) { + val newIndent = + treeParent.lineIndent() + + if (n.elementType == CLOSING_QUOTE) { + "" + } else { + indentConfig.indent + } + n as LeafPsiElement + n.rawInsertBeforeMe( + LeafPsiElement( + REGULAR_STRING_PART, + "\n" + newIndent + ) + ) + } + logger.trace { "$line: " + (if (!autoCorrect) "would have " else "") + "inserted newline before (closing) \"\"\"" } + } + } + } + + private fun mustBeFollowedByNewline(node: ASTNode): Boolean { + // find EOL token (last token before \n) + // if token is in lTokenSet + // find matching rToken + // return true if there is no newline after the rToken + // return false + val nextCodeSibling = node.nextCodeSibling() // e.g. BINARY_EXPRESSION + var lToken = nextCodeSibling?.nextLeaf { it.isWhiteSpaceWithNewline() }?.prevCodeLeaf() + if (lToken != null && lToken.elementType !in lTokenSet) { + // special cases: + // x = y.f({ z -> + // }) + // x = y.f(0, 1, + // 2, 3) + lToken = lToken.prevLeaf { it.elementType in lTokenSet || it == node } + } + if (lToken != null && lToken.elementType in lTokenSet) { + val rElementType = matchingRToken[lToken.elementType] + val rToken = lToken.nextSibling { it.elementType == rElementType } + return rToken?.treeParent == lToken.treeParent + } + if (nextCodeSibling?.textContains('\n') == false) { + return true + } + return false + } + + private fun rearrangeArrow( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val p = node.treeParent + if ( + // check + // `{ p -> ... }` + // and + // `when { m -> ... }` + // only + p.elementType.let { it != FUNCTION_LITERAL && it != WHEN_ENTRY } || + // ... and only if expression after -> spans multiple lines + !p.textContains('\n') || + // permit + // when { + // m -> 0 + d({ + // }) + // } + (p.elementType == WHEN_ENTRY && mustBeFollowedByNewline(node)) || + // permit + // when (this) { + // in 0x1F600..0x1F64F, // Emoticons + // 0x200D // Zero-width Joiner + // -> true + // } + (p.elementType == WHEN_ENTRY && node.prevLeaf()?.textContains('\n') == true) + ) { + return + } + if (!node.nextCodeLeaf()?.prevLeaf().isWhiteSpaceWithNewline()) { + requireNewlineAfterLeaf(node, autoCorrect, emit) + } + val r = node.nextSibling { it.elementType == RBRACE } ?: return + if (!r.prevLeaf().isWhiteSpaceWithNewline()) { + requireNewlineBeforeLeaf(r, autoCorrect, emit, node.lineIndent()) + } + } + + private fun requireNewlineBeforeLeaf( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + indent: String + ) { + emit( + node.startOffset - 1, + """Missing newline before "${node.text}"""", + true + ) + logger.trace { "$line: " + ((if (!autoCorrect) "would have " else "") + "inserted newline before ${node.text}") } + if (autoCorrect) { + (node.psi as LeafPsiElement).upsertWhitespaceBeforeMe("\n" + indent) + } + } + + private fun requireNewlineAfterLeaf( + nodeAfterWhichNewlineIsRequired: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + indent: String? = null, + nodeToFix: ASTNode = nodeAfterWhichNewlineIsRequired + ) { + emit( + nodeAfterWhichNewlineIsRequired.startOffset + 1, + """Missing newline after "${nodeAfterWhichNewlineIsRequired.text}"""", + true + ) + logger.trace { "$line: " + (if (!autoCorrect) "would have " else "") + "inserted newline after ${nodeAfterWhichNewlineIsRequired.text}" } + if (autoCorrect) { + val tempIndent = indent ?: (nodeToFix.lineIndent() + indentConfig.indent) + (nodeToFix.psi as LeafPsiElement).upsertWhitespaceAfterMe("\n" + tempIndent) + } + } + + private fun KtStringTemplateExpression.isMultiLine(): Boolean { + for (child in node.children()) { + if (child.elementType == LITERAL_STRING_TEMPLATE_ENTRY) { + val v = child.text + if (v == "\n") { + return true + } + } + } + return false + } + + private fun ASTNode.hasLineBreak(vararg ignoreElementTypes: IElementType): Boolean { + if (isWhiteSpaceWithNewline()) return true + return if (ignoreElementTypes.isEmpty()) { + textContains('\n') + } else { + elementType !in ignoreElementTypes && + children().any { c -> c.textContains('\n') && c.elementType !in ignoreElementTypes } + } + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt index 3a947c38db..89335e01f7 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/IndentationRuleTest.kt @@ -156,8 +156,9 @@ internal class IndentationRuleTest { @Test fun testFormatRawStringTrimIndent() { + // TODO: Split into simple unit tests not using diffFileFormat and distinct between indentation and wrapping assertThat( - IndentationRule().diffFileFormat( + wrappingAndIndentRule.diffFileFormat( "spec/indent/format-raw-string-trim-indent.kt.spec", "spec/indent/format-raw-string-trim-indent-expected.kt.spec" ) @@ -171,13 +172,13 @@ internal class IndentationRuleTest { @Test fun testLintSuperType() { - assertThat(IndentationRule().diffFileLint("spec/indent/lint-supertype.kt.spec")).isEmpty() + assertThat(wrappingAndIndentRule.diffFileLint("spec/indent/lint-supertype.kt.spec")).isEmpty() } @Test fun testFormatSuperType() { assertThat( - IndentationRule().diffFileFormat( + wrappingAndIndentRule.diffFileFormat( "spec/indent/format-supertype.kt.spec", "spec/indent/format-supertype-expected.kt.spec" ) @@ -229,7 +230,7 @@ internal class IndentationRuleTest { @Test fun testLintWhenExpression() { - assertThat(IndentationRule().diffFileLint("spec/indent/lint-when-expression.kt.spec")).isEmpty() + assertThat(wrappingAndIndentRule.diffFileLint("spec/indent/lint-when-expression.kt.spec")).isEmpty() } @Test @@ -254,8 +255,9 @@ internal class IndentationRuleTest { @Test fun testFormatMultilineString() { + // TODO: Split into simple unit tests not using diffFileFormat and distinct between indentation and wrapping assertThat( - IndentationRule().diffFileFormat( + wrappingAndIndentRule.diffFileFormat( "spec/indent/format-multiline-string.kt.spec", "spec/indent/format-multiline-string-expected.kt.spec" ) @@ -265,7 +267,7 @@ internal class IndentationRuleTest { @Test fun testFormatArrow() { assertThat( - IndentationRule().diffFileFormat( + wrappingAndIndentRule.diffFileFormat( "spec/indent/format-arrow.kt.spec", "spec/indent/format-arrow-expected.kt.spec" ) @@ -275,7 +277,7 @@ internal class IndentationRuleTest { @Test fun testFormatEq() { assertThat( - IndentationRule().diffFileFormat( + wrappingAndIndentRule.diffFileFormat( "spec/indent/format-eq.kt.spec", "spec/indent/format-eq-expected.kt.spec" ) @@ -284,24 +286,16 @@ internal class IndentationRuleTest { @Test fun testFormatParameterList() { + // TODO: Parameter and argument list do have a dedicated wrapping rule. This functionality should therefore be + // removed from the generic rule. assertThat( - IndentationRule().diffFileFormat( + wrappingAndIndentRule.diffFileFormat( "spec/indent/format-parameter-list.kt.spec", "spec/indent/format-parameter-list-expected.kt.spec" ) ).isEmpty() } - @Test - fun testFormatArgumentList() { - assertThat( - IndentationRule().diffFileFormat( - "spec/indent/format-argument-list.kt.spec", - "spec/indent/format-argument-list-expected.kt.spec" - ) - ).isEmpty() - } - @Test // "https://github.com/shyiko/ktlint/issues/180" fun testLintWhereClause() { assertThat( @@ -632,53 +626,6 @@ internal class IndentationRuleTest { assertThat(IndentationRule().format(code)).isEqualTo(code) } - @Test - fun `format new line before opening quotes multiline string as parameter`() { - val code = - """ - fun foo() { - println($MULTILINE_STRING_QUOTE - line1 - line2 - $MULTILINE_STRING_QUOTE.trimIndent()) - } - """.trimIndent() - val expectedCode = - """ - fun foo() { - println( - $MULTILINE_STRING_QUOTE - line1 - line2 - $MULTILINE_STRING_QUOTE.trimIndent() - ) - } - """.trimIndent() - - @Suppress("RemoveCurlyBracesFromTemplate") - val expectedCodeTabs = - """ - fun foo() { - ${TAB}println( - ${TAB}${TAB}$MULTILINE_STRING_QUOTE - ${TAB}${TAB}line1 - ${TAB}${TAB} line2 - ${TAB}${TAB}$MULTILINE_STRING_QUOTE.trimIndent() - ${TAB}) - } - """.trimIndent() - assertThat( - IndentationRule().lint(code) - ).isEqualTo( - listOf( - LintError(2, 13, "indent", "Missing newline after \"(\""), - LintError(5, 24, "indent", "Missing newline before \")\"") - ) - ) - assertThat(IndentationRule().format(code)).isEqualTo(expectedCode) - assertThat(IndentationRule().format(code, INDENT_STYLE_TABS)).isEqualTo(expectedCodeTabs) - } - @Test fun `format multiline string assignment to variable with opening quotes on same line as declaration`() { val code = @@ -714,12 +661,14 @@ internal class IndentationRuleTest { val code = """ fun foo() { - println($MULTILINE_STRING_QUOTE + println( + $MULTILINE_STRING_QUOTE text "" text "" - $MULTILINE_STRING_QUOTE.trimIndent()) + $MULTILINE_STRING_QUOTE.trimIndent() + ) } """.trimIndent() val expectedCode = @@ -739,9 +688,7 @@ internal class IndentationRuleTest { IndentationRule().lint(code) ).isEqualTo( listOf( - LintError(line = 2, col = 13, ruleId = "indent", detail = "Missing newline after \"(\""), - LintError(line = 7, col = 1, ruleId = "indent", detail = "Unexpected indent of multiline string closing quotes"), - LintError(line = 7, col = 20, ruleId = "indent", detail = "Missing newline before \")\"") + LintError(line = 8, col = 1, ruleId = "indent", detail = "Unexpected indent of multiline string closing quotes") ) ) assertThat(IndentationRule().format(code)).isEqualTo(expectedCode) @@ -753,11 +700,13 @@ internal class IndentationRuleTest { val code = """ fun foo() { - println($MULTILINE_STRING_QUOTE + println( + $MULTILINE_STRING_QUOTE ${"$"}{true} ${"$"}{true} - $MULTILINE_STRING_QUOTE.trimIndent()) + $MULTILINE_STRING_QUOTE.trimIndent() + ) } """.trimIndent() val expectedCode = @@ -776,9 +725,7 @@ internal class IndentationRuleTest { IndentationRule().lint(code) ).isEqualTo( listOf( - LintError(line = 2, col = 13, ruleId = "indent", detail = "Missing newline after \"(\""), - LintError(line = 6, col = 1, ruleId = "indent", detail = "Unexpected indent of multiline string closing quotes"), - LintError(line = 6, col = 20, ruleId = "indent", detail = "Missing newline before \")\"") + LintError(line = 7, col = 1, ruleId = "indent", detail = "Unexpected indent of multiline string closing quotes") ) ) assertThat(IndentationRule().format(code)).isEqualTo(expectedCode) @@ -1350,15 +1297,15 @@ internal class IndentationRuleTest { } """.trimIndent() assertThat( - IndentationRule().lint(code) + wrappingAndIndentRule.lint(code) ).isEqualTo( listOf( - LintError(line = 2, col = 12, ruleId = "indent", detail = "Missing newline after \"(\""), + LintError(line = 2, col = 12, ruleId = "wrapping", detail = "Missing newline after \"(\""), LintError(line = 6, col = 1, ruleId = "indent", detail = "Unexpected indent of multiline string closing quotes"), - LintError(line = 6, col = 7, ruleId = "indent", detail = "Missing newline before \")\"") + LintError(line = 6, col = 7, ruleId = "wrapping", detail = "Missing newline before \")\"") ) ) - assertThat(IndentationRule().format(code)).isEqualTo(formattedCode) + assertThat(wrappingAndIndentRule.format(code)).isEqualTo(formattedCode) } @Test @@ -1732,6 +1679,31 @@ internal class IndentationRuleTest { assertThat(IndentationRule().format(code)).isEqualTo(code) } + @Test + fun `Binary expression`() { + val code = + """ + val x = "" + + "" + + f2( + "" // IDEA quirk (ignored) + ) + """.trimIndent() + assertThat(IndentationRule().lint(code)).isEmpty() + assertThat(IndentationRule().format(code)).isEqualTo(code) + } + + fun foo() { + println( + """ + text + + text + _ + """.trimIndent() + ) + } + private companion object { const val MULTILINE_STRING_QUOTE = "${'"'}${'"'}${'"'}" const val TAB = "${'\t'}" @@ -1739,5 +1711,6 @@ internal class IndentationRuleTest { val INDENT_STYLE_TABS = EditorConfigOverride.from( indentStyleProperty to PropertyType.IndentStyleValue.tab ) + val wrappingAndIndentRule = listOf(WrappingRule(), IndentationRule()) } } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRuleTest.kt new file mode 100644 index 0000000000..5dd3b4a8b4 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/WrappingRuleTest.kt @@ -0,0 +1,1380 @@ +package com.pinterest.ktlint.ruleset.standard + +import com.pinterest.ktlint.core.EditorConfig.Companion.indentSizeProperty +import com.pinterest.ktlint.core.EditorConfig.Companion.indentStyleProperty +import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.core.api.FeatureInAlphaState +import com.pinterest.ktlint.test.EditorConfigOverride +import com.pinterest.ktlint.test.diffFileFormat +import com.pinterest.ktlint.test.format +import com.pinterest.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat +import org.ec4j.core.model.PropertyType +import org.junit.jupiter.api.Test + +@FeatureInAlphaState +internal class WrappingRuleTest { + @Test + fun testLintIndentSizeUnset() { + assertThat( + WrappingRule().lint( + """ + fun main() { + val v = "" + println(v) + } + """.trimIndent(), + EditorConfigOverride.from(indentSizeProperty to "unset") + ) + ).isEmpty() + } + + @Test + fun testFormatRawStringTrimIndent() { + // TODO: Split into simple unit tests not using diffFileFormat and distinct between indentation and wrapping + assertThat( + wrappingAndIndentRule.diffFileFormat( + "spec/indent/format-raw-string-trim-indent.kt.spec", + "spec/indent/format-raw-string-trim-indent-expected.kt.spec" + ) + ).isEmpty() + } + + @Test + fun testFormatSuperType() { + assertThat( + WrappingRule().diffFileFormat( + "spec/wrapping/format-supertype.kt.spec", + "spec/wrapping/format-supertype-expected.kt.spec" + ) + ).isEmpty() + } + + @Test + fun testFormatMultilineString() { + // TODO: Split into simple unit tests not using diffFileFormat and distinct between indentation and wrapping + assertThat( + wrappingAndIndentRule.diffFileFormat( + "spec/indent/format-multiline-string.kt.spec", + "spec/indent/format-multiline-string-expected.kt.spec" + ) + ).isEmpty() + } + + @Test + fun testFormatArrow() { + // TODO: Split into simple unit tests not using diffFileFormat and distinct between indentation and wrapping + assertThat( + wrappingAndIndentRule.diffFileFormat( + "spec/indent/format-arrow.kt.spec", + "spec/indent/format-arrow-expected.kt.spec" + ) + ).isEmpty() + } + + @Test + fun testFormatParameterList() { + // TODO: Parameter and argument list do have a dedicated wrapping rule. This functionality should therefore be + // removed from the generic rule. + assertThat( + wrappingAndIndentRule.diffFileFormat( + "spec/wrapping/format-parameter-list.kt.spec", + "spec/wrapping/format-parameter-list-expected.kt.spec" + ) + ).isEmpty() + } + + @Test + fun testFormatArgumentList() { + // TODO: Parameter and argument list do have a dedicated wrapping rule. This functionality should therefore be + // removed from the generic rule. + assertThat( + wrappingAndIndentRule.diffFileFormat( + "spec/wrapping/format-argument-list.kt.spec", + "spec/wrapping/format-argument-list-expected.kt.spec" + ) + ).isEmpty() + } + + @Test // "https://github.com/shyiko/ktlint/issues/180" + fun testLintWhereClause() { + assertThat( + WrappingRule().lint( + """ + class BiAdapter( + val adapter1: A1, + val adapter2: A2 + ) : RecyclerView.Adapter() + where A1 : RecyclerView.Adapter, A1 : ComposableAdapter.ViewTypeProvider, + A2 : RecyclerView.Adapter, A2 : ComposableAdapter.ViewTypeProvider { + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test // "https://github.com/pinterest/ktlint/issues/433" + fun testLintParameterListWithComments() { + assertThat( + WrappingRule().lint( + """ + fun main() { + foo( + /*param1=*/param1, + /*param2=*/param2 + ) + + foo( + /*param1=*/ param1, + /*param2=*/ param2 + ) + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun testLintNewlineAfterEqAllowed() { + assertThat( + WrappingRule().lint( + // Previously the IndentationRule would force the line break after the `=`. Verify that it is + // still allowed. + """ + private fun getImplementationVersion() = + javaClass.`package`.implementationVersion + ?: javaClass.getResourceAsStream("/META-INF/MANIFEST.MF") + ?.let { stream -> + Manifest(stream).mainAttributes.getValue("Implementation-Version") + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint indentation new line before return type`() { + assertThat( + WrappingRule().lint( + """ + abstract fun doPerformSomeOperation(param: ALongParameter): + SomeLongInterface + val s: + String = "" + fun process( + fileName: + String + ): List + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint trailing comment in multiline parameter is allowed`() { + assertThat( + WrappingRule().lint( + """ + fun foo(param: Foo, other: String) { + foo( + param = param + .copy(foo = ""), // A comment + other = "" + ) + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `format trailing comment in multiline parameter is allowed`() { + val code = + """ + fun foo(param: Foo, other: String) { + foo( + param = param + .copy(foo = ""), // A comment + other = "" + ) + } + """.trimIndent() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `lint safe-called wrapped trailing lambda is allowed`() { + assertThat( + WrappingRule().lint( + """ + val foo = bar + ?.filter { number -> + number == 0 + }?.map { evenNumber -> + evenNumber * evenNumber + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `format safe-called wrapped trailing lambda is allowed`() { + val code = + """ + val foo = bar + ?.filter { number -> + number == 0 + }?.map { evenNumber -> + evenNumber * evenNumber + } + """.trimIndent() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `lint block started with parens after if is allowed`() { + val code = + """ + fun test() { + if (true) + (1).toString() + else + 2.toString() + } + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + } + + @Test + fun `format block started with parens after if is allowed`() { + val code = + """ + fun test() { + if (true) + (1).toString() + else + 2.toString() + } + """.trimIndent() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + // https://github.com/pinterest/ktlint/issues/796 + @Test + fun `lint if-condition with multiline call expression is indented properly`() { + val code = + """ + private val gpsRegion = + if (permissionHandler.isPermissionGranted( + context, Manifest.permission.ACCESS_FINE_LOCATION + ) + ) { + // stuff + } + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + } + + @Test + fun `format if-condition with multiline call expression is indented properly`() { + val code = + """ + private val gpsRegion = + if (permissionHandler.isPermissionGranted( + context, Manifest.permission.ACCESS_FINE_LOCATION + ) + ) { + // stuff + } + """.trimIndent() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `format new line before opening quotes multiline string as parameter`() { + val code = + """ + fun foo() { + println($MULTILINE_STRING_QUOTE + line1 + line2 + $MULTILINE_STRING_QUOTE.trimIndent()) + } + """.trimIndent() + val expectedCode = + """ + fun foo() { + println( + $MULTILINE_STRING_QUOTE + line1 + line2 + $MULTILINE_STRING_QUOTE.trimIndent() + ) + } + """.trimIndent() + + assertThat( + WrappingRule().lint(code) + ).isEqualTo( + listOf( + LintError(2, 13, "wrapping", "Missing newline after \"(\""), + LintError(5, 24, "wrapping", "Missing newline before \")\"") + ) + ) + assertThat(WrappingRule().format(code)).isEqualTo(expectedCode) + } + + @Test + @Suppress("RemoveCurlyBracesFromTemplate") + fun `format new line before opening quotes multiline string as parameter with tab spacing`() { + val code = + """ + fun foo() { + ${TAB}println($MULTILINE_STRING_QUOTE + ${TAB}${TAB}line1 + ${TAB}${TAB} line2 + ${TAB}${TAB}$MULTILINE_STRING_QUOTE.trimIndent()) + } + """.trimIndent() + val expectedCode = + """ + fun foo() { + ${TAB}println( + ${TAB}${TAB}$MULTILINE_STRING_QUOTE + ${TAB}${TAB}line1 + ${TAB}${TAB} line2 + ${TAB}${TAB}$MULTILINE_STRING_QUOTE.trimIndent() + ${TAB}) + } + """.trimIndent() + assertThat( + WrappingRule().lint(code, INDENT_STYLE_TABS) + ).isEqualTo( + listOf( + LintError(2, 10, "wrapping", "Missing newline after \"(\""), + LintError(5, 18, "wrapping", "Missing newline before \")\"") + ) + ) + assertThat(WrappingRule().format(code, INDENT_STYLE_TABS)).isEqualTo(expectedCode) + } + + @Test + fun `format multiline string containing quotation marks`() { + val code = + """ + fun foo() { + println($MULTILINE_STRING_QUOTE + text "" + + text + "" + $MULTILINE_STRING_QUOTE.trimIndent()) + } + """.trimIndent() + val expectedCode = + """ + fun foo() { + println( + $MULTILINE_STRING_QUOTE + text "" + + text + "" + $MULTILINE_STRING_QUOTE.trimIndent() + ) + } + """.trimIndent() + assertThat( + WrappingRule().lint(code) + ).isEqualTo( + listOf( + LintError(line = 2, col = 13, ruleId = "wrapping", detail = "Missing newline after \"(\""), + LintError(line = 7, col = 24, ruleId = "wrapping", detail = "Missing newline before \")\"") + ) + ) + assertThat(WrappingRule().format(code)).isEqualTo(expectedCode) + } + + @Test + fun `format multiline string containing a template string as the first non blank element on the line`() { + // Escape '${true}' as '${"$"}{true}' to prevent evaluation before actually processing the multiline sting + val code = + """ + fun foo() { + println($MULTILINE_STRING_QUOTE + ${"$"}{true} + + ${"$"}{true} + $MULTILINE_STRING_QUOTE.trimIndent()) + } + """.trimIndent() + val expectedCode = + """ + fun foo() { + println( + $MULTILINE_STRING_QUOTE + ${"$"}{true} + + ${"$"}{true} + $MULTILINE_STRING_QUOTE.trimIndent() + ) + } + """.trimIndent() + assertThat( + WrappingRule().lint(code) + ).isEqualTo( + listOf( + LintError(line = 2, col = 13, ruleId = "wrapping", detail = "Missing newline after \"(\""), + LintError(line = 6, col = 24, ruleId = "wrapping", detail = "Missing newline before \")\"") + ) + ) + assertThat(WrappingRule().format(code)).isEqualTo(expectedCode) + } + + @Test + fun `issue 575 - format multiline string with tabs after the margin is indented properly`() { + val code = + """ + val str = + $MULTILINE_STRING_QUOTE + ${TAB}Tab at the beginning of this line but after the indentation margin + Tab${TAB}in the middle of this string + Tab at the end of this line.$TAB + $MULTILINE_STRING_QUOTE.trimIndent() + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `lint if-condition with line break and multiline call expression is indented properly`() { + assertThat( + WrappingRule().lint( + """ + // https://github.com/pinterest/ktlint/issues/871 + fun function(param1: Int, param2: Int, param3: Int?): Boolean { + return if ( + listOf( + param1, + param2, + param3 + ).none { it != null } + ) { + true + } else { + false + } + } + + // https://github.com/pinterest/ktlint/issues/900 + enum class Letter(val value: String) { + A("a"), + B("b"); + } + fun broken(key: String): Letter { + for (letter in Letter.values()) { + if ( + letter.value + .equals( + key, + ignoreCase = true + ) + ) { + return letter + } + } + return Letter.B + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint property delegate is indented properly`() { + assertThat( + WrappingRule().lint( + """ + val i: Int + by lazy { 1 } + + val j = 0 + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint property delegate is indented properly 2`() { + assertThat( + WrappingRule().lint( + """ + val i: Int + by lazy { + "".let { + println(it) + } + 1 + } + + val j = 0 + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint property delegate is indented properly 3`() { + assertThat( + WrappingRule().lint( + """ + val i: Int by lazy { + "".let { + println(it) + } + 1 + } + + val j = 0 + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint property delegate is indented properly 4`() { + assertThat( + WrappingRule().lint( + """ + fun lazyList() = lazy { mutableListOf() } + + class Test { + val list: List + by lazyList() + + val aVar = 0 + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint property delegate is indented properly 5`() { + assertThat( + WrappingRule().lint( + """ + fun lazyList(a: Int, b: Int) = lazy { mutableListOf() } + + class Test { + val list: List + by lazyList( + 1, + 2 + ) + + val aVar = 0 + } + """.trimIndent() + ) + ).isEmpty() + } + + // https://github.com/pinterest/ktlint/issues/1210 + @Test + fun `lint delegated properties with a lambda argument`() { + assertThat( + WrappingRule().lint( + """ + import kotlin.properties.Delegates + + class Test { + private var test + by Delegates.vetoable("") { _, old, new -> + true + } + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint delegation 1`() { + assertThat( + WrappingRule().lint( + """ + interface Foo + + class Bar(a: Int, b: Int, c: Int) : Foo + + class Test1 : Foo by Bar( + a = 1, + b = 2, + c = 3 + ) + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint and format delegation 2`() { + val code = + """ + class Test2 : Foo + by Bar( + a = 1, + b = 2, + c = 3 + ) + """.trimIndent() + assertThat(WrappingRule().format(code)).isEqualTo(code) + assertThat(WrappingRule().lint(code)).isEmpty() + } + + @Test + fun `lint delegation 3`() { + assertThat( + WrappingRule().lint( + """ + interface Foo + + class Bar(a: Int, b: Int, c: Int) : Foo + + class Test3 : + Foo by Bar( + a = 1, + b = 2, + c = 3 + ) + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint delegation 4`() { + assertThat( + WrappingRule().lint( + """ + interface Foo + + class Bar(a: Int, b: Int, c: Int) : Foo + + class Test4 : + Foo + by Bar( + a = 1, + b = 2, + c = 3 + ) + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint delegation 5`() { + assertThat( + WrappingRule().lint( + """ + interface Foo + + class Bar(a: Int, b: Int, c: Int) : Foo + + class Test5 { + companion object : Foo by Bar( + a = 1, + b = 2, + c = 3 + ) + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint delegation 6`() { + assertThat( + WrappingRule().lint( + """ + data class Shortcut(val id: String, val url: String) + + object Someclass : List by listOf( + Shortcut( + id = "1", + url = "url" + ), + Shortcut( + id = "2", + url = "asd" + ), + Shortcut( + id = "3", + url = "TV" + ) + ) + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint named argument`() { + assertThat( + WrappingRule().lint( + """ + data class D(val a: Int, val b: Int, val c: Int) + + fun test() { + val d = D( + a = 1, + b = + 2, + c = 3 + ) + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint default parameter`() { + assertThat( + WrappingRule().lint( + """ + data class D( + val a: Int = 1, + val b: Int = + 2, + val c: Int = 3 + ) + """.trimIndent() + ) + ).isEmpty() + } + + // https://github.com/pinterest/ktlint/issues/959 + @Test + fun `lint conditions with multi-line call expressions indented properly`() { + assertThat( + WrappingRule().lint( + """ + fun test() { + val result = true && + minOf( + 1, 2 + ) == 2 + } + """.trimIndent() + ) + ).isEmpty() + } + + // https://github.com/pinterest/ktlint/issues/1003 + @Test + fun `lint multiple interfaces`() { + assertThat( + WrappingRule().lint( + """ + abstract class Parent(a: Int, b: Int) + + interface Parent2 + + class Child( + a: Int, + b: Int + ) : Parent( + a, + b + ), + Parent2 + """.trimIndent() + ) + ).isEmpty() + } + + // https://github.com/pinterest/ktlint/issues/918 + @Test + fun `lint newline after type reference in functions`() { + assertThat( + WrappingRule().lint( + """ + override fun actionProcessor(): + ObservableTransformer = + ObservableTransformer { actions -> + // ... + } + + fun generateGooooooooooooooooogle(): + Gooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooogle { + return Gooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooogle() + } + """.trimIndent() + ) + ).isEmpty() + } + + // https://github.com/pinterest/ktlint/issues/764 + @Test + fun `lint value argument list with lambda`() { + assertThat( + WrappingRule().lint( + """ + fun test(i: Int, f: (Int) -> Unit) { + f(i) + } + + fun main() { + test(1, f = { + println(it) + }) + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint value argument list with two lambdas`() { + assertThat( + WrappingRule().lint( + """ + fun test(f: () -> Unit, g: () -> Unit) { + f() + g() + } + + fun main() { + test({ + println(1) + }, { + println(2) + }) + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint value argument list with anonymous function`() { + assertThat( + WrappingRule().lint( + """ + fun test(i: Int, f: (Int) -> Unit) { + f(i) + } + + fun main() { + test(1, fun(it: Int) { + println(it) + }) + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint value argument list with lambda in super type entry`() { + assertThat( + WrappingRule().lint( + """ + class A : B({ + 1 + }) { + val a = 1 + } + + open class B(f: () -> Int) + """.trimIndent() + ) + ).isEmpty() + } + + // https://github.com/pinterest/ktlint/issues/1202 + @Test + fun `lint lambda argument and call chain`() { + assertThat( + WrappingRule().lint( + """ + class Foo { + fun bar() { + val foo = bar.associateBy({ item -> item.toString() }, ::someFunction).toMap() + } + } + """.trimIndent() + ) + ).isEmpty() + } + + // https://github.com/pinterest/ktlint/issues/1165 + @Test + fun `lint multiline expression with elvis operator in assignment`() { + assertThat( + WrappingRule().lint( + """ + fun test() { + val a: String = "" + + val someTest: Int? + + someTest = + a + .toIntOrNull() + ?: 1 + } + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `multi line string at start of line`() { + val code = + """ + fun foo() = + $MULTILINE_STRING_QUOTE + some text + $MULTILINE_STRING_QUOTE + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a multi line string but closing quotes not a separate line then wrap them to a new line`() { + val code = + """ + fun foo() = + $MULTILINE_STRING_QUOTE + some text$MULTILINE_STRING_QUOTE + """.trimIndent() + val formattedCode = + """ + fun foo() = + $MULTILINE_STRING_QUOTE + some text + $MULTILINE_STRING_QUOTE + """.trimIndent() + assertThat(WrappingRule().lint(code)).containsExactly( + LintError(3, 14, "wrapping", "Missing newline before \"\"\"") + ) + assertThat(WrappingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Issue 1127 - multiline string in parameter list`() { + val code = + """ + interface UserRepository : JpaRepository { + @Query($MULTILINE_STRING_QUOTE + select u from User u + inner join Organization o on u.organization = o + where o = :organization + $MULTILINE_STRING_QUOTE) + fun findByOrganization(organization: Organization, pageable: Pageable): Page + } + """.trimIndent() + val formattedCode = + """ + interface UserRepository : JpaRepository { + @Query( + $MULTILINE_STRING_QUOTE + select u from User u + inner join Organization o on u.organization = o + where o = :organization + $MULTILINE_STRING_QUOTE + ) + fun findByOrganization(organization: Organization, pageable: Pageable): Page + } + """.trimIndent() + assertThat( + WrappingRule().lint(code) + ).isEqualTo( + listOf( + LintError(line = 2, col = 12, ruleId = "wrapping", detail = "Missing newline after \"(\""), + LintError(line = 6, col = 11, ruleId = "wrapping", detail = "Missing newline before \")\"") + ) + ) + assertThat(WrappingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `format kdoc`() { + @Suppress("RemoveCurlyBracesFromTemplate") + val code = + """ + /** + * some function1 + */ + fun someFunction1() { + return Unit + } + + class SomeClass { + /** + * some function2 + */ + fun someFunction2() { + return Unit + } + } + """.trimIndent() + + @Suppress("RemoveCurlyBracesFromTemplate") + val codeTabs = + """ + /** + * some function1 + */ + fun someFunction1() { + ${TAB}return Unit + } + + class SomeClass { + ${TAB}/** + ${TAB} * some function2 + ${TAB} */ + ${TAB}fun someFunction2() { + ${TAB}${TAB}return Unit + ${TAB}} + } + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + + assertThat(WrappingRule().lint(codeTabs, INDENT_STYLE_TABS)).isEmpty() + assertThat(WrappingRule().format(codeTabs, INDENT_STYLE_TABS)).isEqualTo(codeTabs) + } + + @Test + fun `Issue 1210 - format supertype delegate`() { + val code = + """ + object ApplicationComponentFactory : ApplicationComponent.Factory + by DaggerApplicationComponent.factory() + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Issue 1210 - format of statements after supertype delegated entry 2`() { + val code = + """ + interface Foo + + class Bar(a: Int, b: Int, c: Int) : Foo + + class Test4 : + Foo + by Bar( + a = 1, + b = 2, + c = 3 + ) + + // The next line ensures that the fix regarding the expectedIndex due to alignment of "by" keyword in + // class above, is still in place. Without this fix, the expectedIndex would hold a negative value, + // resulting in the formatting to crash on the next line. + val bar = 1 + """.trimIndent() + + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Issue 1330 - Function with lambda parameter having a default value is allowed on a single line`() { + val code = + """ + fun func(lambdaArg: Unit.() -> Unit = {}, secondArg: Int) { + println() + } + fun func(lambdaArg: Unit.(a: String) -> Unit = { it -> it.toUpperCaseAsciiOnly() }, secondArg: Int) { + println() + } + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Function with multiple lambda parameters can be formatted differently`() { + val code = + """ + // https://github.com/pinterest/ktlint/issues/764#issuecomment-646822853 + val foo1 = println({ + bar() + }, { + bar() + }) + // Other formats which should be allowed as well + val foo2 = println( + { + bar() + }, + { bar() } + ) + val foo3 = println( + // Some comment + { + bar() + }, + // Some comment + { bar() } + ) + val foo4 = println( + /* Some comment */ + { + bar() + }, + /* Some comment */ + { bar() } + ) + val foo5 = println( + { bar() }, + { bar() } + ) + val foo6 = println( + // Some comment + { bar() }, + // Some comment + { bar() } + ) + val foo7 = println( + /* Some comment */ + { bar() }, + /* Some comment */ + { bar() } + ) + val foo8 = println( + { bar() }, { bar() } + ) + val foo9 = println({ bar() }, { bar()}) + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a class with one supertype with a multiline call entry then do not reformat`() { + val code = + """ + class FooBar : Foo({ + }) + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a class for which all supertypes start on the same line but the last supertype has a multiline call entry then do not reformat`() { + val code = + """ + class FooBar : Foo1, Foo2({ + }) + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a class with supertypes start on different lines then place each supertype on a separate line`() { + val code = + """ + class FooBar : Foo1, Foo2, + Bar1, Bar2 + """.trimIndent() + val formattedCode = + """ + class FooBar : + Foo1, + Foo2, + Bar1, + Bar2 + """.trimIndent() + assertThat(WrappingRule().lint(code)).containsExactly( + LintError(1, 15, "wrapping", "Missing newline after \":\""), + LintError(1, 21, "wrapping", "Missing newline after \",\""), + LintError(2, 10, "wrapping", "Missing newline after \",\"") + ) + assertThat(WrappingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a class for which the supertypes start on a next line then do not reformat`() { + val code = + """ + class FooBar : + Foo1, Foo2({ + }) + """.trimIndent() + assertThat(WrappingRule().lint(code)).isEmpty() + assertThat(WrappingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a class for which the supertypes start on a next line but they not all start on the same line then place each supertype on a separate line`() { + val code = + """ + class FooBar : + Foo1, Foo2, + Bar1, Bar2 + """.trimIndent() + val formattedCode = + """ + class FooBar : + Foo1, + Foo2, + Bar1, + Bar2 + """.trimIndent() + assertThat(WrappingRule().lint(code)).containsExactly( + LintError(2, 10, "wrapping", "Missing newline after \",\""), + LintError(3, 10, "wrapping", "Missing newline after \",\"") + ) + assertThat(WrappingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a when condition with a multiline expression without block after the arrow then start that expression on the next line`() { + val code = + """ + val bar = when (foo) { + 1 -> true + 2 -> + false + 3 -> false || + true + 4 -> false || foobar({ + }) // Special case which is allowed + else -> { + true + } + } + """.trimIndent() + val formattedCode = + """ + val bar = when (foo) { + 1 -> true + 2 -> + false + 3 -> + false || + true + 4 -> false || foobar({ + }) // Special case which is allowed + else -> { + true + } + } + """.trimIndent() + assertThat(WrappingRule().lint(code)).containsExactly( + LintError(5, 8, "wrapping", "Missing newline after \"->\"") + ) + assertThat(WrappingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given an multiline argument list which is incorrectly formatted then reformat `() { + val code = + """ + fun foo() = + bar(a, + b, + c) + """.trimIndent() + val formattedCode = + """ + fun foo() = + bar( + a, + b, + c + ) + """.trimIndent() + assertThat(WrappingRule().lint(code)).containsExactly( + LintError(2, 9, "wrapping", "Missing newline after \"(\""), + LintError(4, 9, "wrapping", "Missing newline before \")\"") + ) + assertThat(WrappingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a function call and last parameter value is a function call then the clossing parenthesis may be on a single line`() { + val code = + """ + val foobar = foo("" + + "" + + bar("" // IDEA quirk (ignored) + )) + """.trimIndent() + val formattedCode = + """ + val foobar = foo( + "" + + "" + + bar( + "" // IDEA quirk (ignored) + ) + ) + """.trimIndent() + assertThat(WrappingRule().lint(code)).containsExactly( + LintError(1, 18, "wrapping", "Missing newline after \"(\""), + LintError(3, 11, "wrapping", "Missing newline after \"(\""), + LintError(4, 5, "wrapping", "Missing newline before \")\"") + ) + assertThat(WrappingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Multiline string starting at position 0`() { + val code = + """ + fun foo() { + println($MULTILINE_STRING_QUOTE + text + + text + _$MULTILINE_STRING_QUOTE.trimIndent()) + } + """.trimIndent() + val formattedCode = + """ + fun foo() { + println( + $MULTILINE_STRING_QUOTE + text + + text + _ + $MULTILINE_STRING_QUOTE.trimIndent() + ) + } + """.trimIndent() + assertThat(WrappingRule().lint(code)).containsExactly( + LintError(2, 13, "wrapping", "Missing newline after \"(\""), + LintError(6, 2, "wrapping", "Missing newline before \"\"\""), + LintError(6, 17, "wrapping", "Missing newline before \")\"") + ) + assertThat(WrappingRule().format(code)).isEqualTo(formattedCode) + } + + private companion object { + const val MULTILINE_STRING_QUOTE = "${'"'}${'"'}${'"'}" + const val TAB = "${'\t'}" + + val INDENT_STYLE_TABS = EditorConfigOverride.from( + indentStyleProperty to PropertyType.IndentStyleValue.tab + ) + + val wrappingAndIndentRule = listOf(WrappingRule(), IndentationRule()) + } +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/lint.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/lint.kt.spec index 2770cdef8f..f70b614514 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/lint.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/chain-wrapping/lint.kt.spec @@ -36,11 +36,11 @@ fun main() { fun get(key: String): String? // expect -// 2:36:Line must not end with "." -// 3:33:Line must not end with "." -// 5:19:Line must not end with "?:" -// 7:18:Line must not end with "?." -// 12:9:Line must not begin with "&&" -// 14:9:Line must not begin with "&&" -// 16:9:Line must not begin with "/" -// 22:9:Line must not begin with "+" +// 2:36:chain-wrapping:Line must not end with "." +// 3:33:chain-wrapping:Line must not end with "." +// 5:19:chain-wrapping:Line must not end with "?:" +// 7:18:chain-wrapping:Line must not end with "?." +// 12:9:chain-wrapping:Line must not begin with "&&" +// 14:9:chain-wrapping:Line must not begin with "&&" +// 16:9:chain-wrapping:Line must not begin with "/" +// 22:9:chain-wrapping:Line must not begin with "+" diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-binary-expression-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-binary-expression-expected.kt.spec index d3bf1147bd..4791ce083a 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-binary-expression-expected.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-binary-expression-expected.kt.spec @@ -1,5 +1,5 @@ fun f() { - x( + val x = paths.flatMap { dir -> "hello" } + f0( @@ -7,15 +7,6 @@ fun f() { ) + f1( "sssss" ) - ) - - y( - "" - + "" - + f2( - "" // IDEA quirk (ignored) - ) - ) val x = "a" to diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-binary-expression.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-binary-expression.kt.spec index 60f6e7f1c1..b0c71315c3 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-binary-expression.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-binary-expression.kt.spec @@ -1,16 +1,12 @@ fun f() { - x(paths.flatMap { dir -> + val x = + paths.flatMap { dir -> "hello" } + f0( "there" ) + f1( "sssss" - )) - - y("" - + "" - + f2("" // IDEA quirk (ignored) - )) + ) val x = "a" to @@ -31,10 +27,13 @@ fun f() { } object Y { - @Option(names = arrayOf("--install-git-pre-commit-hook"), description = arrayOf( + @Option( + names = arrayOf("--install-git-pre-commit-hook"), + description = arrayOf( "A" + "B" - )) + ) + ) private val DEPRECATED_FLAGS = mapOf( "--ruleset-repository" to "--repository" + diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-eq-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-eq-expected.kt.spec index c34e74c9c3..19e13a7530 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-eq-expected.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-eq-expected.kt.spec @@ -23,11 +23,6 @@ fun f() { // } - val tokenSet = TokenSet.create( - FOR_KEYWORD, IF_KEYWORD, ELSE_KEYWORD, WHILE_KEYWORD, DO_KEYWORD, - TRY_KEYWORD, CATCH_KEYWORD, FINALLY_KEYWORD, WHEN_KEYWORD - ) - val x = when (1) { else -> "" } diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-eq.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-eq.kt.spec index 8423f61fee..6975559db3 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-eq.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-eq.kt.spec @@ -23,9 +23,6 @@ fun f() { // } - val tokenSet = TokenSet.create(FOR_KEYWORD, IF_KEYWORD, ELSE_KEYWORD, WHILE_KEYWORD, DO_KEYWORD, - TRY_KEYWORD, CATCH_KEYWORD, FINALLY_KEYWORD, WHEN_KEYWORD) - val x = when (1) { else -> "" } diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-kdoc.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-kdoc.kt.spec index d5cfb635b7..48f6019b94 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-kdoc.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-kdoc.kt.spec @@ -14,4 +14,5 @@ data class BuildSystemConfig( * org.gradle.parallel=true * org.gradle.caching=true */ - var properties: Map?) + var properties: Map? +) diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected.kt.spec index 90391fa2cc..7c607dcbd2 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent-expected.kt.spec @@ -37,14 +37,6 @@ ${true} text """.trimIndent() ) - println( - """ - text - - text -_ - """.trimIndent() - ) println( """ text "" diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent.kt.spec index 3a7decb496..104872e9a7 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-raw-string-trim-indent.kt.spec @@ -27,11 +27,6 @@ println(""" text """.trimIndent()) -println(""" - text - - text -_""".trimIndent()) println( """ text "" diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-supertype-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-supertype-expected.kt.spec index e24c9130cf..d21c3bc469 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-supertype-expected.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-supertype-expected.kt.spec @@ -1,43 +1,3 @@ -class A1 : Spek({ - - describe("") { - } -}) - -class A2 : X, Spek({ - - describe("") { - } -}) - -class A3 : - X, - Spek({ - - describe("") { - } - }), - Y - -class A4 : - Spek1({ - - describe("") { - } - }), - Spek2({ - - describe("") { - } - }) - -class A5 : - Spek({ - - describe("") { - } - }) - class A6 : T< K, @@ -46,30 +6,3 @@ class A6 : Z({ }) - -class MyClass( - thisIsAParameter: ThisIsTheParameterClass -) : AnotherClassName(thisIsAParameter), - YetAnotherInterfaceWeDeriveFrom { - val x = 1 - val y = 2 -} - -class AndroidModuleDependency() - : ModuleDependency(name, methodToCall, method) - -class AndroidModuleDependency() - : ModuleDependency(name, methodToCall, method), - ModuleDependency(name, methodToCall, method) - -// https://github.com/pinterest/ktlint/issues/518 -enum class Color(val displayName: String, val value: Int) { - RED( - displayName = "Red", - value = 1 - ), - BLUE( - displayName = "Blue", - value = 2 - ); -} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-supertype.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-supertype.kt.spec index 20be56449e..3269b537fb 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-supertype.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/format-supertype.kt.spec @@ -1,38 +1,3 @@ -class A1 : Spek({ - - describe("") { - } -}) - -class A2 : X, Spek({ - - describe("") { - } -}) - -class A3 : X, Spek({ - - describe("") { - } -}), Y - -class A4 : Spek1({ - - describe("") { - } -}), Spek2({ - - describe("") { - } -}) - -class A5 : - Spek({ - - describe("") { - } - }) - class A6 : T< K, @@ -41,30 +6,3 @@ class A6 : Z({ }) - -class MyClass( - thisIsAParameter: ThisIsTheParameterClass -) : AnotherClassName(thisIsAParameter), - YetAnotherInterfaceWeDeriveFrom { - val x = 1 - val y = 2 -} - -class AndroidModuleDependency() - : ModuleDependency(name, methodToCall, method) - -class AndroidModuleDependency() - : ModuleDependency(name, methodToCall, method), - ModuleDependency(name, methodToCall, method) - -// https://github.com/pinterest/ktlint/issues/518 -enum class Color(val displayName: String, val value: Int) { - RED( - displayName = "Red", - value = 1 - ), - BLUE( - displayName = "Blue", - value = 2 - ); -} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-argument-list.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-argument-list.kt.spec index 7fa3e56908..50d4106861 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-argument-list.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-argument-list.kt.spec @@ -1,17 +1,19 @@ // https://kotlinlang.org/docs/reference/coding-conventions.html#method-call-formatting fun main() { - fn(a, + foo( + a, b, - c) + c + ) fn() fn(a, b, c) } // expect -// 4:8:Missing newline after "(" -// 5:1:Unexpected indentation (7) (should be 8) -// 6:1:Unexpected indentation (7) (should be 8) -// 6:8:Missing newline before ")" +// 5:1:indent:Unexpected indentation (7) (should be 8) +// 6:1:indent:Unexpected indentation (7) (should be 8) +// 7:1:indent:Unexpected indentation (7) (should be 8) +// 8:1:indent:Unexpected indentation (7) (should be 4) diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-supertype.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-supertype.kt.spec index 691aab0a08..9b44bb9df8 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-supertype.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-supertype.kt.spec @@ -8,10 +8,6 @@ public class A1 : Appendable { } -public class A2 : Comparable<*>, - Appendable { -} - public class A3 : T< K, @@ -20,6 +16,5 @@ public class A3 : } // expect -// 2:1:Unexpected indentation (0) (should be 4) -// 3:1:Unexpected indentation (8) (should be 4) -// 11:18:Missing newline after ":" +// 2:1:indent:Unexpected indentation (0) (should be 4) +// 3:1:indent:Unexpected indentation (8) (should be 4) diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-when-expression.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-when-expression.kt.spec index 1360afcb76..aa2e067eae 100644 --- a/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-when-expression.kt.spec +++ b/ktlint-ruleset-standard/src/test/resources/spec/indent/lint-when-expression.kt.spec @@ -5,8 +5,6 @@ fun main() { 2 -> false 3 -> true - 4 -> false || - true else -> { true } @@ -39,5 +37,4 @@ fun main() { } // expect -// 7:1:Unexpected indentation (8) (should be 12) -// 8:12:Missing newline after "->" +// 7:1:indent:Unexpected indentation (8) (should be 12) diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-argument-list-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-argument-list-expected.kt.spec similarity index 100% rename from ktlint-ruleset-standard/src/test/resources/spec/indent/format-argument-list-expected.kt.spec rename to ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-argument-list-expected.kt.spec diff --git a/ktlint-ruleset-standard/src/test/resources/spec/indent/format-argument-list.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-argument-list.kt.spec similarity index 100% rename from ktlint-ruleset-standard/src/test/resources/spec/indent/format-argument-list.kt.spec rename to ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-argument-list.kt.spec diff --git a/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-parameter-list-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-parameter-list-expected.kt.spec new file mode 100644 index 0000000000..d84e43a68e --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-parameter-list-expected.kt.spec @@ -0,0 +1,18 @@ +class C ( + val a: Int, val b: Int, + val e: ( + r: Int + ) -> Unit, + val c: Int, val d: Int +) { + + fun f( + a: Int, b: Int, + e: ( + r: Int + ) -> Unit, + c: Int, d: Int + ) { + + } +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-parameter-list.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-parameter-list.kt.spec new file mode 100644 index 0000000000..214dbc5cdb --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-parameter-list.kt.spec @@ -0,0 +1,10 @@ +class C (val a: Int, val b: Int, val e: ( + r: Int +) -> Unit, val c: Int, val d: Int) { + + fun f(a: Int, b: Int, e: ( + r: Int + ) -> Unit, c: Int, d: Int) { + + } +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-supertype-expected.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-supertype-expected.kt.spec new file mode 100644 index 0000000000..243e87614f --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-supertype-expected.kt.spec @@ -0,0 +1,35 @@ +class A6 : + T< + K, + V + >, // IDEA quirk + Z({ + + }) + +class MyClass( + thisIsAParameter: ThisIsTheParameterClass +) : AnotherClassName(thisIsAParameter), + YetAnotherInterfaceWeDeriveFrom { + val x = 1 + val y = 2 +} + +class AndroidModuleDependency() + : ModuleDependency(name, methodToCall, method) + +class AndroidModuleDependency() + : ModuleDependency(name, methodToCall, method), + ModuleDependency(name, methodToCall, method) + +// https://github.com/pinterest/ktlint/issues/518 +enum class Color(val displayName: String, val value: Int) { + RED( + displayName = "Red", + value = 1 + ), + BLUE( + displayName = "Blue", + value = 2 + ); +} diff --git a/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-supertype.kt.spec b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-supertype.kt.spec new file mode 100644 index 0000000000..243e87614f --- /dev/null +++ b/ktlint-ruleset-standard/src/test/resources/spec/wrapping/format-supertype.kt.spec @@ -0,0 +1,35 @@ +class A6 : + T< + K, + V + >, // IDEA quirk + Z({ + + }) + +class MyClass( + thisIsAParameter: ThisIsTheParameterClass +) : AnotherClassName(thisIsAParameter), + YetAnotherInterfaceWeDeriveFrom { + val x = 1 + val y = 2 +} + +class AndroidModuleDependency() + : ModuleDependency(name, methodToCall, method) + +class AndroidModuleDependency() + : ModuleDependency(name, methodToCall, method), + ModuleDependency(name, methodToCall, method) + +// https://github.com/pinterest/ktlint/issues/518 +enum class Color(val displayName: String, val value: Int) { + RED( + displayName = "Red", + value = 1 + ), + BLUE( + displayName = "Blue", + value = 2 + ); +} diff --git a/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/RuleExtension.kt b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/RuleExtension.kt index d7ff5a10a0..c0ffb142c0 100644 --- a/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/RuleExtension.kt +++ b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/RuleExtension.kt @@ -365,6 +365,10 @@ public fun List.format( public fun Rule.diffFileLint( path: String +): String = listOf(this).diffFileLint(path, emptyMap()) + +public fun List.diffFileLint( + path: String ): String = diffFileLint(path, emptyMap()) @Suppress("DeprecatedCallableAddReplaceWith") @@ -373,7 +377,7 @@ public fun Rule.diffFileLint( "specify these properties via parameter 'EditorConfigOverride.'", level = DeprecationLevel.WARNING ) -public fun Rule.diffFileLint( +public fun List.diffFileLint( path: String, userData: Map = emptyMap() ): String { @@ -387,22 +391,29 @@ public fun Rule.diffFileLint( if (line.isBlank() || line == "// expect") { null } else { - line.trimMargin("// ").split(':', limit = 3).let { expectation -> - if (expectation.size != 3) { - throw RuntimeException("$path expectation must be a triple ::") + line.trimMargin("// ").split(':', limit = 4).let { expectation -> + if (this.size > 1 && expectation.size != 4) { + throw RuntimeException("$path expectation must be a quartet ::: because diffFileLint is running on multiple rules") + // " ( is not allowed to contain \":\")") + } else if (expectation.size < 3 || expectation.size > 4) { + throw RuntimeException("$path expectation must be a triple :: or quartet :::") // " ( is not allowed to contain \":\")") } - val message = expectation[2] + val message = expectation.last() val detail = message.removeSuffix(" (cannot be auto-corrected)") - LintError(expectation[0].toInt(), expectation[1].toInt(), id, detail, message == detail) + val ruleId = if (expectation.size == 4) { + expectation[2] + } else { + this.first().id + } + LintError(expectation[0].toInt(), expectation[1].toInt(), ruleId, detail, message == detail) } } } val actual = lint(input, userData, script = true) val str = { err: LintError -> - val ruleId = if (err.ruleId != id) " (${err.ruleId})" else "" val correctionStatus = if (!err.canBeAutoCorrected) " (cannot be auto-corrected)" else "" - "${err.line}:${err.col}:${err.detail}$ruleId$correctionStatus" + "${err.line}:${err.col}:${err.detail}${err.ruleId}$correctionStatus" } val diff = generateUnifiedDiff( @@ -467,6 +478,11 @@ public fun Rule.diffFileLint( public fun Rule.diffFileFormat( srcPath: String, expectedPath: String +): String = listOf(this).diffFileFormat(srcPath, expectedPath, emptyMap()) + +public fun List.diffFileFormat( + srcPath: String, + expectedPath: String ): String = diffFileFormat(srcPath, expectedPath, emptyMap()) @Suppress("DeprecatedCallableAddReplaceWith") @@ -475,7 +491,7 @@ public fun Rule.diffFileFormat( "specify these properties via parameter 'EditorConfigOverride.'", level = DeprecationLevel.WARNING ) -public fun Rule.diffFileFormat( +public fun List.diffFileFormat( srcPath: String, expectedPath: String, userData: Map = emptyMap() @@ -493,8 +509,15 @@ public fun Rule.diffFileFormat( srcPath: String, expectedPath: String, editorConfigOverride: EditorConfigOverride = EditorConfigOverride.emptyEditorConfigOverride +): String = listOf(this).diffFileFormat(srcPath, expectedPath, editorConfigOverride) + +@FeatureInAlphaState +public fun List.diffFileFormat( + srcPath: String, + expectedPath: String, + editorConfigOverride: EditorConfigOverride = EditorConfigOverride.emptyEditorConfigOverride ): String { - val actual = listOf(this).format( + val actual = format( lintedFilePath = null, text = getResourceAsText(srcPath), editorConfigOverride = editorConfigOverride,