Skip to content

Commit

Permalink
Add multiline-loop to complement multiline-if-else
Browse files Browse the repository at this point in the history
  • Loading branch information
hanggrian committed Oct 9, 2023
1 parent 8f952f6 commit 33686d6
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 0 deletions.
10 changes: 10 additions & 0 deletions ktlint-ruleset-standard/api/ktlint-ruleset-standard.api
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,16 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/MultiLineIfElseRu
public static final fun getMULTI_LINE_IF_ELSE_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;
}

public final class com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRule : com/pinterest/ktlint/ruleset/standard/StandardRule {
public fun <init> ()V
public fun beforeFirstNode (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig;)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/MultiLineLoopRuleKt {
public static final fun getMULTI_LINE_LOOP_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId;
}

public final class com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressionWrappingRule : com/pinterest/ktlint/ruleset/standard/StandardRule, com/pinterest/ktlint/rule/engine/core/api/Rule$OfficialCodeStyle {
public fun <init> ()V
public fun beforeFirstNode (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.pinterest.ktlint.ruleset.standard.rules.MaxLineLengthRule
import com.pinterest.ktlint.ruleset.standard.rules.ModifierListSpacingRule
import com.pinterest.ktlint.ruleset.standard.rules.ModifierOrderRule
import com.pinterest.ktlint.ruleset.standard.rules.MultiLineIfElseRule
import com.pinterest.ktlint.ruleset.standard.rules.MultiLineLoopRule
import com.pinterest.ktlint.ruleset.standard.rules.MultilineExpressionWrappingRule
import com.pinterest.ktlint.ruleset.standard.rules.NoBlankLineBeforeRbraceRule
import com.pinterest.ktlint.ruleset.standard.rules.NoBlankLineInListRule
Expand Down Expand Up @@ -129,6 +130,7 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) {
RuleProvider { ModifierListSpacingRule() },
RuleProvider { ModifierOrderRule() },
RuleProvider { MultiLineIfElseRule() },
RuleProvider { MultiLineLoopRule() },
RuleProvider { MultilineExpressionWrappingRule() },
RuleProvider { NoBlankLineBeforeRbraceRule() },
RuleProvider { NoBlankLineInListRule() },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.pinterest.ktlint.ruleset.standard.rules

import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK
import com.pinterest.ktlint.rule.engine.core.api.ElementType.BODY
import com.pinterest.ktlint.rule.engine.core.api.ElementType.DO_KEYWORD
import com.pinterest.ktlint.rule.engine.core.api.ElementType.LBRACE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RBRACE
import com.pinterest.ktlint.rule.engine.core.api.ElementType.RPAR
import com.pinterest.ktlint.rule.engine.core.api.IndentConfig
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.editorconfig.EditorConfig
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_SIZE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.editorconfig.INDENT_STYLE_PROPERTY
import com.pinterest.ktlint.rule.engine.core.api.indent
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.isWhiteSpaceWithoutNewline
import com.pinterest.ktlint.rule.engine.core.api.nextSibling
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.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.psiUtil.leaves

/**
* https://developer.android.com/kotlin/style-guide#braces
*/
@SinceKtlint("1.0", EXPERIMENTAL)
public class MultiLineLoopRule :
StandardRule(
id = "multiline-loop",
usesEditorConfigProperties =
setOf(
INDENT_SIZE_PROPERTY,
INDENT_STYLE_PROPERTY,
),
) {
private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG

override fun beforeFirstNode(editorConfig: EditorConfig) {
indentConfig =
IndentConfig(
indentStyle = editorConfig[INDENT_STYLE_PROPERTY],
tabWidth = editorConfig[INDENT_SIZE_PROPERTY],
)
}

override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
) {
if (node.elementType != BODY) {
return
}

// Ignore when already wrapped in a block
if (node.firstChildNode?.elementType == BLOCK) {
return
}

if (!node.treePrev.textContains('\n')) {
if (!node.treeParent.textContains('\n')) {
// Allow single line loop statements as long as they are really simple (e.g. do not contain newlines)
// for (...) <statement>
// while (...) <statement>
// do <statement> while (...)
return
}

Unit
}

emit(node.firstChildNode.startOffset, "Missing { ... }", true)
if (autoCorrect) {
autocorrect(node)
}
}

private fun autocorrect(node: ASTNode) {
val prevLeaves =
node
.leaves(forward = false)
.takeWhile { it.elementType !in listOf(RPAR, DO_KEYWORD) }
.toList()
.reversed()
val nextLeaves =
node
.leaves(forward = true)
.takeWhile { it.isWhiteSpaceWithoutNewline() || it.isPartOfComment() }
.toList()
.dropLastWhile { it.isWhiteSpaceWithoutNewline() }

prevLeaves
.firstOrNull()
.takeIf { it.isWhiteSpace() }
?.let {
(it as LeafPsiElement).rawReplaceWithText(" ")
}
KtBlockExpression(null).apply {
val previousChild = node.firstChildNode
node.replaceChild(node.firstChildNode, this)
addChild(LeafPsiElement(LBRACE, "{"))
addChild(PsiWhiteSpaceImpl(indentConfig.childIndentOf(node)))
prevLeaves
.dropWhile { it.isWhiteSpace() }
.forEach(::addChild)
addChild(previousChild)
nextLeaves.forEach(::addChild)
addChild(PsiWhiteSpaceImpl(node.indent()))
addChild(LeafPsiElement(RBRACE, "}"))
}

// Make sure while starts on same line as newly inserted right brace
if (node.elementType == BODY) {
node
.nextSibling { !it.isPartOfComment() }
?.upsertWhitespaceBeforeMe(" ")
}
}
}

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

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

class MultiLineLoopRuleTest {
private val multiLineLoopRuleAssertThat = assertThatRule { MultiLineLoopRule() }

@Test
fun `Given loop statements with curly braces on single line`() {
val code =
"""
fun foo() {
for (c in "Hello World") { return 0 }
while (true) { return 0 }
do { return 0 } while (true)
}
""".trimIndent()
multiLineLoopRuleAssertThat(code).hasNoLintViolations()
}

@Test
fun `Given loop statements without curly braces on single line`() {
val code =
"""
fun foo() {
for (c in "Hello World") return 0
while (true) return 0
do return 0 while (true)
}
""".trimIndent()
multiLineLoopRuleAssertThat(code).hasNoLintViolations()
}

@Test
fun `Given multiline loop statements with curly braces`() {
val code =
"""
fun foo() {
for (c in "Hello World") {
return 0
}
while (true) {
return 0
}
do {
return 0
} while (true)
}
""".trimIndent()
multiLineLoopRuleAssertThat(code).hasNoLintViolations()
}

@Test
fun `Given multiline loop statements without curly braces`() {
val code =
"""
fun foo() {
for (c in "Hello World")
return 0
while (true)
return 0
do
return 0
while (true)
}
""".trimIndent()
val formattedCode =
"""
fun foo() {
for (c in "Hello World") {
return 0
}
while (true) {
return 0
}
do {
return 0
} while (true)
}
""".trimIndent()
multiLineLoopRuleAssertThat(code)
.hasLintViolations(
LintViolation(3, 9, "Missing { ... }"),
LintViolation(5, 9, "Missing { ... }"),
LintViolation(7, 9, "Missing { ... }"),
)
.isFormattedAs(formattedCode)
}

@Test
fun `Given deep nested loop statements without curly braces`() {
val code =
"""
fun main() {
for (c in "Hello World")
while (true)
do
return 0
while (true)
}
""".trimIndent()
val formattedCode =
"""
fun main() {
for (c in "Hello World") {
while (true) {
do {
return 0
} while (true)
}
}
}
""".trimIndent()
multiLineLoopRuleAssertThat(code)
.hasLintViolations(
LintViolation(3, 9, "Missing { ... }"),
LintViolation(4, 13, "Missing { ... }"),
LintViolation(5, 17, "Missing { ... }"),
).isFormattedAs(formattedCode)
}

@Test
fun `Given loop statements inside a lambda`() {
val code =
"""
fun test(s: String?): Int {
val i = s.let {
for (c in s)
1
while (true)
2
do
3
while (true)
} ?: 0
return i
}
""".trimIndent()
val formattedCode =
"""
fun test(s: String?): Int {
val i = s.let {
for (c in s) {
1
}
while (true) {
2
}
do {
3
} while (true)
} ?: 0
return i
}
""".trimIndent()
multiLineLoopRuleAssertThat(code)
.hasLintViolations(
LintViolation(4, 13, "Missing { ... }"),
LintViolation(6, 13, "Missing { ... }"),
LintViolation(8, 13, "Missing { ... }"),
).isFormattedAs(formattedCode)
}

@Test
fun `Given a do-while-statement with do keyword on same line as main statement`() {
val code =
"""
fun foo() {
do return 0
while (true)
}
""".trimIndent()
val formattedCode =
"""
fun foo() {
do {
return 0
} while (true)
}
""".trimIndent()
multiLineLoopRuleAssertThat(code)
.hasLintViolations(
LintViolation(2, 8, "Missing { ... }"),
).isFormattedAs(formattedCode)
}

@Test
fun `Given loop statements with multiline statement starting on same line as loop`() {
val code =
"""
fun foo() {
for (c in "Hello World") 25
.toString()
while (true) 50
.toString()
do 75
.toString()
while (true)
}
""".trimIndent()
val formattedCode =
"""
fun foo() {
for (c in "Hello World") {
25
.toString()
}
while (true) {
50
.toString()
}
do {
75
.toString()
} while (true)
}
""".trimIndent()
multiLineLoopRuleAssertThat(code)
.addAdditionalRuleProvider { IndentationRule() }
.hasLintViolations(
LintViolation(2, 30, "Missing { ... }"),
LintViolation(4, 18, "Missing { ... }"),
LintViolation(6, 8, "Missing { ... }"),
)
.isFormattedAs(formattedCode)
}
}

0 comments on commit 33686d6

Please sign in to comment.