Skip to content

Commit

Permalink
Merge pull request #2473 from pinterest/2459-operator-spacing
Browse files Browse the repository at this point in the history
Fix operator spacing
  • Loading branch information
paul-dingemans authored Jan 8, 2024
2 parents 9981e62 + 360e2c8 commit d69eb99
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.pinterest.ktlint.rule.engine.core.api.ElementType.EXCLEQ
import com.pinterest.ktlint.rule.engine.core.api.ElementType.EXCLEQEQEQ
import com.pinterest.ktlint.rule.engine.core.api.ElementType.GT
import com.pinterest.ktlint.rule.engine.core.api.ElementType.GTEQ
import com.pinterest.ktlint.rule.engine.core.api.ElementType.IDENTIFIER
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LT
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LTEQ
import com.pinterest.ktlint.rule.engine.core.api.ElementType.MINUS
Expand Down Expand Up @@ -42,31 +43,42 @@ import org.jetbrains.kotlin.psi.KtPrefixExpression

@SinceKtlint("0.1", STABLE)
public class SpacingAroundOperatorsRule : StandardRule("op-spacing") {
private val tokenSet =
TokenSet.create(
MUL, PLUS, MINUS, DIV, PERC, LT, GT, LTEQ, GTEQ, EQEQEQ, EXCLEQEQEQ, EQEQ,
EXCLEQ, ANDAND, OROR, ELVIS, EQ, MULTEQ, DIVEQ, PERCEQ, PLUSEQ, MINUSEQ, ARROW,
)

override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
if (tokenSet.contains(node.elementType) &&
node.isNotUnaryOperator() &&
isNotSpreadOperator(node) &&
isNotImport(node)
if (node.isUnaryOperator()) {
// Allow:
// val foo = -1
return
}

if (node.isSpreadOperator()) {
// Allow:
// foo(*array)
return
}

if (node.isImport()) {
// Allow:
// import *
return
}

if ((node.elementType == LT || node.elementType == GT || node.elementType == MUL) &&
node.treeParent.elementType != OPERATION_REFERENCE
) {
// Allow:
// <T> fun foo(...)
// class Foo<T> { ... }
// Foo<*>
return
}

if (node.elementType in OPERATORS ||
(node.elementType == IDENTIFIER && node.treeParent.elementType == OPERATION_REFERENCE)
) {
if ((node.elementType == LT || node.elementType == GT || node.elementType == MUL) &&
node.treeParent.elementType != OPERATION_REFERENCE
) {
// Do not format parameter types like:
// <T> fun foo(...)
// class Foo<T> { ... }
// Foo<*>
return
}
val spacingBefore = node.prevLeaf() is PsiWhiteSpace
val spacingAfter = node.nextLeaf() is PsiWhiteSpace
when {
Expand All @@ -84,7 +96,7 @@ public class SpacingAroundOperatorsRule : StandardRule("op-spacing") {
}
}
!spacingAfter -> {
emit(node.startOffset + 1, "Missing spacing after \"${node.text}\"", true)
emit(node.startOffset + node.textLength, "Missing spacing after \"${node.text}\"", true)
if (autoCorrect) {
node.upsertWhitespaceAfterMe(" ")
}
Expand All @@ -93,15 +105,44 @@ public class SpacingAroundOperatorsRule : StandardRule("op-spacing") {
}
}

private fun ASTNode.isNotUnaryOperator() = !isPartOf(KtPrefixExpression::class)
private fun ASTNode.isUnaryOperator() = isPartOf(KtPrefixExpression::class)

private fun isNotSpreadOperator(node: ASTNode) =
private fun ASTNode.isSpreadOperator() =
// fn(*array)
!(node.elementType == MUL && node.treeParent.elementType == VALUE_ARGUMENT)
elementType == MUL && treeParent.elementType == VALUE_ARGUMENT

private fun isNotImport(node: ASTNode) =
private fun ASTNode.isImport() =
// import *
!node.isPartOf(KtImportDirective::class)
isPartOf(KtImportDirective::class)

private companion object {
private val OPERATORS =
TokenSet.create(
ANDAND,
ARROW,
DIV,
DIVEQ,
ELVIS,
EQ,
EQEQ,
EQEQEQ,
EXCLEQ,
EXCLEQEQEQ,
GT,
GTEQ,
LT,
LTEQ,
MINUS,
MINUSEQ,
MUL,
MULTEQ,
OROR,
PERC,
PERCEQ,
PLUS,
PLUSEQ,
)
}
}

public val SPACING_AROUND_OPERATORS_RULE_ID: RuleId = SpacingAroundOperatorsRule().ruleId
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class SpacingAroundOperatorsRuleTest {
.hasLintViolations(
LintViolation(1, 13, "Missing spacing around \"${operator}\""),
LintViolation(2, 13, "Missing spacing before \"${operator}\""),
LintViolation(3, 15, "Missing spacing after \"${operator}\""),
LintViolation(3, 14 + operator.length, "Missing spacing after \"${operator}\""),
).isFormattedAs(formattedCode)
}

Expand Down Expand Up @@ -248,10 +248,10 @@ class SpacingAroundOperatorsRuleTest {
""".trimIndent()
spacingAroundOperatorsRuleAssertThat(code)
.hasLintViolations(
LintViolation(3, 8, "Missing spacing after \"+=\""),
LintViolation(4, 8, "Missing spacing after \"-=\""),
LintViolation(5, 8, "Missing spacing after \"/=\""),
LintViolation(6, 8, "Missing spacing after \"*=\""),
LintViolation(3, 9, "Missing spacing after \"+=\""),
LintViolation(4, 9, "Missing spacing after \"-=\""),
LintViolation(5, 9, "Missing spacing after \"/=\""),
LintViolation(6, 9, "Missing spacing after \"*=\""),
).isFormattedAs(formattedCode)
}
}
Expand Down Expand Up @@ -285,4 +285,33 @@ class SpacingAroundOperatorsRuleTest {
""".trimIndent()
spacingAroundOperatorsRuleAssertThat(code).hasNoLintViolations()
}

@Test
fun `Given a custom DSL`() {
val code =
"""
fun foo() {
every { foo() }returns(bar)andThen(baz)
every { foo() }returns (bar)andThen (baz)
every { foo() } returns(bar) andThen(baz)
}
""".trimIndent()
val formattedCode =
"""
fun foo() {
every { foo() } returns (bar) andThen (baz)
every { foo() } returns (bar) andThen (baz)
every { foo() } returns (bar) andThen (baz)
}
""".trimIndent()
spacingAroundOperatorsRuleAssertThat(code)
.hasLintViolations(
LintViolation(2, 20, "Missing spacing around \"returns\""),
LintViolation(2, 32, "Missing spacing around \"andThen\""),
LintViolation(3, 20, "Missing spacing before \"returns\""),
LintViolation(3, 33, "Missing spacing before \"andThen\""),
LintViolation(4, 28, "Missing spacing after \"returns\""),
LintViolation(4, 41, "Missing spacing after \"andThen\""),
).isFormattedAs(formattedCode)
}
}

0 comments on commit d69eb99

Please sign in to comment.