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 operator spacing #2473

Merged
merged 1 commit into from
Jan 8, 2024
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
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)
}
}