Skip to content

Commit

Permalink
Add multiline-loop to complement multiline-if-else (#2298)
Browse files Browse the repository at this point in the history
* Add multiline-loop to complement multiline-if-else
  • Loading branch information
hanggrian authored Nov 16, 2023
1 parent 2bfc5a1 commit 05aa8a0
Show file tree
Hide file tree
Showing 6 changed files with 379 additions and 1 deletion.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).

### Added

* 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

* Remove obsolete idea configuration files [#2249](https://github.com/pinterest/ktlint/issues/2249)
Expand Down Expand Up @@ -146,7 +149,7 @@ 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
* 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<EditorConfigProperty<*>, *>)`. 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`.

Expand Down
21 changes: 21 additions & 0 deletions documentation/snapshot/docs/rules/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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 @@ -407,6 +407,16 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/MultilineExpressi
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/MultilineLoopRule : com/pinterest/ktlint/ruleset/standard/StandardRule, com/pinterest/ktlint/rule/engine/core/api/Rule$Experimental {
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 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 {
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 @@ -40,6 +40,7 @@ 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.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
Expand Down Expand Up @@ -130,6 +131,7 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) {
RuleProvider { ModifierOrderRule() },
RuleProvider { MultiLineIfElseRule() },
RuleProvider { MultilineExpressionWrappingRule() },
RuleProvider { MultilineLoopRule() },
RuleProvider { NoBlankLineBeforeRbraceRule() },
RuleProvider { NoBlankLineInListRule() },
RuleProvider { NoBlankLinesInChainedMethodCallsRule() },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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.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
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,
),
),
Rule.Experimental {
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,
) {
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 (...) <statement>
// while (...) <statement>
// do <statement> while (...)
!it.treeParent.textContains('\n')
} ?: return
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, "}"))
}

node
.nextSibling { !it.isPartOfComment() }
?.upsertWhitespaceBeforeMe(" ")
}
}

public val MULTILINE_LOOP_RULE_ID: RuleId = MultilineLoopRule().ruleId
Loading

0 comments on commit 05aa8a0

Please sign in to comment.