Skip to content

Commit

Permalink
Fix spacing around colon in annotations (#2126)
Browse files Browse the repository at this point in the history
Closes #2093
  • Loading branch information
paul-dingemans authored Jul 15, 2023
1 parent ce93a6d commit 7d8a509
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 117 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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<KtBlockExpression>()
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<KtBlockExpression>()
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7d8a509

Please sign in to comment.