Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix spacing around colon in annotations #2126

Merged
merged 2 commits into from
Jul 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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