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,