diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/NoTrailingCommaRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/NoTrailingCommaRule.kt index c09129176a..476058d25d 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/NoTrailingCommaRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/NoTrailingCommaRule.kt @@ -1,36 +1,76 @@ package com.pinterest.ktlint.ruleset.experimental +import com.pinterest.ktlint.core.KtLint import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.api.FeatureInAlphaState +import com.pinterest.ktlint.core.api.UsesEditorConfigProperties import com.pinterest.ktlint.core.ast.ElementType import com.pinterest.ktlint.core.ast.assertElementType import com.pinterest.ktlint.core.ast.children -import com.pinterest.ktlint.core.ast.logStructure +import com.pinterest.ktlint.core.ast.isRoot 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.psi.psiUtil.endOffset -class NoTrailingCommaRule : Rule("no-trailing-comma") { +@OptIn(FeatureInAlphaState::class) +class NoTrailingCommaRule : + Rule("no-trailing-comma"), + UsesEditorConfigProperties { + + private var allowTrailingComma by Delegates.notNull() + private var allowTrailingCommaOnCallSite by Delegates.notNull() + + override val editorConfigProperties: List> = listOf( + ijKotlinAllowTrailingCommaEditorConfigProperty, + ijKotlinAllowTrailingCommaOnCallSiteEditorConfigProperty, + ) override fun visit( node: ASTNode, autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit ) { - when (node.elementType) { - ElementType.COLLECTION_LITERAL_EXPRESSION -> visitCollectionLiteralExpression(node, emit, autoCorrect) - ElementType.DESTRUCTURING_DECLARATION -> visitDestructuringDeclaration(node, emit, autoCorrect) - ElementType.FUNCTION_LITERAL -> visitFunctionLiteral(node, emit, autoCorrect) - ElementType.FUNCTION_TYPE -> visitFunctionType(node, emit, autoCorrect) - ElementType.INDICES -> visitIndices(node, emit, autoCorrect) - ElementType.TYPE_ARGUMENT_LIST -> visitTypeList(node, emit, autoCorrect) - ElementType.TYPE_PARAMETER_LIST -> visitTypeList(node, emit, autoCorrect) - ElementType.VALUE_ARGUMENT_LIST -> visitValueList(node, emit, autoCorrect) - ElementType.VALUE_PARAMETER_LIST -> visitValueList(node, emit, autoCorrect) - ElementType.WHEN_ENTRY -> visitWhenEntry(node, emit, autoCorrect) - else -> Unit + if (node.isRoot()) { + getEditorConfigValues(node) + return + } + + // 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 + if (!allowTrailingComma) { + when (node.elementType) { + ElementType.DESTRUCTURING_DECLARATION -> visitDestructuringDeclaration(node, emit, autoCorrect) + ElementType.FUNCTION_LITERAL -> visitFunctionLiteral(node, emit, autoCorrect) + ElementType.FUNCTION_TYPE -> visitFunctionType(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 + } + } + if (!allowTrailingCommaOnCallSite) { + 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 getEditorConfigValues(node: ASTNode) { + val android = node.getUserData(KtLint.ANDROID_USER_DATA_KEY) ?: false + val editorConfig = node.getUserData(KtLint.EDITOR_CONFIG_PROPERTIES_USER_DATA_KEY)!! + allowTrailingComma = + editorConfig.getEditorConfigValue(ijKotlinAllowTrailingCommaEditorConfigProperty, android) + allowTrailingCommaOnCallSite = + editorConfig.getEditorConfigValue(ijKotlinAllowTrailingCommaOnCallSiteEditorConfigProperty, android) + } + private fun visitCollectionLiteralExpression( node: ASTNode, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, @@ -172,4 +212,49 @@ class NoTrailingCommaRule : Rule("no-trailing-comma") { elementType == ElementType.WHITE_SPACE || elementType == ElementType.EOL_COMMENT || elementType == ElementType.BLOCK_COMMENT + + internal companion object { + /** A parser for kotlin boolean values `true` and `false` */ + private var KOTLIN_BOOLEAN_VALUE_PARSER: PropertyValueParser = + PropertyValueParser { name, value -> + when { + value == null -> PropertyType.PropertyValue.invalid( + null, + "Property '$name' expects a boolean; found: null" + ) + value.equals("true", ignoreCase = true) -> PropertyType.PropertyValue.valid(value, true) + value.equals("false", ignoreCase = true) -> PropertyType.PropertyValue.valid(value, false) + else -> PropertyType.PropertyValue.invalid( + value, + "Property '$name' expects a boolean. The parsed '$value' is not a boolean." + ) + } + } + + internal val ijKotlinAllowTrailingCommaEditorConfigProperty = + UsesEditorConfigProperties.EditorConfigProperty( + type = PropertyType( + "ij_kotlin_allow_trailing_comma", + "ij_kotlin_allow_trailing_comma description", + KOTLIN_BOOLEAN_VALUE_PARSER, + setOf("true", "false") + ), + defaultValue = false, + defaultAndroidValue = false, + propertyWriter = { it.toString() } + ) + + internal val ijKotlinAllowTrailingCommaOnCallSiteEditorConfigProperty = + UsesEditorConfigProperties.EditorConfigProperty( + type = PropertyType( + "ij_kotlin_allow_trailing_comma_on_call_site", + "ij_kotlin_allow_trailing_comma_on_call_site description", + KOTLIN_BOOLEAN_VALUE_PARSER, + setOf("true", "false") + ), + defaultValue = false, + defaultAndroidValue = false, + propertyWriter = { it.toString() } + ) + } } diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/NoTrailingCommaRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/NoTrailingCommaRuleTest.kt index 664b5aa6f1..c7e38d8cee 100644 --- a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/NoTrailingCommaRuleTest.kt +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/NoTrailingCommaRuleTest.kt @@ -1,12 +1,74 @@ package com.pinterest.ktlint.ruleset.experimental import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.core.api.FeatureInAlphaState +import com.pinterest.ktlint.test.EditorConfigTestRule import com.pinterest.ktlint.test.format import com.pinterest.ktlint.test.lint import org.assertj.core.api.Assertions.assertThat +import org.ec4j.core.model.PropertyType +import org.junit.Rule import org.junit.Test +@FeatureInAlphaState class NoTrailingCommaRuleTest { + @get:Rule + val editorConfigTestRule = EditorConfigTestRule() + + @Test + fun testAllowTrailingCommaOnCallSite() { + 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 editorConfigFilePath = writeEditorConfigFile(ALLOW_TRAILING_COMMA_ON_CALL_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEmpty() + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)).isEqualTo(code) + } + + @Test + fun testAllowTrailingCommaOnDeclarationSite() { + 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 editorConfigFilePath = writeEditorConfigFile(ALLOW_TRAILING_COMMA_ON_DECLARATION_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEmpty() + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)).isEqualTo(code) + } + @Test fun testFormatIsCorrectWithArgumentList() { val code = @@ -34,14 +96,16 @@ class NoTrailingCommaRuleTest { ) """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_CALL_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 1, col = 28, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 4, col = 8, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 8, col = 8, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -68,14 +132,16 @@ class NoTrailingCommaRuleTest { ) """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_DECLARATION_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 1, col = 29, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 3, col = 16, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 6, col = 16, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -106,14 +172,16 @@ class NoTrailingCommaRuleTest { > {} """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_DECLARATION_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 1, col = 16, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 4, col = 6, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 8, col = 6, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -140,14 +208,16 @@ class NoTrailingCommaRuleTest { } """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_DECLARATION_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 2, col = 9, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 3, col = 9, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 5, col = 9, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -186,14 +256,16 @@ class NoTrailingCommaRuleTest { } """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_DECLARATION_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 4, col = 14, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 7, col = 10, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 11, col = 10, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -224,14 +296,16 @@ class NoTrailingCommaRuleTest { ) """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_CALL_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 1, col = 21, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 4, col = 6, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 8, col = 6, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -262,14 +336,16 @@ class NoTrailingCommaRuleTest { ) -> Int = 42 """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_DECLARATION_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 1, col = 23, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 4, col = 8, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 8, col = 8, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -304,14 +380,16 @@ class NoTrailingCommaRuleTest { } """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_DECLARATION_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 1, col = 44, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 4, col = 8, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 9, col = 8, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -338,14 +416,16 @@ class NoTrailingCommaRuleTest { > = emptyList() """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_CALL_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 1, col = 23, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 3, col = 11, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 6, col = 11, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -374,14 +454,16 @@ class NoTrailingCommaRuleTest { ] """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_CALL_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 2, col = 17, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 4, col = 6, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 7, col = 6, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } @@ -426,14 +508,29 @@ class NoTrailingCommaRuleTest { val foo3: Int = 0 """.trimIndent() - assertThat(NoTrailingCommaRule().lint(code)).isEqualTo( + val editorConfigFilePath = writeEditorConfigFile(DO_NOT_ALLOW_TRAILING_COMMA_ON_CALL_SITE).absolutePath + + assertThat(NoTrailingCommaRule().lint(editorConfigFilePath, code)).isEqualTo( listOf( LintError(line = 3, col = 18, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 8, col = 6, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), LintError(line = 14, col = 6, ruleId = "no-trailing-comma", detail = "Trailing comma is redundant"), ) ) - assertThat(NoTrailingCommaRule().format(code)) + assertThat(NoTrailingCommaRule().format(editorConfigFilePath, code)) .isEqualTo(autoCorrectedCode) } + + private fun writeEditorConfigFile(editorConfigProperty: Pair, String>) = editorConfigTestRule + .writeToEditorConfig( + mapOf(editorConfigProperty) + ) + + private companion object { + val ALLOW_TRAILING_COMMA_ON_DECLARATION_SITE = NoTrailingCommaRule.ijKotlinAllowTrailingCommaEditorConfigProperty.type to true.toString() + val DO_NOT_ALLOW_TRAILING_COMMA_ON_DECLARATION_SITE = NoTrailingCommaRule.ijKotlinAllowTrailingCommaEditorConfigProperty.type to false.toString() + + val ALLOW_TRAILING_COMMA_ON_CALL_SITE = NoTrailingCommaRule.ijKotlinAllowTrailingCommaOnCallSiteEditorConfigProperty.type to true.toString() + val DO_NOT_ALLOW_TRAILING_COMMA_ON_CALL_SITE = NoTrailingCommaRule.ijKotlinAllowTrailingCommaOnCallSiteEditorConfigProperty.type to false.toString() + } }