Skip to content

Commit

Permalink
Add rule to check spacing around square brackets 'square-brackets-spa…
Browse files Browse the repository at this point in the history
…cing' (#2555)

Closes #2543
  • Loading branch information
paul-dingemans authored Feb 18, 2024
1 parent 8eb0a58 commit 114a776
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 0 deletions.
34 changes: 34 additions & 0 deletions documentation/snapshot/docs/rules/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -648,3 +648,37 @@ Braces required for multiline for, while, and do statements.
```

Rule id: `multiline-loop` (`standard` rule set)

## Square brackets spacing

Check for spacing around square brackets.

=== "[:material-heart:](#) Ktlint"

```kotlin
val foo1 = bar[1]
val foo2 =
bar[
1,
2,
]

@Foo(
fooBar = ["foo", "bar"],
fooBaz = [
"foo",
"baz",
],
)
fun foo() {}
```

=== "[:material-heart-off-outline:](#) Disallowed"

```kotlin
val foo1 = bar [1]
val foo2 = bar[ 1]
val foo3 = bar[1 ]
```

Rule id: `square-brackets-spacing` (`standard` rule set)
9 changes: 9 additions & 0 deletions ktlint-ruleset-standard/api/ktlint-ruleset-standard.api
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,15 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundRang
public static final fun getSPACING_AROUND_RANGE_OPERATOR_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;
}

public final class com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundSquareBracketsRule : com/pinterest/ktlint/ruleset/standard/StandardRule {
public fun <init> ()V
public fun beforeVisitChildNodes (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;ZLkotlin/jvm/functions/Function3;)V
}

public final class com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundSquareBracketsRuleKt {
public static final fun getSPACING_AROUND_SQUARE_BRACKETS_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;
}

public final class com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundUnaryOperatorRule : com/pinterest/ktlint/ruleset/standard/StandardRule {
public fun <init> ()V
public fun beforeVisitChildNodes (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;ZLkotlin/jvm/functions/Function3;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import com.pinterest.ktlint.ruleset.standard.rules.SpacingAroundKeywordRule
import com.pinterest.ktlint.ruleset.standard.rules.SpacingAroundOperatorsRule
import com.pinterest.ktlint.ruleset.standard.rules.SpacingAroundParensRule
import com.pinterest.ktlint.ruleset.standard.rules.SpacingAroundRangeOperatorRule
import com.pinterest.ktlint.ruleset.standard.rules.SpacingAroundSquareBracketsRule
import com.pinterest.ktlint.ruleset.standard.rules.SpacingAroundUnaryOperatorRule
import com.pinterest.ktlint.ruleset.standard.rules.SpacingBetweenDeclarationsWithAnnotationsRule
import com.pinterest.ktlint.ruleset.standard.rules.SpacingBetweenDeclarationsWithCommentsRule
Expand Down Expand Up @@ -179,6 +180,7 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) {
RuleProvider { SpacingAroundOperatorsRule() },
RuleProvider { SpacingAroundParensRule() },
RuleProvider { SpacingAroundRangeOperatorRule() },
RuleProvider { SpacingAroundSquareBracketsRule() },
RuleProvider { SpacingAroundUnaryOperatorRule() },
RuleProvider { SpacingBetweenDeclarationsWithAnnotationsRule() },
RuleProvider { SpacingBetweenDeclarationsWithCommentsRule() },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.pinterest.ktlint.ruleset.standard.rules

import com.pinterest.ktlint.rule.engine.core.api.ElementType.COLLECTION_LITERAL_EXPRESSION
import com.pinterest.ktlint.rule.engine.core.api.ElementType.KDOC_MARKDOWN_LINK
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LBRACKET
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACKET
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint
import com.pinterest.ktlint.rule.engine.core.api.SinceKtlint.Status.EXPERIMENTAL
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.prevLeaf
import com.pinterest.ktlint.ruleset.standard.StandardRule
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

/**
* Ensures there are no extra spaces around square brackets.
*
* See https://kotlinlang.org/docs/reference/coding-conventions.html#horizontal-whitespace
*/
@SinceKtlint("1.2", EXPERIMENTAL)
public class SpacingAroundSquareBracketsRule : StandardRule("square-brackets-spacing") {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
if (node.elementType == LBRACKET || node.elementType == RBRACKET) {
val prevLeaf = node.prevLeaf()
val nextLeaf = node.nextLeaf()
val spacingBefore =
when (node.treeParent.elementType) {
KDOC_MARKDOWN_LINK -> {
// Allow:
// /**
// * @see [Foo] for more information,
// */
// fun foo() {}
false
}
COLLECTION_LITERAL_EXPRESSION -> {
// Allow:
// @Foo(
// fooBar = ["foo", "bar"],
// fooBaz = [
// "foo"
// ]
// Disallow:
// @Foo(fooBar = ["foo", "bar" ])
node.elementType == RBRACKET && prevLeaf.isWhiteSpaceWithoutNewline()
}
else -> {
prevLeaf.isWhiteSpaceWithoutNewline()
}
}
val spacingAfter =
// Allow:
// val foo = bar[
// 1,
// baz
// ]
// and
// @Foo(
// fooBar = ["foo", "bar"],
// fooBaz = [
// "foo"
// ]
// Disallow:
// @Foo(fooBar = [ "foo", "bar"])
node.elementType == LBRACKET && nextLeaf.isWhiteSpaceWithoutNewline()
when {
spacingBefore && spacingAfter -> {
emit(node.startOffset, "Unexpected spacing around '${node.text}'", true)
if (autoCorrect) {
prevLeaf!!.treeParent.removeChild(prevLeaf)
nextLeaf!!.treeParent.removeChild(nextLeaf)
}
}
spacingBefore -> {
emit(prevLeaf!!.startOffset, "Unexpected spacing before '${node.text}'", true)
if (autoCorrect) {
prevLeaf.treeParent.removeChild(prevLeaf)
}
}
spacingAfter -> {
emit(node.startOffset + 1, "Unexpected spacing after '${node.text}'", true)
if (autoCorrect) {
nextLeaf!!.treeParent.removeChild(nextLeaf)
}
}
}
}
}
}

public val SPACING_AROUND_SQUARE_BRACKETS_RULE_ID: RuleId = SpacingAroundSquareBracketsRule().ruleId
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.pinterest.ktlint.ruleset.standard.rules

import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
import org.junit.jupiter.api.Test

class SpacingAroundSquareBracketsRuleTest {
private val spacingAroundSquareBracketsRuleAssertThat = assertThatRule { SpacingAroundSquareBracketsRule() }

@Test
fun `Given array access expression`() {
val code =
"""
val foo = bar[1]
""".trimIndent()
spacingAroundSquareBracketsRuleAssertThat(code).hasNoLintViolations()
}

@Test
fun `Given array access expression with unexpected spacing before LBRACKET`() {
val code =
"""
val foo = bar [1]
""".trimIndent()
val formattedCode =
"""
val foo = bar[1]
""".trimIndent()
spacingAroundSquareBracketsRuleAssertThat(code)
.hasLintViolation(1, 14, "Unexpected spacing before '['")
.isFormattedAs(formattedCode)
}

@Test
fun `Given a KDoc with white space before LBRACKET then do not emit`() {
val code =
"""
/**
* @See [Foo] for more information.
*/
fun foo() {}
""".trimIndent()
spacingAroundSquareBracketsRuleAssertThat(code).hasNoLintViolations()
}

@Test
fun `Given array access expression with unexpected spacing after LBRACKET`() {
val code =
"""
val foo = bar[ 1]
""".trimIndent()
val formattedCode =
"""
val foo = bar[1]
""".trimIndent()
spacingAroundSquareBracketsRuleAssertThat(code)
.hasLintViolation(1, 15, "Unexpected spacing after '['")
.isFormattedAs(formattedCode)
}

@Test
fun `Given array access expression with unexpected spacing around LBRACKET`() {
val code =
"""
val foo = bar [ 1]
""".trimIndent()
val formattedCode =
"""
val foo = bar[1]
""".trimIndent()
spacingAroundSquareBracketsRuleAssertThat(code)
.hasLintViolation(1, 15, "Unexpected spacing around '['")
.isFormattedAs(formattedCode)
}

@Test
fun `Given array access expression with unexpected spacing before RBRACKET`() {
val code =
"""
val foo = bar[1 ]
""".trimIndent()
val formattedCode =
"""
val foo = bar[1]
""".trimIndent()
spacingAroundSquareBracketsRuleAssertThat(code)
.hasLintViolation(1, 16, "Unexpected spacing before ']'")
.isFormattedAs(formattedCode)
}

@Test
fun `Given a multiline array access expression then do not emit`() {
val code =
"""
val foo = bar[
1,
baz
]
""".trimIndent()
spacingAroundSquareBracketsRuleAssertThat(code).hasNoLintViolations()
}

@Test
fun `Given array access expression with whitespace after RBRACKET then do not emit`() {
val code =
"""
val foo1 = bar[1]
val foo2 = bar[1] + bar[2]
val foo3 = bar[1].count()
""".trimIndent()
spacingAroundSquareBracketsRuleAssertThat(code).hasNoLintViolations()
}

@Test
fun `Given an annotation with a collection literal expression with whitespaces before around LBRACKET and RBRACKET then do not emit`() {
val code =
"""
@Foo(
fooBar = ["foo", "bar"],
fooBaz = [
"foo",
"baz",
],
)
fun foo() {}
""".trimIndent()
spacingAroundSquareBracketsRuleAssertThat(code).hasNoLintViolations()
}
}

0 comments on commit 114a776

Please sign in to comment.