diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c49dcbfb5..5a4038fb86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Allow to disable ktlint in `.editorconfig` for a glob ([#2100](https://github.com/pinterest/ktlint/issues/2100)) * Fix wrapping of nested function literals `wrapping` ([#2106](https://github.com/pinterest/ktlint/issues/2106)) * Do not indent class body for classes having a long super type list in code style `ktlint_official` as it is inconsistent compared to other class bodies `indent` [#2115](https://github.com/pinterest/ktlint/issues/2115) +* Fix spacing around colon in annotations `spacing-around-colon` ([#2093](https://github.com/pinterest/ktlint/issues/2093)) ### Changed diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundColonRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundColonRule.kt index a3a8c61af3..4105a3c79e 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundColonRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundColonRule.kt @@ -1,14 +1,12 @@ package com.pinterest.ktlint.ruleset.standard.rules -import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION -import com.pinterest.ktlint.rule.engine.core.api.ElementType.ANNOTATION_ENTRY import com.pinterest.ktlint.rule.engine.core.api.ElementType.COLON import com.pinterest.ktlint.rule.engine.core.api.ElementType.EQ import com.pinterest.ktlint.rule.engine.core.api.RuleId -import com.pinterest.ktlint.rule.engine.core.api.isPartOf import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithNewline +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpaceWithoutNewline import com.pinterest.ktlint.rule.engine.core.api.nextLeaf import com.pinterest.ktlint.rule.engine.core.api.nextSibling import com.pinterest.ktlint.rule.engine.core.api.prevLeaf @@ -18,6 +16,8 @@ import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe import com.pinterest.ktlint.ruleset.standard.StandardRule import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.psi.KtAnnotation +import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtBlockExpression import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtConstructor @@ -35,138 +35,190 @@ public class SpacingAroundColonRule : StandardRule("colon-spacing") { emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { if (node.elementType == COLON) { - val psiParent = node.psi.parent - if (node.isPartOf(ANNOTATION) || node.isPartOf(ANNOTATION_ENTRY)) { - // TODO: https://github.com/pinterest/ktlint/issues/2093 Enforce no spacing - return - } - val prevLeaf = node.prevLeaf() - if (prevLeaf != null && prevLeaf.isWhiteSpaceWithNewline()) { - emit(prevLeaf.startOffset, "Unexpected newline before \":\"", true) - if (autoCorrect) { - val prevNonCodeElements = - node - .siblings(forward = false) - .takeWhile { it.isWhiteSpace() || it.isPartOfComment() } - .toList() - .reversed() - when { - psiParent is KtProperty || psiParent is KtNamedFunction -> { - val equalsSignElement = - node - .siblings(forward = true) - .firstOrNull { it.elementType == EQ } - if (equalsSignElement != null) { - equalsSignElement - .treeNext - ?.let { treeNext -> - prevNonCodeElements.forEach { - node.treeParent.addChild(it, treeNext) - } - if (treeNext.isWhiteSpace()) { - equalsSignElement.treeParent.removeChild(treeNext) - } - Unit + removeUnexpectedNewlineBefore(node, emit, autoCorrect) + removeUnexpectedSpacingAround(node, emit, autoCorrect) + addMissingSpacingAround(node, emit, autoCorrect) + } + } + + private fun removeUnexpectedNewlineBefore( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean, + ) { + val psiParent = node.psi.parent + val prevLeaf = node.prevLeaf() + if (prevLeaf != null && prevLeaf.isWhiteSpaceWithNewline()) { + emit(prevLeaf.startOffset, "Unexpected newline before \":\"", true) + if (autoCorrect) { + val prevNonCodeElements = + node + .siblings(forward = false) + .takeWhile { it.isWhiteSpace() || it.isPartOfComment() } + .toList() + .reversed() + when { + psiParent is KtProperty || psiParent is KtNamedFunction -> { + val equalsSignElement = + node + .siblings(forward = true) + .firstOrNull { it.elementType == EQ } + if (equalsSignElement != null) { + equalsSignElement + .treeNext + ?.let { treeNext -> + prevNonCodeElements.forEach { + node.treeParent.addChild(it, treeNext) + } + if (treeNext.isWhiteSpace()) { + equalsSignElement.treeParent.removeChild(treeNext) + } + Unit + } + } + val blockElement = + node + .siblings(forward = true) + .firstIsInstanceOrNull() + if (blockElement != null) { + val before = + blockElement + .firstChildNode + .nextSibling() + prevNonCodeElements + .let { + if (it.first().isWhiteSpace()) { + blockElement.treeParent.removeChild(it.first()) + it.drop(1) } - } - val blockElement = - node - .siblings(forward = true) - .firstIsInstanceOrNull() - if (blockElement != null) { - val before = - blockElement - .firstChildNode - .nextSibling() - prevNonCodeElements - .let { - if (it.first().isWhiteSpace()) { - blockElement.treeParent.removeChild(it.first()) - it.drop(1) - } - if (it.last().isWhiteSpaceWithNewline()) { - blockElement.treeParent.removeChild(it.last()) - it.dropLast(1) - } else { - it - } - }.forEach { - blockElement.addChild(it, before) + if (it.last().isWhiteSpaceWithNewline()) { + blockElement.treeParent.removeChild(it.last()) + it.dropLast(1) + } else { + it } - } + }.forEach { + blockElement.addChild(it, before) + } } - prevLeaf.prevLeaf()?.isPartOfComment() == true -> { - val nextLeaf = node.nextLeaf() - prevNonCodeElements.forEach { - node.treeParent.addChild(it, nextLeaf) - } - if (nextLeaf != null && nextLeaf.isWhiteSpace()) { - node.treeParent.removeChild(nextLeaf) - } + } + + prevLeaf.prevLeaf()?.isPartOfComment() == true -> { + val nextLeaf = node.nextLeaf() + prevNonCodeElements.forEach { + node.treeParent.addChild(it, nextLeaf) } - else -> { - val text = prevLeaf.text - if (node.removeSpacingBefore) { - prevLeaf.treeParent.removeChild(prevLeaf) - } else { - (prevLeaf as LeafPsiElement).rawReplaceWithText(" ") - } - node.upsertWhitespaceAfterMe(text) + if (nextLeaf != null && nextLeaf.isWhiteSpace()) { + node.treeParent.removeChild(nextLeaf) } } - } - } - if (node.prevSibling().isWhiteSpace() && node.removeSpacingBefore && !prevLeaf.isWhiteSpaceWithNewline()) { - emit(node.startOffset, "Unexpected spacing before \":\"", true) - if (autoCorrect) { - node - .prevSibling() - ?.let { prevSibling -> - prevSibling.treeParent.removeChild(prevSibling) + + else -> { + val text = prevLeaf.text + if (node.spacingBefore) { + (prevLeaf as LeafPsiElement).rawReplaceWithText(" ") + } else { + prevLeaf.treeParent.removeChild(prevLeaf) } + node.upsertWhitespaceAfterMe(text) + } } } - val missingSpacingBefore = - !node.prevSibling().isWhiteSpace() && - ( - psiParent is KtClassOrObject || psiParent is KtConstructor<*> || - psiParent is KtTypeConstraint || psiParent.parent is KtTypeParameterList - ) - val missingSpacingAfter = !node.nextSibling().isWhiteSpace() - when { - missingSpacingBefore && missingSpacingAfter -> { - emit(node.startOffset, "Missing spacing around \":\"", true) - if (autoCorrect) { - node.upsertWhitespaceBeforeMe(" ") - node.upsertWhitespaceAfterMe(" ") + } + } + + private fun removeUnexpectedSpacingAround( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean, + ) { + if (node.prevSibling().isWhiteSpaceWithoutNewline() && node.noSpacingBefore) { + emit(node.startOffset, "Unexpected spacing before \":\"", true) + if (autoCorrect) { + node + .prevSibling() + ?.let { prevSibling -> + prevSibling.treeParent.removeChild(prevSibling) } - } - missingSpacingBefore -> { - emit(node.startOffset, "Missing spacing before \":\"", true) - if (autoCorrect) { - node.upsertWhitespaceBeforeMe(" ") + } + } + if (node.nextSibling().isWhiteSpaceWithoutNewline() && node.spacingAfter) { + emit(node.startOffset, "Unexpected spacing after \":\"", true) + if (autoCorrect) { + node + .nextSibling() + ?.let { nextSibling -> + nextSibling.treeParent.removeChild(nextSibling) } + } + } + } + + private fun addMissingSpacingAround( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean, + ) { + val missingSpacingBefore = !node.prevSibling().isWhiteSpace() && node.spacingBefore + val missingSpacingAfter = !node.nextSibling().isWhiteSpace() && node.noSpacingAfter + when { + missingSpacingBefore && missingSpacingAfter -> { + emit(node.startOffset, "Missing spacing around \":\"", true) + if (autoCorrect) { + node.upsertWhitespaceBeforeMe(" ") + node.upsertWhitespaceAfterMe(" ") } - missingSpacingAfter -> { - emit(node.startOffset + 1, "Missing spacing after \":\"", true) - if (autoCorrect) { - node.upsertWhitespaceAfterMe(" ") - } + } + + missingSpacingBefore -> { + emit(node.startOffset, "Missing spacing before \":\"", true) + if (autoCorrect) { + node.upsertWhitespaceBeforeMe(" ") + } + } + + missingSpacingAfter -> { + emit(node.startOffset + 1, "Missing spacing after \":\"", true) + if (autoCorrect) { + node.upsertWhitespaceAfterMe(" ") } } } } - private inline val ASTNode.removeSpacingBefore: Boolean + private inline val ASTNode.spacingBefore: Boolean get() = - psi - .parent - .let { psiParent -> - psiParent !is KtClassOrObject && - psiParent !is KtConstructor<*> && // constructor : this/super - psiParent !is KtTypeConstraint && // where T : S - psiParent?.parent !is KtTypeParameterList + when { + psi.parent is KtClassOrObject -> true + + psi.parent is KtConstructor<*> -> { + // constructor : this/super + true + } + + psi.parent is KtTypeConstraint -> { + // where T : S + true } + + psi.parent.parent is KtTypeParameterList -> + true + + else -> false + } + + private inline val ASTNode.noSpacingBefore: Boolean + get() = !spacingBefore + + private inline val ASTNode.spacingAfter: Boolean + get() = + when (psi.parent) { + is KtAnnotation, is KtAnnotationEntry -> true + else -> false + } + + private inline val ASTNode.noSpacingAfter: Boolean + get() = !spacingAfter } public val SPACING_AROUND_COLON_RULE_ID: RuleId = SpacingAroundColonRule().ruleId diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundColonRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundColonRuleTest.kt index af7413fd58..7fde51e24f 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundColonRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundColonRuleTest.kt @@ -92,14 +92,46 @@ class SpacingAroundColonRuleTest { val code = """ @file:JvmName("Foo") + @file : JvmName("Foo") class Example(@field:Ann val foo: String, @get:Ann val bar: String) + class Example(@field : Ann val foo: String, @get : Ann val bar: String) class Example { @set:[Inject VisibleForTesting] public var collaborator: Collaborator + @set : [Inject VisibleForTesting] + public var collaborator: Collaborator } fun @receiver:Fancy String.myExtension() { } + fun @receiver : Fancy String.myExtension() { } """.trimIndent() - spacingAroundColonRuleAssertThat(code).hasNoLintViolations() + val formattedCode = + """ + @file:JvmName("Foo") + @file:JvmName("Foo") + class Example(@field:Ann val foo: String, @get:Ann val bar: String) + class Example(@field:Ann val foo: String, @get:Ann val bar: String) + class Example { + @set:[Inject VisibleForTesting] + public var collaborator: Collaborator + @set:[Inject VisibleForTesting] + public var collaborator: Collaborator + } + fun @receiver:Fancy String.myExtension() { } + fun @receiver:Fancy String.myExtension() { } + """.trimIndent() + spacingAroundColonRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 7, "Unexpected spacing before \":\""), + LintViolation(2, 7, "Unexpected spacing after \":\""), + LintViolation(4, 22, "Unexpected spacing before \":\""), + LintViolation(4, 22, "Unexpected spacing after \":\""), + LintViolation(4, 50, "Unexpected spacing before \":\""), + LintViolation(4, 50, "Unexpected spacing after \":\""), + LintViolation(8, 10, "Unexpected spacing before \":\""), + LintViolation(8, 10, "Unexpected spacing after \":\""), + LintViolation(12, 15, "Unexpected spacing before \":\""), + LintViolation(12, 15, "Unexpected spacing after \":\""), + ).isFormattedAs(formattedCode) } @Test