From f7f7900ffe5a63289e8bc611dedc9a721c62e079 Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Wed, 9 Mar 2022 19:41:48 +0100 Subject: [PATCH] Add rule ModifierListSpacingRule (#1361) This rule lints and format the spacing between modifier in and after the last modifier in a modifier list. This rule is required to create a rule which can rewrite the function signature automatically as is described in #1341 --- CHANGELOG.md | 1 + README.md | 1 + .../ExperimentalRuleSetProvider.kt | 3 +- .../experimental/ModifierListSpacingRule.kt | 88 ++++++++++ .../ModifierListSpacingRuleTest.kt | 165 ++++++++++++++++++ 5 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRule.kt create mode 100644 ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRuleTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index b6bd331005..3d2d7d7886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Please welcome [paul-dingemans](https://github.com/paul-dingemans) as an officia - Basic tests for CLI ([#540](https://github.com/pinterest/ktlint/issues/540)) - Add experimental rule for unexpected spaces in a type reference before a function identifier (`function-type-reference-spacing`) ([#1341](https://github.com/pinterest/ktlint/issues/1341)) - Add experimental rule for unnecessary parentheses in function call followed by lambda ([#1068](https://github.com/pinterest/ktlint/issues/1068)) +- Add experimental rules for unnecessary spacing between modifiers in and after the last modifier in a modifier list ([#1361](https://github.com/pinterest/ktlint/pull/1361)) ### Fixed - Fix indentation of function literal ([#1247](https://github.com/pinterest/ktlint/issues/1247)) diff --git a/README.md b/README.md index 533c6f863d..ec81811263 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ by passing the `--experimental` flag to `ktlint`. - `experimental:annotation-spacing`: Annotations should be separated by the annotated declaration by a single line break - `experimental:double-colon-spacing`: No spaces around `::` - `experimental:function-type-reference-spacing`: Consistent spacing in the type reference before a function +- `experimental:modifier-list-spacing`: Consistent spacing between modifiers in and after the last modifier in a modifier list - `experimental:spacing-around-angle-brackets`: No spaces around angle brackets - `experimental:spacing-between-declarations-with-annotations`: Declarations with annotations should be separated by a blank line - `experimental:spacing-between-declarations-with-comments`: Declarations with comments should be separated by a blank line diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt index d51c85fd9b..da7a44ede1 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt @@ -21,7 +21,8 @@ public class ExperimentalRuleSetProvider : RuleSetProvider { SpacingAroundAngleBracketsRule(), SpacingAroundUnaryOperatorRule(), AnnotationSpacingRule(), + UnnecessaryParenthesesBeforeTrailingLambdaRule(), FunctionTypeReferenceSpacingRule(), - UnnecessaryParenthesesBeforeTrailingLambdaRule() + ModifierListSpacingRule() ) } diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRule.kt new file mode 100644 index 0000000000..15866ce058 --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRule.kt @@ -0,0 +1,88 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION_ENTRY +import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST +import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE +import com.pinterest.ktlint.core.ast.children +import com.pinterest.ktlint.core.ast.isPartOfComment +import com.pinterest.ktlint.core.ast.lineIndent +import com.pinterest.ktlint.core.ast.nextLeaf +import com.pinterest.ktlint.core.ast.nextSibling +import com.pinterest.ktlint.core.ast.prevLeaf +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement + +/** + * Lint and format the spacing between the modifiers in and after the last modifier in a modifier list. + */ +public class ModifierListSpacingRule : Rule("modifier-list-spacing") { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == MODIFIER_LIST) { + node + .children() + .forEach { visitModifierChild(it, autoCorrect, emit) } + // The whitespace of the last entry of the modifier list is actually placed outside the modifier list + visitModifierChild(node, autoCorrect, emit) + } + } + + private fun visitModifierChild( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == WHITE_SPACE) { + return + } + node.nextSibling { it.elementType == WHITE_SPACE && it.nextLeaf()?.isPartOfComment() != true } + ?.takeIf { it.elementType == WHITE_SPACE } + ?.takeUnless { + // Regardless of element type, a single white space is always ok and does not need to be checked. + it.text == " " + } + ?.takeUnless { + // An annotation entry followed by a single newline (and possibly an indent for the next line) is + // always ok and does not need further checking. + it.elementType == ANNOTATION_ENTRY && it.text.trimEnd(' ', '\t') == "\n" + } + ?.takeUnless { + // A single newline after a comment is always ok and does not need further checking. + it.text.trim(' ', '\t').contains('\n') && it.prevLeaf()?.isPartOfComment() == true + } + ?.let { whitespace -> + if (node.elementType == ANNOTATION_ENTRY || + (node.elementType == MODIFIER_LIST && node.lastChildNode?.elementType == ANNOTATION_ENTRY) + ) { + val expectedWhiteSpace = if (whitespace.textContains('\n')) { + "\n" + node.lineIndent() + } else { + " " + } + if (whitespace.text != expectedWhiteSpace) { + emit( + whitespace.startOffset, + "Single whitespace or newline expected after annotation", + true + ) + if (autoCorrect) { + (whitespace as LeafPsiElement).rawReplaceWithText(expectedWhiteSpace) + } + } + } else { + emit( + whitespace.startOffset, + "Single whitespace expected after modifier", + true + ) + if (autoCorrect) { + (whitespace as LeafPsiElement).rawReplaceWithText(" ") + } + } + } + } +} diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRuleTest.kt new file mode 100644 index 0000000000..214951424c --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/ModifierListSpacingRuleTest.kt @@ -0,0 +1,165 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.test.format +import com.pinterest.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ModifierListSpacingRuleTest { + @Test + fun `Given a function preceded by multiple modifiers separated by multiple space then remove redundant spaces`() { + val code = + """ + abstract class Foo { + @Throws(RuntimeException::class) + protected abstract suspend fun execute() + } + """.trimIndent() + val formattedCode = + """ + abstract class Foo { + @Throws(RuntimeException::class) + protected abstract suspend fun execute() + } + """.trimIndent() + assertThat(ModifierListSpacingRule().lint(code)).containsExactly( + LintError(1, 9, "modifier-list-spacing", "Single whitespace expected after modifier"), + LintError(3, 14, "modifier-list-spacing", "Single whitespace expected after modifier"), + LintError(3, 24, "modifier-list-spacing", "Single whitespace expected after modifier"), + LintError(3, 33, "modifier-list-spacing", "Single whitespace expected after modifier") + ) + assertThat(ModifierListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a function preceded by multiple modifiers separated by newlines then remove redundant spaces`() { + val code = + """ + abstract + class Foo { + @Throws(RuntimeException::class) + protected + abstract + suspend + fun execute() + } + """.trimIndent() + val formattedCode = + """ + abstract class Foo { + @Throws(RuntimeException::class) + protected abstract suspend fun execute() + } + """.trimIndent() + assertThat(ModifierListSpacingRule().lint(code)).containsExactly( + LintError(1, 9, "modifier-list-spacing", "Single whitespace expected after modifier"), + LintError(4, 14, "modifier-list-spacing", "Single whitespace expected after modifier"), + LintError(5, 13, "modifier-list-spacing", "Single whitespace expected after modifier"), + LintError(6, 12, "modifier-list-spacing", "Single whitespace expected after modifier") + ) + assertThat(ModifierListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a modifier list followed by multiple space then remove the redundant spaces`() { + val code = + """ + fun foo(vararg bar) = "some-result" + fun foo( + vararg + bar + ) = "some-result" + """.trimIndent() + val formattedCode = + """ + fun foo(vararg bar) = "some-result" + fun foo( + vararg bar + ) = "some-result" + """.trimIndent() + assertThat(ModifierListSpacingRule().lint(code)).containsExactly( + LintError(1, 15, "modifier-list-spacing", "Single whitespace expected after modifier"), + LintError(3, 11, "modifier-list-spacing", "Single whitespace expected after modifier") + ) + assertThat(ModifierListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Annotation modifiers may be followed by a newline or a space`() { + val code = + """ + @Foo1 @Foo2 + class Bar {} + """.trimIndent() + assertThat(ModifierListSpacingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Annotation modifiers may not be followed by multiple spaces`() { + val code = + """ + @Foo1 @Foo2 class Bar {} + """.trimIndent() + val formattedCode = + """ + @Foo1 @Foo2 class Bar {} + """.trimIndent() + assertThat(ModifierListSpacingRule().lint(code)).containsExactly( + LintError(1, 6, "modifier-list-spacing", "Single whitespace or newline expected after annotation"), + LintError(1, 13, "modifier-list-spacing", "Single whitespace or newline expected after annotation") + ) + assertThat(ModifierListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Annotation modifiers may not be followed by multiple newlines`() { + val code = + """ + @Foo1 + + @Foo2 + + class Bar {} + """.trimIndent() + val formattedCode = + """ + @Foo1 + @Foo2 + class Bar {} + """.trimIndent() + assertThat(ModifierListSpacingRule().lint(code)).containsExactly( + LintError(1, 6, "modifier-list-spacing", "Single whitespace or newline expected after annotation"), + LintError(3, 6, "modifier-list-spacing", "Single whitespace or newline expected after annotation") + ) + assertThat(ModifierListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given annotations that correctly indented then do no emit warnings`() { + val code = + """ + @Foo1 + @Foo2 + class Bar {} + """.trimIndent() + assertThat(ModifierListSpacingRule().lint(code)).isEmpty() + assertThat(ModifierListSpacingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given annotations followed by comments that correctly indented then do no emit warnings`() { + val code = + """ + @Foo1 // some-comment + @Foo2 + /** + * Some comment + */ + @Foo3 + class Bar {} + """.trimIndent() + assertThat(ModifierListSpacingRule().lint(code)).isEmpty() + assertThat(ModifierListSpacingRule().format(code)).isEqualTo(code) + } +}