From 6ff38b1730501d018c7f50c12ca9a936d932d9a2 Mon Sep 17 00:00:00 2001 From: Chao Zhang Date: Sat, 23 Jul 2022 16:42:45 -0700 Subject: [PATCH] Split TrailingCommaRule --- .editorconfig | 3 - .../standard/StandardRuleSetProvider.kt | 4 +- .../standard/TrailingCommaOnCallSiteRule.kt | 160 +++ .../TrailingCommaOnDeclarationSiteRule.kt | 191 ++++ .../ruleset/standard/TrailingCommaRule.kt | 376 ------- .../trailingcomma/TrailingCommaExtensions.kt | 140 +++ .../trailingcomma/TrailingCommaState.kt | 24 + .../TrailingCommaOnCallSiteRuleTest.kt | 456 +++++++++ .../TrailingCommaOnDeclarationSiteRuleTest.kt | 510 ++++++++++ .../ruleset/standard/TrailingCommaRuleTest.kt | 959 +----------------- 10 files changed, 1498 insertions(+), 1325 deletions(-) create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnCallSiteRule.kt create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnDeclarationSiteRule.kt delete mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRule.kt create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/trailingcomma/TrailingCommaExtensions.kt create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/trailingcomma/TrailingCommaState.kt create mode 100644 ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnCallSiteRuleTest.kt create mode 100644 ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnDeclarationSiteRuleTest.kt diff --git a/.editorconfig b/.editorconfig index ac0cadddec..b982aa88a1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,9 +15,6 @@ indent_size = 4 [*.{kt,kts}] ij_kotlin_imports_layout=* -# Ideally, no experimental rule should be disabled. Ktlint should follow the dogfooding principle. This means that an -# experimental rule should only be added to the master branch no sooner than that this rule has been applied on the -# ktlint code base itself. ij_kotlin_allow_trailing_comma=false ij_kotlin_allow_trailing_comma_on_call_site=false 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 6e68e01406..47589c4a78 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 @@ -2,7 +2,6 @@ package com.pinterest.ktlint.ruleset.standard import com.pinterest.ktlint.core.RuleSet import com.pinterest.ktlint.core.RuleSetProvider -import com.pinterest.ktlint.ruleset.experimental.trailingcomma.TrailingCommaRule public class StandardRuleSetProvider : RuleSetProvider { @@ -51,7 +50,8 @@ public class StandardRuleSetProvider : RuleSetProvider { SpacingBetweenDeclarationsWithAnnotationsRule(), SpacingBetweenDeclarationsWithCommentsRule(), StringTemplateRule(), - TrailingCommaRule(), + TrailingCommaOnDeclarationSiteRule(), + TrailingCommaOnCallSiteRule(), WrappingRule() ) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnCallSiteRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnCallSiteRule.kt new file mode 100644 index 0000000000..0b6823c1db --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnCallSiteRule.kt @@ -0,0 +1,160 @@ +package com.pinterest.ktlint.ruleset.standard + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.api.EditorConfigProperties +import com.pinterest.ktlint.core.api.UsesEditorConfigProperties +import com.pinterest.ktlint.core.ast.ElementType +import com.pinterest.ktlint.core.ast.children +import com.pinterest.ktlint.ruleset.standard.internal.trailingcomma.reportAndCorrectTrailingCommaNodeBefore +import kotlin.properties.Delegates +import org.ec4j.core.model.PropertyType +import org.ec4j.core.model.PropertyType.PropertyValueParser +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet + +/** + * Linting trailing comma for call site. + * + * @see [Kotlin Style Guide](https://kotlinlang.org/docs/coding-conventions.html#trailing-commas) + */ +public class TrailingCommaOnCallSiteRule : + Rule( + id = "trailing-comma-on-call-site", + visitorModifiers = setOf( + VisitorModifier.RunAfterRule( + ruleId = "standard:indent", + loadOnlyWhenOtherRuleIsLoaded = true, + runOnlyWhenOtherRuleIsEnabled = true + ), + VisitorModifier.RunAsLateAsPossible + ) + ), + UsesEditorConfigProperties { + + override val editorConfigProperties: List> = listOf( + allowTrailingCommaOnCallSiteProperty + ) + + private var allowTrailingCommaOnCallSite by Delegates.notNull() + + private fun ASTNode.isTrailingCommaAllowed() = + elementType in TYPES_ON_CALL_SITE && allowTrailingCommaOnCallSite + + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + allowTrailingCommaOnCallSite = editorConfigProperties.getEditorConfigValue(allowTrailingCommaOnCallSiteProperty) + } + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + // Keep processing of element types in sync with Intellij Kotlin formatting settings. + // https://github.com/JetBrains/intellij-kotlin/blob/master/formatter/src/org/jetbrains/kotlin/idea/formatter/trailingComma/util.kt + when (node.elementType) { + ElementType.COLLECTION_LITERAL_EXPRESSION -> visitCollectionLiteralExpression(node, autoCorrect, emit) + ElementType.INDICES -> visitIndices(node, autoCorrect, emit) + ElementType.TYPE_ARGUMENT_LIST -> visitTypeList(node, autoCorrect, emit) + ElementType.VALUE_ARGUMENT_LIST -> visitValueList(node, autoCorrect, emit) + else -> Unit + } + } + + private fun visitCollectionLiteralExpression( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val inspectNode = node + .children() + .last { it.elementType == ElementType.RBRACKET } + node.reportAndCorrectTrailingCommaNodeBefore( + inspectNode = inspectNode, + emit = emit, + isTrailingCommaAllowed = node.isTrailingCommaAllowed(), + autoCorrect = autoCorrect + ) + } + + private fun visitIndices( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val inspectNode = node + .children() + .last { it.elementType == ElementType.RBRACKET } + node.reportAndCorrectTrailingCommaNodeBefore( + inspectNode = inspectNode, + emit = emit, + isTrailingCommaAllowed = node.isTrailingCommaAllowed(), + autoCorrect = autoCorrect + ) + } + + private fun visitValueList( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.treeParent.elementType != ElementType.FUNCTION_LITERAL) { + node + .children() + .lastOrNull { it.elementType == ElementType.RPAR } + ?.let { inspectNode -> + node.reportAndCorrectTrailingCommaNodeBefore( + inspectNode = inspectNode, + emit = emit, + isTrailingCommaAllowed = node.isTrailingCommaAllowed(), + autoCorrect = autoCorrect + ) + } + } + } + + private fun visitTypeList( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val inspectNode = node + .children() + .first { it.elementType == ElementType.GT } + node.reportAndCorrectTrailingCommaNodeBefore( + inspectNode = inspectNode, + emit = emit, + isTrailingCommaAllowed = node.isTrailingCommaAllowed(), + autoCorrect = autoCorrect + ) + } + + public companion object { + + internal const val ALLOW_TRAILING_COMMA_ON_CALL_SITE_NAME = "ij_kotlin_allow_trailing_comma_on_call_site" + private const val ALLOW_TRAILING_COMMA_ON_CALL_SITE_DESCRIPTION = + "Defines whether a trailing comma (or no trailing comma)" + + "should be enforced on the calling side," + + "e.g. argument-list, when-entries, lambda-arguments, indices, etc." + private val BOOLEAN_VALUES_SET = setOf("true", "false") + + // TODO: Rename property to trailingCommaOnCallSite. The word 'allow' is misleading as the comma is + // enforced when the property is enabled and prohibited when disabled. + public val allowTrailingCommaOnCallSiteProperty: UsesEditorConfigProperties.EditorConfigProperty = + UsesEditorConfigProperties.EditorConfigProperty( + type = PropertyType.LowerCasingPropertyType( + ALLOW_TRAILING_COMMA_ON_CALL_SITE_NAME, + ALLOW_TRAILING_COMMA_ON_CALL_SITE_DESCRIPTION, + PropertyValueParser.BOOLEAN_VALUE_PARSER, + BOOLEAN_VALUES_SET + ), + defaultValue = false + ) + + private val TYPES_ON_CALL_SITE = TokenSet.create( + ElementType.COLLECTION_LITERAL_EXPRESSION, + ElementType.INDICES, + ElementType.TYPE_ARGUMENT_LIST, + ElementType.VALUE_ARGUMENT_LIST + ) + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnDeclarationSiteRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnDeclarationSiteRule.kt new file mode 100644 index 0000000000..e099538d39 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnDeclarationSiteRule.kt @@ -0,0 +1,191 @@ +package com.pinterest.ktlint.ruleset.standard + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.api.EditorConfigProperties +import com.pinterest.ktlint.core.api.UsesEditorConfigProperties +import com.pinterest.ktlint.core.ast.ElementType +import com.pinterest.ktlint.core.ast.children +import com.pinterest.ktlint.ruleset.standard.internal.trailingcomma.reportAndCorrectTrailingCommaNodeBefore +import kotlin.properties.Delegates +import org.ec4j.core.model.PropertyType +import org.ec4j.core.model.PropertyType.PropertyValueParser +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet +import org.jetbrains.kotlin.psi.KtWhenEntry +import org.jetbrains.kotlin.psi.KtWhenExpression +import org.jetbrains.kotlin.utils.addToStdlib.cast + +/** + * Linting trailing comma for declaration site. + * + * @see [Kotlin Style Guide](https://kotlinlang.org/docs/coding-conventions.html#trailing-commas) + */ +public class TrailingCommaOnDeclarationSiteRule : + Rule( + id = "trailing-comma-on-declaration-site", + visitorModifiers = setOf( + VisitorModifier.RunAfterRule( + ruleId = "standard:indent", + loadOnlyWhenOtherRuleIsLoaded = true, + runOnlyWhenOtherRuleIsEnabled = true + ), + VisitorModifier.RunAsLateAsPossible + ) + ), + UsesEditorConfigProperties { + + override val editorConfigProperties: List> = listOf( + allowTrailingCommaProperty + ) + + private var allowTrailingComma by Delegates.notNull() + + private fun ASTNode.isTrailingCommaAllowed() = + elementType in TYPES_ON_DECLARATION_SITE && allowTrailingComma + + override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { + allowTrailingComma = editorConfigProperties.getEditorConfigValue(allowTrailingCommaProperty) + } + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + // Keep processing of element types in sync with Intellij Kotlin formatting settings. + // https://github.com/JetBrains/intellij-kotlin/blob/master/formatter/src/org/jetbrains/kotlin/idea/formatter/trailingComma/util.kt + when (node.elementType) { + ElementType.DESTRUCTURING_DECLARATION -> visitDestructuringDeclaration(node, autoCorrect, emit) + ElementType.FUNCTION_LITERAL -> visitFunctionLiteral(node, autoCorrect, emit) + ElementType.TYPE_PARAMETER_LIST -> visitTypeList(node, autoCorrect, emit) + ElementType.VALUE_PARAMETER_LIST -> visitValueList(node, autoCorrect, emit) + ElementType.WHEN_ENTRY -> visitWhenEntry(node, autoCorrect, emit) + else -> Unit + } + } + + private fun visitDestructuringDeclaration( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val inspectNode = node + .children() + .last { it.elementType == ElementType.RPAR } + node.reportAndCorrectTrailingCommaNodeBefore( + inspectNode = inspectNode, + isTrailingCommaAllowed = node.isTrailingCommaAllowed(), + autoCorrect = autoCorrect, + emit = emit + ) + } + + private fun visitFunctionLiteral( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val inspectNode = node + .children() + .lastOrNull { it.elementType == ElementType.ARROW } + ?: // lambda w/o an arrow -> no arguments -> no commas + return + node.reportAndCorrectTrailingCommaNodeBefore( + inspectNode = inspectNode, + isTrailingCommaAllowed = node.isTrailingCommaAllowed(), + autoCorrect = autoCorrect, + emit = emit + ) + } + + private fun visitValueList( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.treeParent.elementType != ElementType.FUNCTION_LITERAL) { + node + .children() + .lastOrNull { it.elementType == ElementType.RPAR } + ?.let { inspectNode -> + node.reportAndCorrectTrailingCommaNodeBefore( + inspectNode = inspectNode, + isTrailingCommaAllowed = node.isTrailingCommaAllowed(), + autoCorrect = autoCorrect, + emit = emit + ) + } + } + } + + private fun visitTypeList( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val inspectNode = node + .children() + .first { it.elementType == ElementType.GT } + node.reportAndCorrectTrailingCommaNodeBefore( + inspectNode = inspectNode, + isTrailingCommaAllowed = node.isTrailingCommaAllowed(), + autoCorrect = autoCorrect, + emit = emit + ) + } + + private fun visitWhenEntry( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + val psi = node.psi + require(psi is KtWhenEntry) + if (psi.isElse || psi.parent.cast().leftParenthesis == null) { + // no commas for "else" or when there are no opening parenthesis for the when-expression + return + } + + val inspectNode = node + .children() + .first { it.elementType == ElementType.ARROW } + node.reportAndCorrectTrailingCommaNodeBefore( + inspectNode = inspectNode, + isTrailingCommaAllowed = node.isTrailingCommaAllowed(), + autoCorrect = autoCorrect, + emit = emit + ) + } + + public companion object { + + internal const val ALLOW_TRAILING_COMMA_NAME = "ij_kotlin_allow_trailing_comma" + private const val ALLOW_TRAILING_COMMA_DESCRIPTION = "Defines whether a trailing comma (or no trailing comma)" + + "should be enforced on the defining side," + + "e.g. parameter-list, type-argument-list, lambda-value-parameters, enum-entries, etc." + + private val BOOLEAN_VALUES_SET = setOf("true", "false") + + // TODO: Rename property to trailingCommaOnDeclarationSite. The word 'allow' is misleading as the comma is + // enforced when the property is enabled and prohibited when disabled. + public val allowTrailingCommaProperty: UsesEditorConfigProperties.EditorConfigProperty = + UsesEditorConfigProperties.EditorConfigProperty( + type = PropertyType.LowerCasingPropertyType( + ALLOW_TRAILING_COMMA_NAME, + ALLOW_TRAILING_COMMA_DESCRIPTION, + PropertyValueParser.BOOLEAN_VALUE_PARSER, + BOOLEAN_VALUES_SET + ), + defaultValue = false + ) + + private val TYPES_ON_DECLARATION_SITE = TokenSet.create( + ElementType.DESTRUCTURING_DECLARATION, + ElementType.FUNCTION_LITERAL, + ElementType.FUNCTION_TYPE, + ElementType.TYPE_PARAMETER_LIST, + ElementType.VALUE_PARAMETER_LIST, + ElementType.WHEN_ENTRY + ) + } +} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRule.kt deleted file mode 100644 index 54f51b5214..0000000000 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRule.kt +++ /dev/null @@ -1,376 +0,0 @@ -package com.pinterest.ktlint.ruleset.experimental.trailingcomma - -import com.pinterest.ktlint.core.Rule -import com.pinterest.ktlint.core.api.EditorConfigProperties -import com.pinterest.ktlint.core.api.UsesEditorConfigProperties -import com.pinterest.ktlint.core.ast.ElementType -import com.pinterest.ktlint.core.ast.children -import com.pinterest.ktlint.core.ast.containsLineBreakInRange -import com.pinterest.ktlint.core.ast.prevCodeLeaf -import com.pinterest.ktlint.core.ast.prevLeaf -import kotlin.properties.Delegates -import org.ec4j.core.model.PropertyType -import org.ec4j.core.model.PropertyType.PropertyValueParser -import org.jetbrains.kotlin.com.intellij.lang.ASTNode -import org.jetbrains.kotlin.com.intellij.psi.PsiElement -import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace -import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet -import org.jetbrains.kotlin.psi.KtCollectionLiteralExpression -import org.jetbrains.kotlin.psi.KtDestructuringDeclaration -import org.jetbrains.kotlin.psi.KtFunctionLiteral -import org.jetbrains.kotlin.psi.KtPsiFactory -import org.jetbrains.kotlin.psi.KtValueArgumentList -import org.jetbrains.kotlin.psi.KtWhenEntry -import org.jetbrains.kotlin.psi.KtWhenExpression -import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType -import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType -import org.jetbrains.kotlin.psi.psiUtil.nextLeaf -import org.jetbrains.kotlin.psi.psiUtil.prevLeaf -import org.jetbrains.kotlin.utils.addToStdlib.cast - -private enum class TrailingCommaState { - /** - * The trailing comma is needed and exists - */ - EXISTS, - - /** - * The trailing comma is needed and doesn't exists - */ - MISSING, - - /** - * The trailing comma isn't needed and doesn't exists - */ - NOT_EXISTS, - - /** - * The trailing comma isn't needed, but exists - */ - REDUNDANT, - ; -} - -public class TrailingCommaRule : - Rule( - id = "trailing-comma", - visitorModifiers = setOf( - VisitorModifier.RunAfterRule( - ruleId = "standard:indent", - loadOnlyWhenOtherRuleIsLoaded = true, - runOnlyWhenOtherRuleIsEnabled = true - ), - VisitorModifier.RunAsLateAsPossible - ) - ), - UsesEditorConfigProperties { - override val editorConfigProperties: List> = listOf( - allowTrailingCommaProperty, - allowTrailingCommaOnCallSiteProperty - ) - - private var allowTrailingComma by Delegates.notNull() - private var allowTrailingCommaOnCallSite by Delegates.notNull() - - override fun beforeFirstNode(editorConfigProperties: EditorConfigProperties) { - allowTrailingComma = editorConfigProperties.getEditorConfigValue(allowTrailingCommaProperty) - allowTrailingCommaOnCallSite = editorConfigProperties.getEditorConfigValue(allowTrailingCommaOnCallSiteProperty) - } - - override fun beforeVisitChildNodes( - node: ASTNode, - autoCorrect: Boolean, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit - ) { - // Keep processing of element types in sync with Intellij Kotlin formatting settings. - // https://github.com/JetBrains/intellij-kotlin/blob/master/formatter/src/org/jetbrains/kotlin/idea/formatter/trailingComma/util.kt - when (node.elementType) { - ElementType.DESTRUCTURING_DECLARATION -> visitDestructuringDeclaration(node, emit, autoCorrect) - ElementType.FUNCTION_LITERAL -> visitFunctionLiteral(node, emit, autoCorrect) - ElementType.TYPE_PARAMETER_LIST -> visitTypeList(node, emit, autoCorrect) - ElementType.VALUE_PARAMETER_LIST -> visitValueList(node, emit, autoCorrect) - ElementType.WHEN_ENTRY -> visitWhenEntry(node, emit, autoCorrect) - else -> Unit - } - when (node.elementType) { - ElementType.COLLECTION_LITERAL_EXPRESSION -> visitCollectionLiteralExpression(node, emit, autoCorrect) - ElementType.INDICES -> visitIndices(node, emit, autoCorrect) - ElementType.TYPE_ARGUMENT_LIST -> visitTypeList(node, emit, autoCorrect) - ElementType.VALUE_ARGUMENT_LIST -> visitValueList(node, emit, autoCorrect) - else -> Unit - } - } - - private fun visitCollectionLiteralExpression( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean - ) { - val inspectNode = node - .children() - .last { it.elementType == ElementType.RBRACKET } - node.reportAndCorrectTrailingCommaNodeBefore(inspectNode, emit, autoCorrect) - } - - private fun visitDestructuringDeclaration( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean - ) { - val inspectNode = node - .children() - .last { it.elementType == ElementType.RPAR } - node.reportAndCorrectTrailingCommaNodeBefore(inspectNode, emit, autoCorrect) - } - - private fun visitFunctionLiteral( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean - ) { - val inspectNode = node - .children() - .lastOrNull { it.elementType == ElementType.ARROW } - ?: // lambda w/o an arrow -> no arguments -> no commas - return - node.reportAndCorrectTrailingCommaNodeBefore(inspectNode, emit, autoCorrect) - } - - private fun visitIndices( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean - ) { - val inspectNode = node - .children() - .last { it.elementType == ElementType.RBRACKET } - node.reportAndCorrectTrailingCommaNodeBefore(inspectNode, emit, autoCorrect) - } - - private fun visitValueList( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean - ) { - if (node.treeParent.elementType != ElementType.FUNCTION_LITERAL) { - node - .children() - .lastOrNull { it.elementType == ElementType.RPAR } - ?.let { - node.reportAndCorrectTrailingCommaNodeBefore(it, emit, autoCorrect) - } - } - } - - private fun visitTypeList( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean - ) { - val inspectNode = node - .children() - .first { it.elementType == ElementType.GT } - node.reportAndCorrectTrailingCommaNodeBefore(inspectNode, emit, autoCorrect) - } - - private fun visitWhenEntry( - node: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean - ) { - val psi = node.psi - require(psi is KtWhenEntry) - if (psi.isElse || psi.parent.cast().leftParenthesis == null) { - // no commas for "else" or when there are no opening parenthesis for the when-expression - return - } - - val inspectNode = node - .children() - .first { it.elementType == ElementType.ARROW } - node.reportAndCorrectTrailingCommaNodeBefore(inspectNode, emit, autoCorrect) - } - - private fun ASTNode.reportAndCorrectTrailingCommaNodeBefore( - inspectNode: ASTNode, - emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, - autoCorrect: Boolean - ) { - val prevLeaf = inspectNode.prevLeaf() - val trailingCommaNode = prevLeaf.findPreviousTrailingCommaNodeOrNull() - val trailingCommaState = when { - isMultiline(psi) -> if (trailingCommaNode != null) TrailingCommaState.EXISTS else TrailingCommaState.MISSING - else -> if (trailingCommaNode != null) TrailingCommaState.REDUNDANT else TrailingCommaState.NOT_EXISTS - } - val isTrailingCommaAllowed = when (elementType) { - in TYPES_ON_DECLARATION_SITE -> allowTrailingComma - in TYPES_ON_CALL_SITE -> allowTrailingCommaOnCallSite - else -> false - } - when (trailingCommaState) { - TrailingCommaState.EXISTS -> if (!isTrailingCommaAllowed) { - emit( - trailingCommaNode!!.startOffset, - "Unnecessary trailing comma before \"${inspectNode.text}\"", - true - ) - if (autoCorrect) { - this.removeChild(trailingCommaNode) - } - } - TrailingCommaState.MISSING -> if (isTrailingCommaAllowed) { - val addNewLineBeforeArrowInWhenEntry = addNewLineBeforeArrowInWhen() - val prevNode = inspectNode.prevCodeLeaf()!! - if (addNewLineBeforeArrowInWhenEntry) { - emit( - prevNode.startOffset + prevNode.textLength, - "Missing trailing comma and newline before \"${inspectNode.text}\"", - true - ) - } else { - emit( - prevNode.startOffset + prevNode.textLength, - "Missing trailing comma before \"${inspectNode.text}\"", - true - ) - } - if (autoCorrect) { - if (addNewLineBeforeArrowInWhenEntry) { - val parentIndent = (prevNode.psi.parent.prevLeaf() as? PsiWhiteSpace)?.text ?: "\n" - val leafBeforeArrow = (psi as KtWhenEntry).arrow?.prevLeaf() - if (leafBeforeArrow != null && leafBeforeArrow is PsiWhiteSpace) { - val newLine = KtPsiFactory(prevNode.psi).createWhiteSpace(parentIndent) - leafBeforeArrow.replace(newLine) - } else { - val newLine = KtPsiFactory(prevNode.psi).createWhiteSpace(parentIndent) - prevNode.psi.parent.addAfter(newLine, prevNode.psi) - } - } - val comma = KtPsiFactory(prevNode.psi).createComma() - prevNode.psi.parent.addAfter(comma, prevNode.psi) - } - } - TrailingCommaState.REDUNDANT -> { - emit( - trailingCommaNode!!.startOffset, - "Unnecessary trailing comma before \"${inspectNode.text}\"", - true - ) - if (autoCorrect) { - this.removeChild(trailingCommaNode) - } - } - TrailingCommaState.NOT_EXISTS -> Unit - } - } - - private fun ASTNode.addNewLineBeforeArrowInWhen() = - if (psi is KtWhenEntry) { - val leafBeforeArrow = (psi as KtWhenEntry).arrow?.prevLeaf() - !(leafBeforeArrow is PsiWhiteSpace && leafBeforeArrow.textContains('\n')) - } else { - false - } - - private fun ASTNode?.findPreviousTrailingCommaNodeOrNull(): ASTNode? { - var node = this - while (node?.isIgnorable() == true) { - node = node.prevLeaf() - } - return if (node?.elementType == ElementType.COMMA) { - node - } else { - null - } - } - - private fun isMultiline(element: PsiElement): Boolean = when { - element.parent is KtFunctionLiteral -> isMultiline(element.parent) - element is KtFunctionLiteral -> containsLineBreakInRange(element.valueParameterList!!, element.arrow!!) - element is KtWhenEntry -> containsLineBreakInRange(element.firstChild, element.arrow!!) - element is KtDestructuringDeclaration -> containsLineBreakInRange(element.lPar!!, element.rPar!!) - element is KtValueArgumentList && element.children.size == 1 && element.anyDescendantOfType() -> { - // special handling for collection literal - // @Annotation([ - // "something", - // ]) - val lastChild = element.collectDescendantsOfType().last() - containsLineBreakInLeafsRange(lastChild.rightBracket!!, element.rightParenthesis!!) - } - else -> element.textContains('\n') - } - - private fun containsLineBreakInLeafsRange(from: PsiElement, to: PsiElement): Boolean { - var leaf: PsiElement? = from - while (leaf != null && !leaf.isEquivalentTo(to)) { - if (leaf.textContains('\n')) { - return true - } - leaf = leaf.nextLeaf(skipEmptyElements = false) - } - return leaf?.textContains('\n') ?: false - } - - private fun ASTNode.isIgnorable(): Boolean = - elementType == ElementType.WHITE_SPACE || - elementType == ElementType.EOL_COMMENT || - elementType == ElementType.BLOCK_COMMENT - - public companion object { - - private val TYPES_ON_DECLARATION_SITE = TokenSet.create( - ElementType.DESTRUCTURING_DECLARATION, - ElementType.FUNCTION_LITERAL, - ElementType.FUNCTION_TYPE, - ElementType.TYPE_PARAMETER_LIST, - ElementType.VALUE_PARAMETER_LIST, - ElementType.WHEN_ENTRY - ) - - private val TYPES_ON_CALL_SITE = TokenSet.create( - ElementType.COLLECTION_LITERAL_EXPRESSION, - ElementType.INDICES, - ElementType.TYPE_ARGUMENT_LIST, - ElementType.VALUE_ARGUMENT_LIST - ) - - internal const val ALLOW_TRAILING_COMMA_NAME = "ij_kotlin_allow_trailing_comma" - private const val ALLOW_TRAILING_COMMA_DESCRIPTION = "Defines whether a trailing comma (or no trailing comma)" + - "should be enforced on the defining side," + - "e.g. parameter-list, type-argument-list, lambda-value-parameters, enum-entries, etc." - - private val BOOLEAN_VALUES_SET = setOf("true", "false") - - // TODO: Rename property to trailingCommaOnDeclarationSite. The word 'allow' is misleading as the comma is - // enforced when the property is enabled and prohibited when disabled. - public val allowTrailingCommaProperty: UsesEditorConfigProperties.EditorConfigProperty = - UsesEditorConfigProperties.EditorConfigProperty( - type = PropertyType.LowerCasingPropertyType( - ALLOW_TRAILING_COMMA_NAME, - ALLOW_TRAILING_COMMA_DESCRIPTION, - PropertyValueParser.BOOLEAN_VALUE_PARSER, - BOOLEAN_VALUES_SET - ), - defaultValue = false - ) - - internal const val ALLOW_TRAILING_COMMA_ON_CALL_SITE_NAME = "ij_kotlin_allow_trailing_comma_on_call_site" - private const val ALLOW_TRAILING_COMMA_ON_CALL_SITE_DESCRIPTION = - "Defines whether a trailing comma (or no trailing comma)" + - "should be enforced on the calling side," + - "e.g. argument-list, when-entries, lambda-arguments, indices, etc." - - // TODO: Rename property to trailingCommaOnCallSite. The word 'allow' is misleading as the comma is - // // enforced when the property is enabled and prohibited when disabled. - public val allowTrailingCommaOnCallSiteProperty: UsesEditorConfigProperties.EditorConfigProperty = - UsesEditorConfigProperties.EditorConfigProperty( - type = PropertyType.LowerCasingPropertyType( - ALLOW_TRAILING_COMMA_ON_CALL_SITE_NAME, - ALLOW_TRAILING_COMMA_ON_CALL_SITE_DESCRIPTION, - PropertyValueParser.BOOLEAN_VALUE_PARSER, - BOOLEAN_VALUES_SET - ), - defaultValue = false - ) - } -} diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/trailingcomma/TrailingCommaExtensions.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/trailingcomma/TrailingCommaExtensions.kt new file mode 100644 index 0000000000..b35e2d1468 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/trailingcomma/TrailingCommaExtensions.kt @@ -0,0 +1,140 @@ +package com.pinterest.ktlint.ruleset.standard.internal.trailingcomma + +import com.pinterest.ktlint.core.ast.ElementType +import com.pinterest.ktlint.core.ast.containsLineBreakInRange +import com.pinterest.ktlint.core.ast.prevCodeLeaf +import com.pinterest.ktlint.core.ast.prevLeaf +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.psi.KtCollectionLiteralExpression +import org.jetbrains.kotlin.psi.KtDestructuringDeclaration +import org.jetbrains.kotlin.psi.KtFunctionLiteral +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.KtValueArgumentList +import org.jetbrains.kotlin.psi.KtWhenEntry +import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType +import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType +import org.jetbrains.kotlin.psi.psiUtil.nextLeaf +import org.jetbrains.kotlin.psi.psiUtil.prevLeaf + +internal fun ASTNode.reportAndCorrectTrailingCommaNodeBefore( + inspectNode: ASTNode, + isTrailingCommaAllowed: Boolean, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit +) { + val prevLeaf = inspectNode.prevLeaf() + val trailingCommaNode = prevLeaf.findPreviousTrailingCommaNodeOrNull() + val trailingCommaState = when { + isMultiline(psi) -> if (trailingCommaNode != null) TrailingCommaState.EXISTS else TrailingCommaState.MISSING + else -> if (trailingCommaNode != null) TrailingCommaState.REDUNDANT else TrailingCommaState.NOT_EXISTS + } + when (trailingCommaState) { + TrailingCommaState.EXISTS -> if (!isTrailingCommaAllowed) { + emit( + trailingCommaNode!!.startOffset, + "Unnecessary trailing comma before \"${inspectNode.text}\"", + true + ) + if (autoCorrect) { + this.removeChild(trailingCommaNode) + } + } + TrailingCommaState.MISSING -> if (isTrailingCommaAllowed) { + val addNewLineBeforeArrowInWhenEntry = addNewLineBeforeArrowInWhen() + val prevNode = inspectNode.prevCodeLeaf()!! + if (addNewLineBeforeArrowInWhenEntry) { + emit( + prevNode.startOffset + prevNode.textLength, + "Missing trailing comma and newline before \"${inspectNode.text}\"", + true + ) + } else { + emit( + prevNode.startOffset + prevNode.textLength, + "Missing trailing comma before \"${inspectNode.text}\"", + true + ) + } + if (autoCorrect) { + if (addNewLineBeforeArrowInWhenEntry) { + val parentIndent = (prevNode.psi.parent.prevLeaf() as? PsiWhiteSpace)?.text ?: "\n" + val leafBeforeArrow = (psi as KtWhenEntry).arrow?.prevLeaf() + if (leafBeforeArrow != null && leafBeforeArrow is PsiWhiteSpace) { + val newLine = KtPsiFactory(prevNode.psi).createWhiteSpace(parentIndent) + leafBeforeArrow.replace(newLine) + } else { + val newLine = KtPsiFactory(prevNode.psi).createWhiteSpace(parentIndent) + prevNode.psi.parent.addAfter(newLine, prevNode.psi) + } + } + val comma = KtPsiFactory(prevNode.psi).createComma() + prevNode.psi.parent.addAfter(comma, prevNode.psi) + } + } + TrailingCommaState.REDUNDANT -> { + emit( + trailingCommaNode!!.startOffset, + "Unnecessary trailing comma before \"${inspectNode.text}\"", + true + ) + if (autoCorrect) { + this.removeChild(trailingCommaNode) + } + } + TrailingCommaState.NOT_EXISTS -> Unit + } +} + +private fun isMultiline(element: PsiElement): Boolean = when { + element.parent is KtFunctionLiteral -> isMultiline(element.parent) + element is KtFunctionLiteral -> containsLineBreakInRange(element.valueParameterList!!, element.arrow!!) + element is KtWhenEntry -> containsLineBreakInRange(element.firstChild, element.arrow!!) + element is KtDestructuringDeclaration -> containsLineBreakInRange(element.lPar!!, element.rPar!!) + element is KtValueArgumentList && element.children.size == 1 && element.anyDescendantOfType() -> { + // special handling for collection literal + // @Annotation([ + // "something", + // ]) + val lastChild = element.collectDescendantsOfType().last() + containsLineBreakInLeafsRange(lastChild.rightBracket!!, element.rightParenthesis!!) + } + else -> element.textContains('\n') +} + +private fun ASTNode.addNewLineBeforeArrowInWhen() = + if (psi is KtWhenEntry) { + val leafBeforeArrow = (psi as KtWhenEntry).arrow?.prevLeaf() + !(leafBeforeArrow is PsiWhiteSpace && leafBeforeArrow.textContains('\n')) + } else { + false + } + +private fun ASTNode?.findPreviousTrailingCommaNodeOrNull(): ASTNode? { + var node = this + while (node?.isIgnorable() == true) { + node = node.prevLeaf() + } + return if (node?.elementType == ElementType.COMMA) { + node + } else { + null + } +} + +private fun containsLineBreakInLeafsRange(from: PsiElement, to: PsiElement): Boolean { + var leaf: PsiElement? = from + while (leaf != null && !leaf.isEquivalentTo(to)) { + if (leaf.textContains('\n')) { + return true + } + leaf = leaf.nextLeaf(skipEmptyElements = false) + } + return leaf?.textContains('\n') ?: false +} + +private fun ASTNode.isIgnorable(): Boolean = + elementType == ElementType.WHITE_SPACE || + elementType == ElementType.EOL_COMMENT || + elementType == ElementType.BLOCK_COMMENT diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/trailingcomma/TrailingCommaState.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/trailingcomma/TrailingCommaState.kt new file mode 100644 index 0000000000..50ff34b798 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/internal/trailingcomma/TrailingCommaState.kt @@ -0,0 +1,24 @@ +package com.pinterest.ktlint.ruleset.standard.internal.trailingcomma + +internal enum class TrailingCommaState { + /** + * The trailing comma is needed and exists + */ + EXISTS, + + /** + * The trailing comma is needed and doesn't exist + */ + MISSING, + + /** + * The trailing comma isn't needed and doesn't exist + */ + NOT_EXISTS, + + /** + * The trailing comma isn't needed, but exists + */ + REDUNDANT, + ; +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnCallSiteRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnCallSiteRuleTest.kt new file mode 100644 index 0000000000..2a774b1a42 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnCallSiteRuleTest.kt @@ -0,0 +1,456 @@ +package com.pinterest.ktlint.ruleset.standard + +import com.pinterest.ktlint.ruleset.standard.TrailingCommaOnCallSiteRule.Companion.allowTrailingCommaOnCallSiteProperty +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThat +import com.pinterest.ktlint.test.LintViolation +import org.junit.jupiter.api.Test + +class TrailingCommaOnCallSiteRuleTest { + + private val ruleAssertThat = + TrailingCommaOnCallSiteRule() + .assertThat( + // Apply the IndentationRule always as additional rule, so that the formattedCode in the unit test looks + // correct. + IndentationRule() + ) + + @Test + fun `Given property allow trailing comma on call site is not set then remove trailing comma's`() { + val code = + """ + val foo1 = listOf("a", "b",) + + val foo2 = Pair(1, 2,) + + val foo3: List = emptyList() + + val foo4 = Array(2) { 42 } + val bar4 = foo4[1,] + + annotation class Foo5(val params: IntArray) + @Foo5([1, 2,]) + val foo5: Int = 0 + """.trimIndent() + val formattedCode = + """ + val foo1 = listOf("a", "b") + + val foo2 = Pair(1, 2) + + val foo3: List = emptyList() + + val foo4 = Array(2) { 42 } + val bar4 = foo4[1] + + annotation class Foo5(val params: IntArray) + @Foo5([1, 2]) + val foo5: Int = 0 + """.trimIndent() + ruleAssertThat(code) + .hasLintViolations( + LintViolation(1, 27, "Unnecessary trailing comma before \")\""), + LintViolation(3, 21, "Unnecessary trailing comma before \")\""), + LintViolation(5, 22, "Unnecessary trailing comma before \">\""), + LintViolation(8, 18, "Unnecessary trailing comma before \"]\""), + LintViolation(11, 12, "Unnecessary trailing comma before \"]\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is not allowed on call site then remove it from an argument list when present`() { + val code = + """ + val list1 = listOf("a", "b",) + val list2 = listOf( + "a", + "b", // The comma before the comment should be removed without removing the comment itself + ) + val list3 = listOf( + "a", + "b", /* The comma before the comment should be removed without removing the comment itself */ + ) + """.trimIndent() + val formattedCode = + """ + val list1 = listOf("a", "b") + val list2 = listOf( + "a", + "b" // The comma before the comment should be removed without removing the comment itself + ) + val list3 = listOf( + "a", + "b" /* The comma before the comment should be removed without removing the comment itself */ + ) + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to false) + .hasLintViolations( + LintViolation(1, 28, "Unnecessary trailing comma before \")\""), + LintViolation(4, 8, "Unnecessary trailing comma before \")\""), + LintViolation(8, 8, "Unnecessary trailing comma before \")\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is required on call site then add it to the argument list when missing`() { + val code = + """ + val list1 = listOf("a", "b") + val list2 = listOf( + "a", + "b" // The comma should be inserted before the comment + ) + val list3 = listOf( + "a", + "b" /* The comma should be inserted before the comment */ + ) + """.trimIndent() + val formattedCode = + """ + val list1 = listOf("a", "b") + val list2 = listOf( + "a", + "b", // The comma should be inserted before the comment + ) + val list3 = listOf( + "a", + "b", /* The comma should be inserted before the comment */ + ) + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) + .hasLintViolations( + LintViolation(4, 8, "Missing trailing comma before \")\""), + LintViolation(8, 8, "Missing trailing comma before \")\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is not allowed on call site then remove it from the type argument list when present`() { + val code = + """ + val list1: List = emptyList() + val list2: List< + String, // The comma before the comment should be removed without removing the comment itself + > = emptyList() + val list3: List< + String, /* The comma before the comment should be removed without removing the comment itself */ + > = emptyList() + """.trimIndent() + val formattedCode = + """ + val list1: List = emptyList() + val list2: List< + String // The comma before the comment should be removed without removing the comment itself + > = emptyList() + val list3: List< + String /* The comma before the comment should be removed without removing the comment itself */ + > = emptyList() + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to false) + .hasLintViolations( + LintViolation(1, 23, "Unnecessary trailing comma before \">\""), + LintViolation(3, 11, "Unnecessary trailing comma before \">\""), + LintViolation(6, 11, "Unnecessary trailing comma before \">\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is required on call site then add it to the type argument list when missing`() { + val code = + """ + val list1: List = emptyList() + val list2: List< + String // The comma should be inserted before the comment + > = emptyList() + val list3: List< + String /* The comma should be inserted before the comment */ + > = emptyList() + """.trimIndent() + val formattedCode = + """ + val list1: List = emptyList() + val list2: List< + String, // The comma should be inserted before the comment + > = emptyList() + val list3: List< + String, /* The comma should be inserted before the comment */ + > = emptyList() + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) + .hasLintViolations( + LintViolation(3, 11, "Missing trailing comma before \">\""), + LintViolation(6, 11, "Missing trailing comma before \">\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is not allowed on call site then remove it from the array index when present`() { + val code = + """ + val foo = Array(2) { 42 } + val bar1 = foo[1,] + val bar2 = foo[ + 1, // The comma before the comment should be removed without removing the comment itself + ] + val bar3 = foo[ + 1, /* The comma before the comment should be removed without removing the comment itself */ + ] + """.trimIndent() + val formattedCode = + """ + val foo = Array(2) { 42 } + val bar1 = foo[1] + val bar2 = foo[ + 1 // The comma before the comment should be removed without removing the comment itself + ] + val bar3 = foo[ + 1 /* The comma before the comment should be removed without removing the comment itself */ + ] + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to false) + .hasLintViolations( + LintViolation(2, 17, "Unnecessary trailing comma before \"]\""), + LintViolation(4, 6, "Unnecessary trailing comma before \"]\""), + LintViolation(7, 6, "Unnecessary trailing comma before \"]\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is required on call site then add it to the array index when missing`() { + val code = + """ + val foo = Array(2) { 42 } + val bar1 = foo[1] + val bar2 = foo[ + 1 // The comma should be inserted before the comment + ] + val bar3 = foo[ + 1 /* The comma should be inserted before the comment */ + ] + """.trimIndent() + val formattedCode = + """ + val foo = Array(2) { 42 } + val bar1 = foo[1] + val bar2 = foo[ + 1, // The comma should be inserted before the comment + ] + val bar3 = foo[ + 1, /* The comma should be inserted before the comment */ + ] + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) + .hasLintViolations( + LintViolation(4, 6, "Missing trailing comma before \"]\""), + LintViolation(7, 6, "Missing trailing comma before \"]\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is not allowed on call site then remove it from the collection literal when present`() { + val code = + """ + annotation class Annotation(val params: IntArray) + + @Annotation([1, 2,]) + val foo1: Int = 0 + + @Annotation([ + 1, + 2, // The comma before the comment should be removed without removing the comment itself + ]) + val foo2: Int = 0 + + @Annotation([ + 1, + 2, /* The comma before the comment should be removed without removing the comment itself */ + ]) + val foo3: Int = 0 + """.trimIndent() + val formattedCode = + """ + annotation class Annotation(val params: IntArray) + + @Annotation([1, 2]) + val foo1: Int = 0 + + @Annotation([ + 1, + 2 // The comma before the comment should be removed without removing the comment itself + ]) + val foo2: Int = 0 + + @Annotation([ + 1, + 2 /* The comma before the comment should be removed without removing the comment itself */ + ]) + val foo3: Int = 0 + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to false) + .hasLintViolations( + LintViolation(3, 18, "Unnecessary trailing comma before \"]\""), + LintViolation(8, 6, "Unnecessary trailing comma before \"]\""), + LintViolation(14, 6, "Unnecessary trailing comma before \"]\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is required on call site then add it to the collection literal when missing`() { + val code = + """ + annotation class Annotation(val params: IntArray) + + @Annotation([1, 2]) + val foo1: Int = 0 + + @Annotation([ + 1, + 2 // The comma should be inserted before the comment + ]) + val foo2: Int = 0 + + @Annotation([ + 1, + 2 /* The comma should be inserted before the comment */ + ]) + val foo3: Int = 0 + + @Annotation( + [ + 1, + 2 /* The comma should be inserted before the comment */ + ], + [ + 3, + 4 /* The comma should be inserted before the comment */ + ] + ) + val foo4: Int = 0 + """.trimIndent() + val formattedCode = + """ + annotation class Annotation(val params: IntArray) + + @Annotation([1, 2]) + val foo1: Int = 0 + + @Annotation([ + 1, + 2, // The comma should be inserted before the comment + ]) + val foo2: Int = 0 + + @Annotation([ + 1, + 2, /* The comma should be inserted before the comment */ + ]) + val foo3: Int = 0 + + @Annotation( + [ + 1, + 2, /* The comma should be inserted before the comment */ + ], + [ + 3, + 4, /* The comma should be inserted before the comment */ + ], + ) + val foo4: Int = 0 + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) + .hasLintViolations( + LintViolation(8, 6, "Missing trailing comma before \"]\""), + LintViolation(14, 6, "Missing trailing comma before \"]\""), + LintViolation(21, 10, "Missing trailing comma before \"]\""), + LintViolation(25, 10, "Missing trailing comma before \"]\""), + LintViolation(26, 6, "Missing trailing comma before \")\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `1297 - Given that the trailing comma is required on call site the a trailing comma to collection literal when missing`() { + val code = + """ + annotation class FooBar( + val foo1: Array = [], + val foo2: Array = [], + val bar1: String = "" + ) + + @FooBar( + foo1 = [ + "foo-1" // Add trailing comma as the argument value list foo1 is a multiline statement + ], + foo2 = ["foo-2"], // Do not add trailing comma in the array as the argument value list foo2 is a single line statement + bar1 = "bar-1" // Add trailing comma as the outer argument value list of the annotation is a multiline statement + ) + val fooBar = null + """.trimIndent() + val formattedCode = + """ + annotation class FooBar( + val foo1: Array = [], + val foo2: Array = [], + val bar1: String = "" + ) + + @FooBar( + foo1 = [ + "foo-1", // Add trailing comma as the argument value list foo1 is a multiline statement + ], + foo2 = ["foo-2"], // Do not add trailing comma in the array as the argument value list foo2 is a single line statement + bar1 = "bar-1", // Add trailing comma as the outer argument value list of the annotation is a multiline statement + ) + val fooBar = null + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) + .hasLintViolations( + LintViolation(9, 16, "Missing trailing comma before \"]\""), + LintViolation(12, 19, "Missing trailing comma before \")\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Issue 1379 - Given that a trailing comma is required on call site then add trailing comma after array in annotation when missing`() { + val code = + """ + import kotlin.reflect.KClass + + @Foo( + values = [ + Foo::class, + Foo::class + ] + ) + annotation class Foo(val values: Array>) + """.trimIndent() + val formattedCode = + """ + import kotlin.reflect.KClass + + @Foo( + values = [ + Foo::class, + Foo::class, + ], + ) + annotation class Foo(val values: Array>) + """.trimIndent() + ruleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) + .hasLintViolations( + LintViolation(6, 19, "Missing trailing comma before \"]\""), + LintViolation(7, 6, "Missing trailing comma before \")\"") + ).isFormattedAs(formattedCode) + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnDeclarationSiteRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnDeclarationSiteRuleTest.kt new file mode 100644 index 0000000000..7e04de7d25 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaOnDeclarationSiteRuleTest.kt @@ -0,0 +1,510 @@ +package com.pinterest.ktlint.ruleset.standard + +import com.pinterest.ktlint.ruleset.standard.TrailingCommaOnDeclarationSiteRule.Companion.allowTrailingCommaProperty +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThat +import com.pinterest.ktlint.test.LintViolation +import org.junit.jupiter.api.Test + +class TrailingCommaOnDeclarationSiteRuleTest { + + private val trailingCommaOnDeclarationSiteRuleAssertThat = + TrailingCommaOnDeclarationSiteRule() + .assertThat( + // Apply the IndentationRule always as additional rule, so that the formattedCode in the unit test looks + // correct. + IndentationRule() + ) + + @Test + fun `Given property allow trailing comma on declaration site is not set then remove trailing comma's`() { + val code = + """ + data class Foo1(val bar: Int,) + + class Foo2 {} + + fun foo3(bar: Int): String = when(bar) { + 1, 2, -> "a" + else -> "b" + } + + fun foo4() { + fun bar(): Pair = Pair(1, 2) + + val (x, y,) = bar() + } + + val foo5: (Int, Int,) -> Int = 42 + + val foo6: (Int, Int,) -> Int = { foo, bar, -> foo * bar } + """.trimIndent() + val formattedCode = + """ + data class Foo1(val bar: Int) + + class Foo2 {} + + fun foo3(bar: Int): String = when(bar) { + 1, 2 -> "a" + else -> "b" + } + + fun foo4() { + fun bar(): Pair = Pair(1, 2) + + val (x, y) = bar() + } + + val foo5: (Int, Int) -> Int = 42 + + val foo6: (Int, Int) -> Int = { foo, bar -> foo * bar } + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .hasLintViolations( + LintViolation(1, 29, "Unnecessary trailing comma before \")\""), + LintViolation(3, 16, "Unnecessary trailing comma before \">\""), + LintViolation(6, 9, "Unnecessary trailing comma before \"->\""), + LintViolation(13, 14, "Unnecessary trailing comma before \")\""), + LintViolation(16, 20, "Unnecessary trailing comma before \")\""), + LintViolation(18, 20, "Unnecessary trailing comma before \")\""), + LintViolation(18, 42, "Unnecessary trailing comma before \"->\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is not allowed on declaration site then remove it from an parameter list when present`() { + val code = + """ + data class Foo1(val bar: Int,) + data class Foo2( + val bar: Int, // The comma before the comment should be removed without removing the comment itself + ) + data class Foo3( + val bar: Int, /* The comma before the comment should be removed without removing the comment itself */ + ) + """.trimIndent() + val formattedCode = + """ + data class Foo1(val bar: Int) + data class Foo2( + val bar: Int // The comma before the comment should be removed without removing the comment itself + ) + data class Foo3( + val bar: Int /* The comma before the comment should be removed without removing the comment itself */ + ) + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to false) + .hasLintViolations( + LintViolation(1, 29, "Unnecessary trailing comma before \")\""), + LintViolation(3, 17, "Unnecessary trailing comma before \")\""), + LintViolation(6, 17, "Unnecessary trailing comma before \")\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is required on declaration site then add it to the parameter list when missing`() { + val code = + """ + data class Foo1(val bar: Int) + data class Foo2( + val bar: Int // The comma should be inserted before the comment + ) + data class Foo3( + val bar: Int /* The comma should be inserted before the comment */ + ) + """.trimIndent() + val formattedCode = + """ + data class Foo1(val bar: Int) + data class Foo2( + val bar: Int, // The comma should be inserted before the comment + ) + data class Foo3( + val bar: Int, /* The comma should be inserted before the comment */ + ) + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to true) + .hasLintViolations( + LintViolation(3, 17, "Missing trailing comma before \")\""), + LintViolation(6, 17, "Missing trailing comma before \")\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is not allowed on declaration site then remove it from an type parameter list when present`() { + val code = + """ + class Foo1 {} + class Foo2< + A, + B, // The comma before the comment should be removed without removing the comment itself + > {} + class Foo3< + A, + B, /* The comma before the comment should be removed without removing the comment itself */ + > {} + """.trimIndent() + val formattedCode = + """ + class Foo1 {} + class Foo2< + A, + B // The comma before the comment should be removed without removing the comment itself + > {} + class Foo3< + A, + B /* The comma before the comment should be removed without removing the comment itself */ + > {} + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to false) + .hasLintViolations( + LintViolation(1, 16, "Unnecessary trailing comma before \">\""), + LintViolation(4, 6, "Unnecessary trailing comma before \">\""), + LintViolation(8, 6, "Unnecessary trailing comma before \">\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is required on declaration site then add it to the type parameter list when missing`() { + val code = + """ + class Foo1 {} + class Foo2< + A, + B // The comma should be inserted before the comment + > {} + class Foo3< + A, + B /* The comma should be inserted before the comment */ + > {} + """.trimIndent() + val formattedCode = + """ + class Foo1 {} + class Foo2< + A, + B, // The comma should be inserted before the comment + > {} + class Foo3< + A, + B, /* The comma should be inserted before the comment */ + > {} + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to true) + .hasLintViolations( + LintViolation(4, 6, "Missing trailing comma before \">\""), + LintViolation(8, 6, "Missing trailing comma before \">\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is not allowed on declaration site then remove it from when-condition when present`() { + val code = + """ + fun foo(bar: Int): String = when(bar) { + 1, 2, -> "a" + 3, 4, // The comma before the comment should be removed without removing the comment itself + -> "a" + 5, + 6, /* The comma before the comment should be removed without removing the comment itself */ + -> "a" + else -> "b" + } + """.trimIndent() + val formattedCode = + """ + fun foo(bar: Int): String = when(bar) { + 1, 2 -> "a" + 3, 4 // The comma before the comment should be removed without removing the comment itself + -> "a" + 5, + 6 /* The comma before the comment should be removed without removing the comment itself */ + -> "a" + else -> "b" + } + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to false) + .hasLintViolations( + LintViolation(2, 9, "Unnecessary trailing comma before \"->\""), + LintViolation(3, 9, "Unnecessary trailing comma before \"->\""), + LintViolation(6, 6, "Unnecessary trailing comma before \"->\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is required on declaration site then add it to the when-condition when missing`() { + val code = + """ + fun foo(bar: Int): String = when(bar) { + 1, 2 -> "a" + 3, 4 // The comma should be inserted before the comment + -> "a" + 5, + 6 /* The comma should be inserted before the comment */ + -> "a" + else -> "b" + } + """.trimIndent() + val formattedCode = + """ + fun foo(bar: Int): String = when(bar) { + 1, 2 -> "a" + 3, 4, // The comma should be inserted before the comment + -> "a" + 5, + 6, /* The comma should be inserted before the comment */ + -> "a" + else -> "b" + } + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to true) + .hasLintViolations( + LintViolation(3, 9, "Missing trailing comma before \"->\""), + LintViolation(6, 6, "Missing trailing comma before \"->\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is not allowed on declaration site then remove it from the destructuring declaration when present`() { + val code = + """ + fun foo() { + fun bar(): Pair = Pair(1, 2) + + val (x, y,) = bar() + val ( + x, + y, // The comma before the comment should be removed without removing the comment itself + ) = bar() + val ( + x, + y, /* The comma before the comment should be removed without removing the comment itself */ + ) = bar() + } + """.trimIndent() + val formattedCode = + """ + fun foo() { + fun bar(): Pair = Pair(1, 2) + + val (x, y) = bar() + val ( + x, + y // The comma before the comment should be removed without removing the comment itself + ) = bar() + val ( + x, + y /* The comma before the comment should be removed without removing the comment itself */ + ) = bar() + } + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to false) + .hasLintViolations( + LintViolation(4, 14, "Unnecessary trailing comma before \")\""), + LintViolation(7, 10, "Unnecessary trailing comma before \")\""), + LintViolation(11, 10, "Unnecessary trailing comma before \")\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is required on declaration site then add it to the destructuring declaration when missing`() { + val code = + """ + fun foo() { + fun bar(): Pair = Pair(1, 2) + + val (x, y) = bar() + val ( + x, + y // The comma should be inserted before the comment + ) = bar() + val ( + x, + y /* The comma should be inserted before the comment */ + ) = bar() + } + """.trimIndent() + val formattedCode = + """ + fun foo() { + fun bar(): Pair = Pair(1, 2) + + val (x, y) = bar() + val ( + x, + y, // The comma should be inserted before the comment + ) = bar() + val ( + x, + y, /* The comma should be inserted before the comment */ + ) = bar() + } + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to true) + .hasLintViolations( + LintViolation(7, 10, "Missing trailing comma before \")\""), + LintViolation(11, 10, "Missing trailing comma before \")\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is not allowed on declaration site then remove it from the function literal when present`() { + val code = + """ + val fooBar1: (Int, Int) -> Int = { foo, bar, -> foo * bar } + val fooBar2: (Int, Int) -> Int = { + foo, + bar, // The comma before the comment should be removed without removing the comment itself + -> foo * bar + } + val fooBar3: (Int, Int) -> Int = { + foo, + bar, /* The comma before the comment should be removed without removing the comment itself */ + -> foo * bar + } + """.trimIndent() + val formattedCode = + """ + val fooBar1: (Int, Int) -> Int = { foo, bar -> foo * bar } + val fooBar2: (Int, Int) -> Int = { + foo, + bar // The comma before the comment should be removed without removing the comment itself + -> foo * bar + } + val fooBar3: (Int, Int) -> Int = { + foo, + bar /* The comma before the comment should be removed without removing the comment itself */ + -> foo * bar + } + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to false) + .hasLintViolations( + LintViolation(1, 44, "Unnecessary trailing comma before \"->\""), + LintViolation(4, 12, "Unnecessary trailing comma before \"->\""), + LintViolation(9, 12, "Unnecessary trailing comma before \"->\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that the trailing comma is required on declaration site then add it to function literal when missing`() { + val code = + """ + val fooBar1: (Int, Int) -> Int = { foo, bar -> foo * bar } + val fooBar2: (Int, Int) -> Int = { + foo, + bar // The comma should be inserted before the comment + -> foo * bar + } + val fooBar3: (Int, Int) -> Int = { + foo, + bar /* The comma should be inserted before the comment */ + -> foo * bar + } + """.trimIndent() + val formattedCode = + """ + val fooBar1: (Int, Int) -> Int = { foo, bar -> foo * bar } + val fooBar2: (Int, Int) -> Int = { + foo, + bar, // The comma should be inserted before the comment + -> foo * bar + } + val fooBar3: (Int, Int) -> Int = { + foo, + bar, /* The comma should be inserted before the comment */ + -> foo * bar + } + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to true) + .hasLintViolations( + LintViolation(4, 12, "Missing trailing comma before \"->\""), + LintViolation(9, 12, "Missing trailing comma before \"->\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Issue 1312 - Given that a trailing comma is required on declaration site and multiple elements then force lambda arrow to next line`() { + val code = + """ + fun foo(bar: Int): String = when(bar) { + 1, 2 -> "a" + 3, + 4 -> "b" + 5, + 6-> "c" + else -> "d" + } + """.trimIndent() + val formattedCode = + """ + fun foo(bar: Int): String = when(bar) { + 1, 2 -> "a" + 3, + 4, + -> "b" + 5, + 6, + -> "c" + else -> "d" + } + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + .withEditorConfigOverride(allowTrailingCommaProperty to true) + .hasLintViolations( + LintViolation(4, 6, "Missing trailing comma and newline before \"->\""), + LintViolation(6, 6, "Missing trailing comma and newline before \"->\"") + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given that a trailing comma is required on declaration site and unused imports do not affect each other`() { + val code = + """ + package com.pinterest.ktlint + import com.pinterest.ktlint.enum.Enum + import com.pinterest.ktlint.enum.EnumThree + import com.pinterest.ktlint.enum.EnumTwo + data class TrailingCommaTest( + val foo: String, + val bar: Enum, + val bar2: EnumTwo, + val bar3: EnumThree + ) + """.trimIndent() + val formattedCode = + """ + package com.pinterest.ktlint + import com.pinterest.ktlint.enum.Enum + import com.pinterest.ktlint.enum.EnumThree + import com.pinterest.ktlint.enum.EnumTwo + data class TrailingCommaTest( + val foo: String, + val bar: Enum, + val bar2: EnumTwo, + val bar3: EnumThree, + ) + """.trimIndent() + trailingCommaOnDeclarationSiteRuleAssertThat(code) + // When running format mode, the rules are first executed in parallel to find linting errors. In this + // process, no unused import are found because the trailing comma is not yet added to variable "bar3". Then + // in the next stage the rules are run consecutively. Now the trailing comma rule is adding a trailing comma + // after the type of variable "bar3". When the no-unused-import rule runs after the trailing-comma rule, it + // was incorrectly seen as part of the type of variable "bar3" and a reference "EnumThree," (with the + // trailing comma was added) which in turn resulted in not recognizing that the import of EnumThree actually + // was used. + .addAdditionalRules(NoUnusedImportsRule()) + .withEditorConfigOverride(allowTrailingCommaProperty to true) + .hasLintViolation(9, 24, "Missing trailing comma before \")\"") + .isFormattedAs(formattedCode) + } +} diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRuleTest.kt index 50ff175c2f..d9d568d390 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/TrailingCommaRuleTest.kt @@ -1,119 +1,21 @@ package com.pinterest.ktlint.ruleset.standard -import com.pinterest.ktlint.ruleset.experimental.trailingcomma.TrailingCommaRule -import com.pinterest.ktlint.ruleset.experimental.trailingcomma.TrailingCommaRule.Companion.allowTrailingCommaOnCallSiteProperty -import com.pinterest.ktlint.ruleset.experimental.trailingcomma.TrailingCommaRule.Companion.allowTrailingCommaProperty import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThat import com.pinterest.ktlint.test.LintViolation import org.junit.jupiter.api.Test class TrailingCommaRuleTest { - private val trailingCommaRuleAssertThat = - TrailingCommaRule() + + private val combinedTrailingCommaAssertThat = + TrailingCommaOnDeclarationSiteRule() .assertThat( + TrailingCommaOnCallSiteRule(), // Apply the IndentationRule always as additional rule, so that the formattedCode in the unit test looks // correct. IndentationRule() ) - @Test - fun `Given property allow trailing comma on call site is not set then remove trailing comma's`() { - val code = - """ - val foo1 = listOf("a", "b",) - - val foo2 = Pair(1, 2,) - - val foo3: List = emptyList() - - val foo4 = Array(2) { 42 } - val bar4 = foo4[1,] - - annotation class Foo5(val params: IntArray) - @Foo5([1, 2,]) - val foo5: Int = 0 - """.trimIndent() - val formattedCode = - """ - val foo1 = listOf("a", "b") - - val foo2 = Pair(1, 2) - - val foo3: List = emptyList() - - val foo4 = Array(2) { 42 } - val bar4 = foo4[1] - - annotation class Foo5(val params: IntArray) - @Foo5([1, 2]) - val foo5: Int = 0 - """.trimIndent() - trailingCommaRuleAssertThat(code) - .hasLintViolations( - LintViolation(1, 27, "Unnecessary trailing comma before \")\""), - LintViolation(3, 21, "Unnecessary trailing comma before \")\""), - LintViolation(5, 22, "Unnecessary trailing comma before \">\""), - LintViolation(8, 18, "Unnecessary trailing comma before \"]\""), - LintViolation(11, 12, "Unnecessary trailing comma before \"]\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given property allow trailing comma on declaration site is not set then remove trailing comma's`() { - val code = - """ - data class Foo1(val bar: Int,) - - class Foo2 {} - - fun foo3(bar: Int): String = when(bar) { - 1, 2, -> "a" - else -> "b" - } - - fun foo4() { - fun bar(): Pair = Pair(1, 2) - - val (x, y,) = bar() - } - - val foo5: (Int, Int,) -> Int = 42 - - val foo6: (Int, Int,) -> Int = { foo, bar, -> foo * bar } - """.trimIndent() - val formattedCode = - """ - data class Foo1(val bar: Int) - - class Foo2 {} - - fun foo3(bar: Int): String = when(bar) { - 1, 2 -> "a" - else -> "b" - } - - fun foo4() { - fun bar(): Pair = Pair(1, 2) - - val (x, y) = bar() - } - - val foo5: (Int, Int) -> Int = 42 - - val foo6: (Int, Int) -> Int = { foo, bar -> foo * bar } - """.trimIndent() - trailingCommaRuleAssertThat(code) - .hasLintViolations( - LintViolation(1, 29, "Unnecessary trailing comma before \")\""), - LintViolation(3, 16, "Unnecessary trailing comma before \">\""), - LintViolation(6, 9, "Unnecessary trailing comma before \"->\""), - LintViolation(13, 14, "Unnecessary trailing comma before \")\""), - LintViolation(16, 20, "Unnecessary trailing comma before \")\""), - LintViolation(18, 20, "Unnecessary trailing comma before \")\""), - LintViolation(18, 42, "Unnecessary trailing comma before \"->\"") - ).isFormattedAs(formattedCode) - } - + // TBS? @Test fun `Given that properties to force trailing comma's on call and declaration site have been enabled`() { val code = @@ -160,740 +62,19 @@ class TrailingCommaRuleTest { else -> { _, _ -> block(0, 0) } } """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) - .withEditorConfigOverride(allowTrailingCommaProperty to true) + combinedTrailingCommaAssertThat(code) + .withEditorConfigOverride(TrailingCommaOnCallSiteRule.allowTrailingCommaOnCallSiteProperty to true) + .withEditorConfigOverride(TrailingCommaOnDeclarationSiteRule.allowTrailingCommaProperty to true) .hasLintViolations( LintViolation(4, 29, "Missing trailing comma before \")\""), LintViolation(6, 13, "Missing trailing comma before \")\""), LintViolation(8, 9, "Missing trailing comma before \"->\""), - LintViolation(11, 16, "Missing trailing comma before \"->\""), - LintViolation(15, 22, "Missing trailing comma before \")\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is not allowed on call site then remove it from an argument list when present`() { - val code = - """ - val list1 = listOf("a", "b",) - val list2 = listOf( - "a", - "b", // The comma before the comment should be removed without removing the comment itself - ) - val list3 = listOf( - "a", - "b", /* The comma before the comment should be removed without removing the comment itself */ - ) - """.trimIndent() - val formattedCode = - """ - val list1 = listOf("a", "b") - val list2 = listOf( - "a", - "b" // The comma before the comment should be removed without removing the comment itself - ) - val list3 = listOf( - "a", - "b" /* The comma before the comment should be removed without removing the comment itself */ - ) - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to false) - .hasLintViolations( - LintViolation(1, 28, "Unnecessary trailing comma before \")\""), - LintViolation(4, 8, "Unnecessary trailing comma before \")\""), - LintViolation(8, 8, "Unnecessary trailing comma before \")\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is required on call site then add it to the argument list when missing`() { - val code = - """ - val list1 = listOf("a", "b") - val list2 = listOf( - "a", - "b" // The comma should be inserted before the comment - ) - val list3 = listOf( - "a", - "b" /* The comma should be inserted before the comment */ - ) - """.trimIndent() - val formattedCode = - """ - val list1 = listOf("a", "b") - val list2 = listOf( - "a", - "b", // The comma should be inserted before the comment - ) - val list3 = listOf( - "a", - "b", /* The comma should be inserted before the comment */ - ) - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) - .hasLintViolations( - LintViolation(4, 8, "Missing trailing comma before \")\""), - LintViolation(8, 8, "Missing trailing comma before \")\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is not allowed on declaration site then remove it from an parameter list when present`() { - val code = - """ - data class Foo1(val bar: Int,) - data class Foo2( - val bar: Int, // The comma before the comment should be removed without removing the comment itself - ) - data class Foo3( - val bar: Int, /* The comma before the comment should be removed without removing the comment itself */ - ) - """.trimIndent() - val formattedCode = - """ - data class Foo1(val bar: Int) - data class Foo2( - val bar: Int // The comma before the comment should be removed without removing the comment itself - ) - data class Foo3( - val bar: Int /* The comma before the comment should be removed without removing the comment itself */ - ) - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to false) - .hasLintViolations( - LintViolation(1, 29, "Unnecessary trailing comma before \")\""), - LintViolation(3, 17, "Unnecessary trailing comma before \")\""), - LintViolation(6, 17, "Unnecessary trailing comma before \")\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is required on declaration site then add it to the parameter list when missing`() { - val code = - """ - data class Foo1(val bar: Int) - data class Foo2( - val bar: Int // The comma should be inserted before the comment - ) - data class Foo3( - val bar: Int /* The comma should be inserted before the comment */ - ) - """.trimIndent() - val formattedCode = - """ - data class Foo1(val bar: Int) - data class Foo2( - val bar: Int, // The comma should be inserted before the comment - ) - data class Foo3( - val bar: Int, /* The comma should be inserted before the comment */ - ) - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to true) - .hasLintViolations( - LintViolation(3, 17, "Missing trailing comma before \")\""), - LintViolation(6, 17, "Missing trailing comma before \")\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is not allowed on declaration site then remove it from an type parameter list when present`() { - val code = - """ - class Foo1 {} - class Foo2< - A, - B, // The comma before the comment should be removed without removing the comment itself - > {} - class Foo3< - A, - B, /* The comma before the comment should be removed without removing the comment itself */ - > {} - """.trimIndent() - val formattedCode = - """ - class Foo1 {} - class Foo2< - A, - B // The comma before the comment should be removed without removing the comment itself - > {} - class Foo3< - A, - B /* The comma before the comment should be removed without removing the comment itself */ - > {} - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to false) - .hasLintViolations( - LintViolation(1, 16, "Unnecessary trailing comma before \">\""), - LintViolation(4, 6, "Unnecessary trailing comma before \">\""), - LintViolation(8, 6, "Unnecessary trailing comma before \">\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is required on declaration site then add it to the type parameter list when missing`() { - val code = - """ - class Foo1 {} - class Foo2< - A, - B // The comma should be inserted before the comment - > {} - class Foo3< - A, - B /* The comma should be inserted before the comment */ - > {} - """.trimIndent() - val formattedCode = - """ - class Foo1 {} - class Foo2< - A, - B, // The comma should be inserted before the comment - > {} - class Foo3< - A, - B, /* The comma should be inserted before the comment */ - > {} - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to true) - .hasLintViolations( - LintViolation(4, 6, "Missing trailing comma before \">\""), - LintViolation(8, 6, "Missing trailing comma before \">\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is not allowed on declaration site then remove it from when-condition when present`() { - val code = - """ - fun foo(bar: Int): String = when(bar) { - 1, 2, -> "a" - 3, 4, // The comma before the comment should be removed without removing the comment itself - -> "a" - 5, - 6, /* The comma before the comment should be removed without removing the comment itself */ - -> "a" - else -> "b" - } - """.trimIndent() - val formattedCode = - """ - fun foo(bar: Int): String = when(bar) { - 1, 2 -> "a" - 3, 4 // The comma before the comment should be removed without removing the comment itself - -> "a" - 5, - 6 /* The comma before the comment should be removed without removing the comment itself */ - -> "a" - else -> "b" - } - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to false) - .hasLintViolations( - LintViolation(2, 9, "Unnecessary trailing comma before \"->\""), - LintViolation(3, 9, "Unnecessary trailing comma before \"->\""), - LintViolation(6, 6, "Unnecessary trailing comma before \"->\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is required on declaration site then add it to the when-condition when missing`() { - val code = - """ - fun foo(bar: Int): String = when(bar) { - 1, 2 -> "a" - 3, 4 // The comma should be inserted before the comment - -> "a" - 5, - 6 /* The comma should be inserted before the comment */ - -> "a" - else -> "b" - } - """.trimIndent() - val formattedCode = - """ - fun foo(bar: Int): String = when(bar) { - 1, 2 -> "a" - 3, 4, // The comma should be inserted before the comment - -> "a" - 5, - 6, /* The comma should be inserted before the comment */ - -> "a" - else -> "b" - } - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to true) - .hasLintViolations( - LintViolation(3, 9, "Missing trailing comma before \"->\""), - LintViolation(6, 6, "Missing trailing comma before \"->\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is not allowed on declaration site then remove it from the destructuring declaration when present`() { - val code = - """ - fun foo() { - fun bar(): Pair = Pair(1, 2) - - val (x, y,) = bar() - val ( - x, - y, // The comma before the comment should be removed without removing the comment itself - ) = bar() - val ( - x, - y, /* The comma before the comment should be removed without removing the comment itself */ - ) = bar() - } - """.trimIndent() - val formattedCode = - """ - fun foo() { - fun bar(): Pair = Pair(1, 2) - - val (x, y) = bar() - val ( - x, - y // The comma before the comment should be removed without removing the comment itself - ) = bar() - val ( - x, - y /* The comma before the comment should be removed without removing the comment itself */ - ) = bar() - } - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to false) - .hasLintViolations( - LintViolation(4, 14, "Unnecessary trailing comma before \")\""), - LintViolation(7, 10, "Unnecessary trailing comma before \")\""), - LintViolation(11, 10, "Unnecessary trailing comma before \")\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is required on declaration site then add it to the destructuring declaration when missing`() { - val code = - """ - fun foo() { - fun bar(): Pair = Pair(1, 2) - - val (x, y) = bar() - val ( - x, - y // The comma should be inserted before the comment - ) = bar() - val ( - x, - y /* The comma should be inserted before the comment */ - ) = bar() - } - """.trimIndent() - val formattedCode = - """ - fun foo() { - fun bar(): Pair = Pair(1, 2) - - val (x, y) = bar() - val ( - x, - y, // The comma should be inserted before the comment - ) = bar() - val ( - x, - y, /* The comma should be inserted before the comment */ - ) = bar() - } - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to true) - .hasLintViolations( - LintViolation(7, 10, "Missing trailing comma before \")\""), - LintViolation(11, 10, "Missing trailing comma before \")\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is not allowed on declaration site then remove it from the function literal when present`() { - val code = - """ - val fooBar1: (Int, Int) -> Int = { foo, bar, -> foo * bar } - val fooBar2: (Int, Int) -> Int = { - foo, - bar, // The comma before the comment should be removed without removing the comment itself - -> foo * bar - } - val fooBar3: (Int, Int) -> Int = { - foo, - bar, /* The comma before the comment should be removed without removing the comment itself */ - -> foo * bar - } - """.trimIndent() - val formattedCode = - """ - val fooBar1: (Int, Int) -> Int = { foo, bar -> foo * bar } - val fooBar2: (Int, Int) -> Int = { - foo, - bar // The comma before the comment should be removed without removing the comment itself - -> foo * bar - } - val fooBar3: (Int, Int) -> Int = { - foo, - bar /* The comma before the comment should be removed without removing the comment itself */ - -> foo * bar - } - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to false) - .hasLintViolations( - LintViolation(1, 44, "Unnecessary trailing comma before \"->\""), - LintViolation(4, 12, "Unnecessary trailing comma before \"->\""), - LintViolation(9, 12, "Unnecessary trailing comma before \"->\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is required on declaration site then add it to function literal when missing`() { - val code = - """ - val fooBar1: (Int, Int) -> Int = { foo, bar -> foo * bar } - val fooBar2: (Int, Int) -> Int = { - foo, - bar // The comma should be inserted before the comment - -> foo * bar - } - val fooBar3: (Int, Int) -> Int = { - foo, - bar /* The comma should be inserted before the comment */ - -> foo * bar - } - """.trimIndent() - val formattedCode = - """ - val fooBar1: (Int, Int) -> Int = { foo, bar -> foo * bar } - val fooBar2: (Int, Int) -> Int = { - foo, - bar, // The comma should be inserted before the comment - -> foo * bar - } - val fooBar3: (Int, Int) -> Int = { - foo, - bar, /* The comma should be inserted before the comment */ - -> foo * bar - } - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to true) - .hasLintViolations( - LintViolation(4, 12, "Missing trailing comma before \"->\""), - LintViolation(9, 12, "Missing trailing comma before \"->\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is not allowed on call site then remove it from the type argument list when present`() { - val code = - """ - val list1: List = emptyList() - val list2: List< - String, // The comma before the comment should be removed without removing the comment itself - > = emptyList() - val list3: List< - String, /* The comma before the comment should be removed without removing the comment itself */ - > = emptyList() - """.trimIndent() - val formattedCode = - """ - val list1: List = emptyList() - val list2: List< - String // The comma before the comment should be removed without removing the comment itself - > = emptyList() - val list3: List< - String /* The comma before the comment should be removed without removing the comment itself */ - > = emptyList() - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to false) - .hasLintViolations( - LintViolation(1, 23, "Unnecessary trailing comma before \">\""), - LintViolation(3, 11, "Unnecessary trailing comma before \">\""), - LintViolation(6, 11, "Unnecessary trailing comma before \">\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is required on call site then add it to the type argument list when missing`() { - val code = - """ - val list1: List = emptyList() - val list2: List< - String // The comma should be inserted before the comment - > = emptyList() - val list3: List< - String /* The comma should be inserted before the comment */ - > = emptyList() - """.trimIndent() - val formattedCode = - """ - val list1: List = emptyList() - val list2: List< - String, // The comma should be inserted before the comment - > = emptyList() - val list3: List< - String, /* The comma should be inserted before the comment */ - > = emptyList() - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) - .hasLintViolations( - LintViolation(3, 11, "Missing trailing comma before \">\""), - LintViolation(6, 11, "Missing trailing comma before \">\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is not allowed on call site then remove it from the array index when present`() { - val code = - """ - val foo = Array(2) { 42 } - val bar1 = foo[1,] - val bar2 = foo[ - 1, // The comma before the comment should be removed without removing the comment itself - ] - val bar3 = foo[ - 1, /* The comma before the comment should be removed without removing the comment itself */ - ] - """.trimIndent() - val formattedCode = - """ - val foo = Array(2) { 42 } - val bar1 = foo[1] - val bar2 = foo[ - 1 // The comma before the comment should be removed without removing the comment itself - ] - val bar3 = foo[ - 1 /* The comma before the comment should be removed without removing the comment itself */ - ] - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to false) - .hasLintViolations( - LintViolation(2, 17, "Unnecessary trailing comma before \"]\""), - LintViolation(4, 6, "Unnecessary trailing comma before \"]\""), - LintViolation(7, 6, "Unnecessary trailing comma before \"]\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is required on call site then add it to the array index when missing`() { - val code = - """ - val foo = Array(2) { 42 } - val bar1 = foo[1] - val bar2 = foo[ - 1 // The comma should be inserted before the comment - ] - val bar3 = foo[ - 1 /* The comma should be inserted before the comment */ - ] - """.trimIndent() - val formattedCode = - """ - val foo = Array(2) { 42 } - val bar1 = foo[1] - val bar2 = foo[ - 1, // The comma should be inserted before the comment - ] - val bar3 = foo[ - 1, /* The comma should be inserted before the comment */ - ] - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) - .hasLintViolations( - LintViolation(4, 6, "Missing trailing comma before \"]\""), - LintViolation(7, 6, "Missing trailing comma before \"]\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is not allowed on call site then remove it from the collection literal when present`() { - val code = - """ - annotation class Annotation(val params: IntArray) - - @Annotation([1, 2,]) - val foo1: Int = 0 - - @Annotation([ - 1, - 2, // The comma before the comment should be removed without removing the comment itself - ]) - val foo2: Int = 0 - - @Annotation([ - 1, - 2, /* The comma before the comment should be removed without removing the comment itself */ - ]) - val foo3: Int = 0 - """.trimIndent() - val formattedCode = - """ - annotation class Annotation(val params: IntArray) - - @Annotation([1, 2]) - val foo1: Int = 0 - - @Annotation([ - 1, - 2 // The comma before the comment should be removed without removing the comment itself - ]) - val foo2: Int = 0 - - @Annotation([ - 1, - 2 /* The comma before the comment should be removed without removing the comment itself */ - ]) - val foo3: Int = 0 - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to false) - .hasLintViolations( - LintViolation(3, 18, "Unnecessary trailing comma before \"]\""), - LintViolation(8, 6, "Unnecessary trailing comma before \"]\""), - LintViolation(14, 6, "Unnecessary trailing comma before \"]\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that the trailing comma is required on call site then add it to the collection literal when missing`() { - val code = - """ - annotation class Annotation(val params: IntArray) - - @Annotation([1, 2]) - val foo1: Int = 0 - - @Annotation([ - 1, - 2 // The comma should be inserted before the comment - ]) - val foo2: Int = 0 - - @Annotation([ - 1, - 2 /* The comma should be inserted before the comment */ - ]) - val foo3: Int = 0 - - @Annotation( - [ - 1, - 2 /* The comma should be inserted before the comment */ - ], - [ - 3, - 4 /* The comma should be inserted before the comment */ - ] - ) - val foo4: Int = 0 - """.trimIndent() - val formattedCode = - """ - annotation class Annotation(val params: IntArray) - - @Annotation([1, 2]) - val foo1: Int = 0 - - @Annotation([ - 1, - 2, // The comma should be inserted before the comment - ]) - val foo2: Int = 0 - - @Annotation([ - 1, - 2, /* The comma should be inserted before the comment */ - ]) - val foo3: Int = 0 - - @Annotation( - [ - 1, - 2, /* The comma should be inserted before the comment */ - ], - [ - 3, - 4, /* The comma should be inserted before the comment */ - ], + LintViolation(11, 16, "Missing trailing comma before \"->\"") ) - val foo4: Int = 0 - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) - .hasLintViolations( - LintViolation(8, 6, "Missing trailing comma before \"]\""), - LintViolation(14, 6, "Missing trailing comma before \"]\""), - LintViolation(21, 10, "Missing trailing comma before \"]\""), - LintViolation(25, 10, "Missing trailing comma before \"]\""), - LintViolation(26, 6, "Missing trailing comma before \")\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `1297 - Given that the trailing comma is required on call site the a trailing comma to collection literal when missing`() { - val code = - """ - annotation class FooBar( - val foo1: Array = [], - val foo2: Array = [], - val bar1: String = "" - ) - - @FooBar( - foo1 = [ - "foo-1" // Add trailing comma as the argument value list foo1 is a multiline statement - ], - foo2 = ["foo-2"], // Do not add trailing comma in the array as the argument value list foo2 is a single line statement - bar1 = "bar-1" // Add trailing comma as the outer argument value list of the annotation is a multiline statement - ) - val fooBar = null - """.trimIndent() - val formattedCode = - """ - annotation class FooBar( - val foo1: Array = [], - val foo2: Array = [], - val bar1: String = "" - ) - - @FooBar( - foo1 = [ - "foo-1", // Add trailing comma as the argument value list foo1 is a multiline statement - ], - foo2 = ["foo-2"], // Do not add trailing comma in the array as the argument value list foo2 is a single line statement - bar1 = "bar-1", // Add trailing comma as the outer argument value list of the annotation is a multiline statement + .hasLintViolationsForAdditionalRules( + LintViolation(15, 22, "Missing trailing comma before \")\"") ) - val fooBar = null - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) - .hasLintViolations( - LintViolation(9, 16, "Missing trailing comma before \"]\""), - LintViolation(12, 19, "Missing trailing comma before \")\"") - ).isFormattedAs(formattedCode) + .isFormattedAs(formattedCode) } @Test @@ -906,119 +87,9 @@ class TrailingCommaRuleTest { } } """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to true) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) + combinedTrailingCommaAssertThat(code) + .withEditorConfigOverride(TrailingCommaOnDeclarationSiteRule.allowTrailingCommaProperty to true) + .withEditorConfigOverride(TrailingCommaOnCallSiteRule.allowTrailingCommaOnCallSiteProperty to true) .hasNoLintViolations() } - - @Test - fun `Issue 1312 - Given that a trailing comma is required on declaration site and multiple elements then force lambda arrow to next line`() { - val code = - """ - fun foo(bar: Int): String = when(bar) { - 1, 2 -> "a" - 3, - 4 -> "b" - 5, - 6-> "c" - else -> "d" - } - """.trimIndent() - val formattedCode = - """ - fun foo(bar: Int): String = when(bar) { - 1, 2 -> "a" - 3, - 4, - -> "b" - 5, - 6, - -> "c" - else -> "d" - } - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaProperty to true) - .hasLintViolations( - LintViolation(4, 6, "Missing trailing comma and newline before \"->\""), - LintViolation(6, 6, "Missing trailing comma and newline before \"->\"") - ).isFormattedAs(formattedCode) - } - - @Test - fun `Given that a trailing comma is required on declaration site and unused imports do not affect each other`() { - val code = - """ - package com.pinterest.ktlint - import com.pinterest.ktlint.enum.Enum - import com.pinterest.ktlint.enum.EnumThree - import com.pinterest.ktlint.enum.EnumTwo - data class TrailingCommaTest( - val foo: String, - val bar: Enum, - val bar2: EnumTwo, - val bar3: EnumThree - ) - """.trimIndent() - val formattedCode = - """ - package com.pinterest.ktlint - import com.pinterest.ktlint.enum.Enum - import com.pinterest.ktlint.enum.EnumThree - import com.pinterest.ktlint.enum.EnumTwo - data class TrailingCommaTest( - val foo: String, - val bar: Enum, - val bar2: EnumTwo, - val bar3: EnumThree, - ) - """.trimIndent() - trailingCommaRuleAssertThat(code) - // When running format mode, the rules are first executed in parallel to find linting errors. In this - // process, no unused import are found because the trailing comma is not yet added to variable "bar3". Then - // in the next stage the rules are run consecutively. Now the trailing comma rule is adding a trailing comma - // after the type of variable "bar3". When the no-unused-import rule runs after the trailing-comma rule, it - // was incorrectly seen as part of the type of variable "bar3" and a reference "EnumThree," (with the - // trailing comma was added) which in turn resulted in not recognizing that the import of EnumThree actually - // was used. - .addAdditionalRules(NoUnusedImportsRule()) - .withEditorConfigOverride(allowTrailingCommaProperty to true) - .hasLintViolation(9, 24, "Missing trailing comma before \")\"") - .isFormattedAs(formattedCode) - } - - @Test - fun `Issue 1379 - Given that a trailing comma is required on call site then add trailing comma after array in annotation when missing`() { - val code = - """ - import kotlin.reflect.KClass - - @Foo( - values = [ - Foo::class, - Foo::class - ] - ) - annotation class Foo(val values: Array>) - """.trimIndent() - val formattedCode = - """ - import kotlin.reflect.KClass - - @Foo( - values = [ - Foo::class, - Foo::class, - ], - ) - annotation class Foo(val values: Array>) - """.trimIndent() - trailingCommaRuleAssertThat(code) - .withEditorConfigOverride(allowTrailingCommaOnCallSiteProperty to true) - .hasLintViolations( - LintViolation(6, 19, "Missing trailing comma before \"]\""), - LintViolation(7, 6, "Missing trailing comma before \")\"") - ).isFormattedAs(formattedCode) - } }