From 33686d66e26176658f159a7036e63847cb11f375 Mon Sep 17 00:00:00 2001 From: Hendra Anggrian Date: Mon, 9 Oct 2023 17:57:42 -0500 Subject: [PATCH 1/3] Add multiline-loop to complement multiline-if-else --- .../api/ktlint-ruleset-standard.api | 10 + .../standard/StandardRuleSetProvider.kt | 2 + .../standard/rules/MultiLineLoopRule.kt | 127 ++++++++++ .../standard/rules/MultiLineLoopRuleTest.kt | 229 ++++++++++++++++++ 4 files changed, 368 insertions(+) create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRule.kt create mode 100644 ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt diff --git a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api index e417dad28f..ccdf54463b 100644 --- a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api +++ b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api @@ -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 ()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 ()V public fun beforeFirstNode (Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfig;)V diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 30f8ef18de..4bba6ade83 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -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 @@ -129,6 +130,7 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) { RuleProvider { ModifierListSpacingRule() }, RuleProvider { ModifierOrderRule() }, RuleProvider { MultiLineIfElseRule() }, + RuleProvider { MultiLineLoopRule() }, RuleProvider { MultilineExpressionWrappingRule() }, RuleProvider { NoBlankLineBeforeRbraceRule() }, RuleProvider { NoBlankLineInListRule() }, diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRule.kt new file mode 100644 index 0000000000..ccab7feaa6 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRule.kt @@ -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 (...) + // while (...) + // do 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 diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt new file mode 100644 index 0000000000..749ce2bb93 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt @@ -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) + } +} From 7d3a2de2caf16f5a4e4cb166f5a486a5a273106c Mon Sep 17 00:00:00 2001 From: Hendra Anggrian Date: Wed, 11 Oct 2023 14:55:04 -0500 Subject: [PATCH 2/3] Add multiline-loop changelog and documentation (#2298) --- CHANGELOG.md | 4 +++- .../snapshot/docs/rules/experimental.md | 21 ++++++++++++++++ .../standard/rules/MultiLineLoopRuleTest.kt | 24 +++++++++---------- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26597d8495..00b5d888ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,7 +105,9 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Add new experimental rule `function-type-modifier-spacing` rule - [#2216](https://github.com/pinterest/ktlint/pull/2216), by @t-kameyama -* Define `EditorConfigOverride` for dynamically loaded ruleset - [#2194](https://github.com/pinterest/ktlint/pull/2194), by @paul-dingemans +* Add new experimental rule `multiline-loop` rule - [#2298](https://github.com/pinterest/ktlint/pull/2298), by @hendraanggrian + +* Define `EditorConfigOverride` for dynamically loaded ruleset - [#2194](https://github.com/pinterest/ktlint/pull/2194), by @paul-dingemans The `EditorConfigOverride` parameter of the `KtlintRuleEngine` can be defined using the factory method `EditorConfigOverride.from(vararg properties: Pair, *>)`. This requires the `EditorConfigProperty`'s to be available at compile time. Some common `EditorConfigProperty`'s are defined in `ktlint-rule-engine-core` which is loaded as transitive dependency of `ktlint-rule-engine` and as of that are available at compile. If an `EditorConfigProperty` is defined in a `Rule` that is only provided via a runtime dependency, it gets a bit more complicated. The `ktlint-api-consumer` example has now been updated to show how the `EditorConfigProperty` can be retrieved from the `Rule`. diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md index 6cd18756fe..9e552f2103 100644 --- a/documentation/snapshot/docs/rules/experimental.md +++ b/documentation/snapshot/docs/rules/experimental.md @@ -480,3 +480,24 @@ Enforce a single whitespace between the modifier list and the function type. ``` Rule id: `function-type-modifier-spacing` (`standard` rule set) + +## Multiline loop + +Braces required for multiline for, while, and do statements. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + for (i in 1..10) { + println(i) + } + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + for (i in 1..10) + println(i) + ``` + +Rule id: `multiline-loop` (`standard` rule set) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt index 749ce2bb93..fa81d26440 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt @@ -12,7 +12,7 @@ class MultiLineLoopRuleTest { val code = """ fun foo() { - for (c in "Hello World") { return 0 } + for (i in 1..10) { return 0 } while (true) { return 0 } do { return 0 } while (true) } @@ -25,7 +25,7 @@ class MultiLineLoopRuleTest { val code = """ fun foo() { - for (c in "Hello World") return 0 + for (i in 1..10) return 0 while (true) return 0 do return 0 while (true) } @@ -38,7 +38,7 @@ class MultiLineLoopRuleTest { val code = """ fun foo() { - for (c in "Hello World") { + for (i in 1..10) { return 0 } while (true) { @@ -57,7 +57,7 @@ class MultiLineLoopRuleTest { val code = """ fun foo() { - for (c in "Hello World") + for (i in 1..10) return 0 while (true) return 0 @@ -69,7 +69,7 @@ class MultiLineLoopRuleTest { val formattedCode = """ fun foo() { - for (c in "Hello World") { + for (i in 1..10) { return 0 } while (true) { @@ -94,7 +94,7 @@ class MultiLineLoopRuleTest { val code = """ fun main() { - for (c in "Hello World") + for (i in 1..10) while (true) do return 0 @@ -104,7 +104,7 @@ class MultiLineLoopRuleTest { val formattedCode = """ fun main() { - for (c in "Hello World") { + for (i in 1..10) { while (true) { do { return 0 @@ -127,7 +127,7 @@ class MultiLineLoopRuleTest { """ fun test(s: String?): Int { val i = s.let { - for (c in s) + for (i in 1..10) 1 while (true) 2 @@ -142,7 +142,7 @@ class MultiLineLoopRuleTest { """ fun test(s: String?): Int { val i = s.let { - for (c in s) { + for (i in 1..10) { 1 } while (true) { @@ -191,7 +191,7 @@ class MultiLineLoopRuleTest { val code = """ fun foo() { - for (c in "Hello World") 25 + for (i in 1..10) 25 .toString() while (true) 50 .toString() @@ -203,7 +203,7 @@ class MultiLineLoopRuleTest { val formattedCode = """ fun foo() { - for (c in "Hello World") { + for (i in 1..10) { 25 .toString() } @@ -220,7 +220,7 @@ class MultiLineLoopRuleTest { multiLineLoopRuleAssertThat(code) .addAdditionalRuleProvider { IndentationRule() } .hasLintViolations( - LintViolation(2, 30, "Missing { ... }"), + LintViolation(2, 22, "Missing { ... }"), LintViolation(4, 18, "Missing { ... }"), LintViolation(6, 8, "Missing { ... }"), ) From 30556ec81ec91af969d7808853b594f4009637a8 Mon Sep 17 00:00:00 2001 From: Hendra Anggrian Date: Thu, 19 Oct 2023 02:30:10 -0500 Subject: [PATCH 3/3] Rename `multiline-loop` class --- CHANGELOG.md | 4 +- .../api/ktlint-ruleset-standard.api | 12 ++-- .../standard/StandardRuleSetProvider.kt | 4 +- ...tiLineLoopRule.kt => MultilineLoopRule.kt} | 41 ++++------- ...opRuleTest.kt => MultilineLoopRuleTest.kt} | 71 +++++++++---------- 5 files changed, 59 insertions(+), 73 deletions(-) rename ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/{MultiLineLoopRule.kt => MultilineLoopRule.kt} (85%) rename ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/{MultiLineLoopRuleTest.kt => MultilineLoopRuleTest.kt} (79%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00b5d888ad..eb074f9028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Add `.editorconfig` property `ktlint_function_naming_ignore_when_annotated_with` so that rule `function-naming` can be ignored based on annotations on that rule. See [function-naming](https://pinterest.github.io/ktlint/1.0.1/rules/standard/#function-naming). +* Add new experimental rule `multiline-loop` rule - [#2298](https://github.com/pinterest/ktlint/pull/2298), by @hendraanggrian + ### Removed ### Fixed @@ -105,8 +107,6 @@ This project adheres to [Semantic Versioning](https://semver.org/). * Add new experimental rule `function-type-modifier-spacing` rule - [#2216](https://github.com/pinterest/ktlint/pull/2216), by @t-kameyama -* Add new experimental rule `multiline-loop` rule - [#2298](https://github.com/pinterest/ktlint/pull/2298), by @hendraanggrian - * Define `EditorConfigOverride` for dynamically loaded ruleset - [#2194](https://github.com/pinterest/ktlint/pull/2194), by @paul-dingemans The `EditorConfigOverride` parameter of the `KtlintRuleEngine` can be defined using the factory method `EditorConfigOverride.from(vararg properties: Pair, *>)`. This requires the `EditorConfigProperty`'s to be available at compile time. Some common `EditorConfigProperty`'s are defined in `ktlint-rule-engine-core` which is loaded as transitive dependency of `ktlint-rule-engine` and as of that are available at compile. If an `EditorConfigProperty` is defined in a `Rule` that is only provided via a runtime dependency, it gets a bit more complicated. The `ktlint-api-consumer` example has now been updated to show how the `EditorConfigProperty` can be retrieved from the `Rule`. diff --git a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api index ccdf54463b..85d22eaada 100644 --- a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api +++ b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api @@ -397,24 +397,24 @@ 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 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 ()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/MultilineExpressionWrappingRuleKt { + public static final fun getMULTILINE_EXPRESSION_WRAPPING_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 final class com/pinterest/ktlint/ruleset/standard/rules/MultilineLoopRule : com/pinterest/ktlint/ruleset/standard/StandardRule, com/pinterest/ktlint/rule/engine/core/api/Rule$Experimental { public fun ()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/MultilineExpressionWrappingRuleKt { - public static final fun getMULTILINE_EXPRESSION_WRAPPING_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; +public final class com/pinterest/ktlint/ruleset/standard/rules/MultilineLoopRuleKt { + public static final fun getMULTILINE_LOOP_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; } public final class com/pinterest/ktlint/ruleset/standard/rules/NoBlankLineBeforeRbraceRule : com/pinterest/ktlint/ruleset/standard/StandardRule { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 4bba6ade83..2ee08d6fa8 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -39,8 +39,8 @@ 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.MultilineLoopRule import com.pinterest.ktlint.ruleset.standard.rules.NoBlankLineBeforeRbraceRule import com.pinterest.ktlint.ruleset.standard.rules.NoBlankLineInListRule import com.pinterest.ktlint.ruleset.standard.rules.NoBlankLinesInChainedMethodCallsRule @@ -130,8 +130,8 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) { RuleProvider { ModifierListSpacingRule() }, RuleProvider { ModifierOrderRule() }, RuleProvider { MultiLineIfElseRule() }, - RuleProvider { MultiLineLoopRule() }, RuleProvider { MultilineExpressionWrappingRule() }, + RuleProvider { MultilineLoopRule() }, RuleProvider { NoBlankLineBeforeRbraceRule() }, RuleProvider { NoBlankLineInListRule() }, RuleProvider { NoBlankLinesInChainedMethodCallsRule() }, diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineLoopRule.kt similarity index 85% rename from ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRule.kt rename to ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineLoopRule.kt index ccab7feaa6..ef1d87965d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineLoopRule.kt @@ -7,6 +7,7 @@ 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.Rule 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 @@ -30,7 +31,7 @@ import org.jetbrains.kotlin.psi.psiUtil.leaves * https://developer.android.com/kotlin/style-guide#braces */ @SinceKtlint("1.0", EXPERIMENTAL) -public class MultiLineLoopRule : +public class MultilineLoopRule : StandardRule( id = "multiline-loop", usesEditorConfigProperties = @@ -38,7 +39,8 @@ public class MultiLineLoopRule : INDENT_SIZE_PROPERTY, INDENT_STYLE_PROPERTY, ), - ) { + ), + Rule.Experimental { private var indentConfig = IndentConfig.DEFAULT_INDENT_CONFIG override fun beforeFirstNode(editorConfig: EditorConfig) { @@ -54,28 +56,18 @@ public class MultiLineLoopRule : 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')) { + node + .takeIf { it.elementType == BODY } + ?.takeUnless { it.firstChildNode.elementType == BLOCK } + ?.takeUnless { // Allow single line loop statements as long as they are really simple (e.g. do not contain newlines) // for (...) // while (...) // do while (...) - return - } - - Unit - } - + !it.treeParent.textContains('\n') + } ?: return emit(node.firstChildNode.startOffset, "Missing { ... }", true) + if (autoCorrect) { autocorrect(node) } @@ -115,13 +107,10 @@ public class MultiLineLoopRule : addChild(LeafPsiElement(RBRACE, "}")) } - // Make sure while starts on same line as newly inserted right brace - if (node.elementType == BODY) { - node - .nextSibling { !it.isPartOfComment() } - ?.upsertWhitespaceBeforeMe(" ") - } + node + .nextSibling { !it.isPartOfComment() } + ?.upsertWhitespaceBeforeMe(" ") } } -public val MULTI_LINE_LOOP_RULE_ID: RuleId = MultiLineLoopRule().ruleId +public val MULTILINE_LOOP_RULE_ID: RuleId = MultilineLoopRule().ruleId diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineLoopRuleTest.kt similarity index 79% rename from ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt rename to ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineLoopRuleTest.kt index fa81d26440..3bc5e3f915 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultiLineLoopRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/MultilineLoopRuleTest.kt @@ -4,20 +4,20 @@ 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() } +class MultilineLoopRuleTest { + private val multilineLoopRuleAssertThat = assertThatRule { MultilineLoopRule() } @Test fun `Given loop statements with curly braces on single line`() { val code = """ fun foo() { - for (i in 1..10) { return 0 } - while (true) { return 0 } - do { return 0 } while (true) + for (i in 1..10) { bar() } + while (true) { bar() } + do { bar() } while (true) } """.trimIndent() - multiLineLoopRuleAssertThat(code).hasNoLintViolations() + multilineLoopRuleAssertThat(code).hasNoLintViolations() } @Test @@ -25,12 +25,12 @@ class MultiLineLoopRuleTest { val code = """ fun foo() { - for (i in 1..10) return 0 - while (true) return 0 - do return 0 while (true) + for (i in 1..10) bar() + while (true) bar() + do bar() while (true) } """.trimIndent() - multiLineLoopRuleAssertThat(code).hasNoLintViolations() + multilineLoopRuleAssertThat(code).hasNoLintViolations() } @Test @@ -39,17 +39,17 @@ class MultiLineLoopRuleTest { """ fun foo() { for (i in 1..10) { - return 0 + bar() } while (true) { - return 0 + bar() } do { - return 0 + bar() } while (true) } """.trimIndent() - multiLineLoopRuleAssertThat(code).hasNoLintViolations() + multilineLoopRuleAssertThat(code).hasNoLintViolations() } @Test @@ -58,11 +58,11 @@ class MultiLineLoopRuleTest { """ fun foo() { for (i in 1..10) - return 0 + bar() while (true) - return 0 + bar() do - return 0 + bar() while (true) } """.trimIndent() @@ -70,23 +70,22 @@ class MultiLineLoopRuleTest { """ fun foo() { for (i in 1..10) { - return 0 + bar() } while (true) { - return 0 + bar() } do { - return 0 + bar() } while (true) } """.trimIndent() - multiLineLoopRuleAssertThat(code) + multilineLoopRuleAssertThat(code) .hasLintViolations( LintViolation(3, 9, "Missing { ... }"), LintViolation(5, 9, "Missing { ... }"), LintViolation(7, 9, "Missing { ... }"), - ) - .isFormattedAs(formattedCode) + ).isFormattedAs(formattedCode) } @Test @@ -97,7 +96,7 @@ class MultiLineLoopRuleTest { for (i in 1..10) while (true) do - return 0 + bar() while (true) } """.trimIndent() @@ -107,13 +106,13 @@ class MultiLineLoopRuleTest { for (i in 1..10) { while (true) { do { - return 0 + bar() } while (true) } } } """.trimIndent() - multiLineLoopRuleAssertThat(code) + multilineLoopRuleAssertThat(code) .hasLintViolations( LintViolation(3, 9, "Missing { ... }"), LintViolation(4, 13, "Missing { ... }"), @@ -155,7 +154,7 @@ class MultiLineLoopRuleTest { return i } """.trimIndent() - multiLineLoopRuleAssertThat(code) + multilineLoopRuleAssertThat(code) .hasLintViolations( LintViolation(4, 13, "Missing { ... }"), LintViolation(6, 13, "Missing { ... }"), @@ -164,11 +163,11 @@ class MultiLineLoopRuleTest { } @Test - fun `Given a do-while-statement with do keyword on same line as main statement`() { + fun `Given a do-while-statement with do keyword on same line as body statement and while keyword on separate line`() { val code = """ fun foo() { - do return 0 + do bar() while (true) } """.trimIndent() @@ -176,14 +175,13 @@ class MultiLineLoopRuleTest { """ fun foo() { do { - return 0 + bar() } while (true) } """.trimIndent() - multiLineLoopRuleAssertThat(code) - .hasLintViolations( - LintViolation(2, 8, "Missing { ... }"), - ).isFormattedAs(formattedCode) + multilineLoopRuleAssertThat(code) + .hasLintViolation(2, 8, "Missing { ... }") + .isFormattedAs(formattedCode) } @Test @@ -217,13 +215,12 @@ class MultiLineLoopRuleTest { } while (true) } """.trimIndent() - multiLineLoopRuleAssertThat(code) + multilineLoopRuleAssertThat(code) .addAdditionalRuleProvider { IndentationRule() } .hasLintViolations( LintViolation(2, 22, "Missing { ... }"), LintViolation(4, 18, "Missing { ... }"), LintViolation(6, 8, "Missing { ... }"), - ) - .isFormattedAs(formattedCode) + ).isFormattedAs(formattedCode) } }