diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md index 287503a810..00c0e49950 100644 --- a/documentation/snapshot/docs/rules/experimental.md +++ b/documentation/snapshot/docs/rules/experimental.md @@ -75,6 +75,87 @@ Wraps binary expression at the operator reference whenever the binary expression Rule id: `binary-expression-wrapping` (`standard` rule set) +## Blank lines between when-conditions + +Consistently add or remove blank lines between when-conditions in a when-statement. A blank line is only added between when-conditions if the when-statement contains at lease one multiline when-condition. If a when-statement only contains single line when-conditions, then the blank lines between the when-conditions are removed. + +!!! note + Ktlint uses `.editorconfig` property `ij_kotlin_line_break_after_multiline_when_entry` but applies it also on single line entries to increase consistency. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo1 = + when (bar) { + BAR1 -> "bar1" + BAR2 -> "bar2" + else -> null + } + + // ij_kotlin_line_break_after_multiline_when_entry = true + val foo2 = + when (bar) { + BAR1 -> "bar1" + + BAR2 -> { + "bar2" + } + + else -> null + } + + // ij_kotlin_line_break_after_multiline_when_entry = false + val foo2 = + when (bar) { + BAR1 -> "bar1" + BAR2 -> { + "bar2" + } + else -> null + } + ``` +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + // ij_kotlin_line_break_after_multiline_when_entry = true | false (no blank lines in simple when-statement) + val foo1 = + when (bar) { + BAR1 -> "bar1" + + BAR2 -> "bar2" + + else -> null + } + + // ij_kotlin_line_break_after_multiline_when_entry = true (missing newline after BAR1) + val foo2 = + when (bar) { + BAR1 -> "bar1" + BAR2 -> { + "bar2" + } + + else -> null + } + + // ij_kotlin_line_break_after_multiline_when_entry = false (unexpected newline after BAR2) + val foo2 = + when (bar) { + BAR1 -> "bar1" + BAR2 -> { + "bar2" + } + + else -> null + } + ``` + +| Configuration setting | ktlint_official | intellij_idea | android_studio | +|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------:|:-------------:|:--------------:| +| `ij_kotlin_line_break_after_multiline_when_entry`
Despite its name, forces a blank line between single line and multiline when-entries when at least one multiline when-entry is found in the when-statement. | `true` | `true` | `true` | + +Rule id: `blank-lines-between-when-conditions` (`standard` rule set) + ## Chain method continuation In a multiline method chain, the chain operators (`.` or `?.`) have to be aligned with each other. diff --git a/ktlint-cli-reporter-format/src/main/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporter.kt b/ktlint-cli-reporter-format/src/main/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporter.kt index 98400cf7d2..62e979f297 100644 --- a/ktlint-cli-reporter-format/src/main/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporter.kt +++ b/ktlint-cli-reporter-format/src/main/kotlin/com/pinterest/ktlint/cli/reporter/format/FormatReporter.kt @@ -29,6 +29,7 @@ public class FormatReporter( countAutoCorrectPossibleOrDone.putIfAbsent(file, 0) countAutoCorrectPossibleOrDone.replace(file, countAutoCorrectPossibleOrDone.getOrDefault(file, 0) + 1) } + else -> { countCanNotBeAutoCorrected.putIfAbsent(file, 0) countCanNotBeAutoCorrected.replace(file, countCanNotBeAutoCorrected.getOrDefault(file, 0) + 1) @@ -46,18 +47,21 @@ public class FormatReporter( } else { "Format required (1 violation needs manual fixing)" } + canNotBeAutocorrected > 1 -> if (format) { "Format not completed ($canNotBeAutocorrected violations need manual fixing)" } else { "Format required ($canNotBeAutocorrected violations need manual fixing)" } + countAutoCorrectPossibleOrDone.getOrDefault(file, 0) > 0 -> if (format) { "Format completed (all violations have been fixed)" } else { "Format required (all violations can be autocorrected)" } + else -> "Format not needed (no violations found)" } diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt index a480371f74..27e2a9bd67 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/FileUtils.kt @@ -160,8 +160,10 @@ private fun Path.findCommonParentDir(path: Path): Path = when { path.startsWith(this) -> this + startsWith(path) -> path + else -> this@findCommonParentDir.findCommonParentDir(path.parent) } diff --git a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt index 039e47469c..8aab056cbd 100644 --- a/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt +++ b/ktlint-cli/src/main/kotlin/com/pinterest/ktlint/cli/internal/KtlintCommandLine.kt @@ -499,6 +499,7 @@ internal class KtlintCommandLine : }.also { formattedFileContent -> when { code.isStdIn -> print(formattedFileContent) + code.content != formattedFileContent -> code .filePath @@ -616,6 +617,7 @@ internal class KtlintCommandLine : detail = "Not a valid Kotlin file (${e.message?.lowercase(Locale.getDefault())})", status = KOTLIN_PARSE_EXCEPTION, ) + is KtLintRuleException -> { logger.debug(e) { "Internal Error (${e.ruleId}) in ${code.fileNameOrStdin()} at position '${e.line}:${e.col}" } KtlintCliError( @@ -630,6 +632,7 @@ internal class KtlintCommandLine : status = KTLINT_RULE_ENGINE_EXCEPTION, ) } + else -> throw e } } diff --git a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/CommandLineTestRunner.kt b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/CommandLineTestRunner.kt index 5c3319efd6..8841bba05e 100644 --- a/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/CommandLineTestRunner.kt +++ b/ktlint-cli/src/test/kotlin/com/pinterest/ktlint/cli/CommandLineTestRunner.kt @@ -188,6 +188,7 @@ class CommandLineTestRunner( key.equals(PATH, ignoreCase = true) } ?: PATH } + else -> PATH } environment[pathKey] = "$JAVA_HOME_BIN_DIR${File.pathSeparator}${OsEnvironment()[PATH]}" diff --git a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/IndentConfig.kt b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/IndentConfig.kt index 6df4dc53d3..385d136ff9 100644 --- a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/IndentConfig.kt +++ b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/IndentConfig.kt @@ -112,6 +112,7 @@ public class IndentConfig( require(indent.matches(TABS_AND_SPACES)) return when (indentStyle) { SPACE -> indent.replaceTabWithSpaces() + TAB -> { "\t".repeat(indentLevelFrom(indent)) // Silently swallow spaces if not enough spaces present to convert to a tab diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppression.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppression.kt index 639028b2aa..8ba6536919 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppression.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/api/KtlintRuleEngineSuppression.kt @@ -73,14 +73,17 @@ private fun KtlintSuppressionAtOffset.offsetFromStartOf(code: String): Int { col == 1 && codeLine.isEmpty() -> { startOffsetOfLineContainingLintError } + col <= codeLine.length -> { startOffsetOfLineContainingLintError + (col - 1) } + col == codeLine.length + 1 -> { // Offset of suppression is set at EOL of the line. This is visually correct for the reader. But the newline character was stripped // from the line because the lines were split using that character. startOffsetOfLineContainingLintError + col } + else -> { throw KtlintSuppressionOutOfBoundsException(this) } diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/PositionInTextLocator.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/PositionInTextLocator.kt index 5f96bbf34a..87e730930e 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/PositionInTextLocator.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/PositionInTextLocator.kt @@ -59,6 +59,7 @@ private class SegmentTree( ): Int = when { l > r -> -1 + else -> { val i = l + (r - l) / 2 val s = segments[i] diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilder.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilder.kt index 715bcae262..b38fe6bc85 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilder.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/SuppressionLocatorBuilder.kt @@ -220,6 +220,7 @@ internal object SuppressionLocatorBuilder { .let { suppressedRuleIds -> when { suppressedRuleIds.isEmpty() -> null + suppressedRuleIds.contains(ALL_KTLINT_RULES_SUPPRESSION_ID) -> SuppressionHint( IntRange(startOffset, endOffset - 1), @@ -244,6 +245,7 @@ internal object SuppressionLocatorBuilder { // Disable all rules ALL_KTLINT_RULES_SUPPRESSION_ID } + argumentExpressionText.startsWith("ktlint:") -> { // Disable specific rule. For backwards compatibility prefix rules without rule set id with the "standard" rule set // id. Note that the KtlintSuppressionRule will emit a lint violation on the id. So this fix is only applicable for @@ -252,6 +254,7 @@ internal object SuppressionLocatorBuilder { .removePrefix("ktlint:") .let { RuleId.prefixWithStandardRuleSetIdWhenMissing(it) } } + else -> { // Disable specific rule if the annotation value is mapped to a specific rule annotationValueToRuleMapping[argumentExpressionText] diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleExecutionRuleFilter.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleExecutionRuleFilter.kt index 6a79288a50..f79080f500 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleExecutionRuleFilter.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RuleExecutionRuleFilter.kt @@ -96,10 +96,13 @@ private class RuleExecutionFilter( when { rule is Rule.Experimental && rule is Rule.OfficialCodeStyle -> isExperimentalEnabled(rule) && isOfficialCodeStyleEnabled(rule) + rule is Rule.Experimental -> isExperimentalEnabled(rule) + rule is Rule.OfficialCodeStyle -> isOfficialCodeStyleEnabled(rule) + else -> isRuleSetEnabled(rule) } diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintTest.kt index 920ec92ce9..1fbc943bf1 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintTest.kt @@ -506,6 +506,7 @@ private class AutoCorrectErrorRule : (node as LeafElement).rawReplaceWithText(STRING_VALUE_AFTER_AUTOCORRECT) } } + STRING_VALUE_NOT_TO_BE_CORRECTED -> emit(node.startOffset, ERROR_MESSAGE_CAN_NOT_BE_AUTOCORRECTED, false) } diff --git a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api index e858e4c22c..5c7c9c1356 100644 --- a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api +++ b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api @@ -72,6 +72,21 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/BlankLineBeforeDe public static final fun getBLANK_LINE_BEFORE_DECLARATION_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; } +public final class com/pinterest/ktlint/ruleset/standard/rules/BlankLineBetweenWhenConditions : com/pinterest/ktlint/ruleset/standard/StandardRule { + public static final field Companion Lcom/pinterest/ktlint/ruleset/standard/rules/BlankLineBetweenWhenConditions$Companion; + 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/BlankLineBetweenWhenConditions$Companion { + public final fun getLINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY ()Lcom/pinterest/ktlint/rule/engine/core/api/editorconfig/EditorConfigProperty; +} + +public final class com/pinterest/ktlint/ruleset/standard/rules/BlankLineBetweenWhenConditionsKt { + public static final fun getBLANK_LINE_BETWEEN_WHEN_CONDITIONS_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; +} + public final class com/pinterest/ktlint/ruleset/standard/rules/BlockCommentInitialStarAlignmentRule : com/pinterest/ktlint/ruleset/standard/StandardRule { public fun ()V public fun beforeVisitChildNodes (Lorg/jetbrains/kotlin/com/intellij/lang/ASTNode;ZLkotlin/jvm/functions/Function3;)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 fccfe4505a..26a6b41dfd 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 @@ -9,6 +9,7 @@ import com.pinterest.ktlint.ruleset.standard.rules.ArgumentListWrappingRule import com.pinterest.ktlint.ruleset.standard.rules.BackingPropertyNamingRule import com.pinterest.ktlint.ruleset.standard.rules.BinaryExpressionWrappingRule import com.pinterest.ktlint.ruleset.standard.rules.BlankLineBeforeDeclarationRule +import com.pinterest.ktlint.ruleset.standard.rules.BlankLineBetweenWhenConditions import com.pinterest.ktlint.ruleset.standard.rules.BlockCommentInitialStarAlignmentRule import com.pinterest.ktlint.ruleset.standard.rules.ChainMethodContinuationRule import com.pinterest.ktlint.ruleset.standard.rules.ChainWrappingRule @@ -109,6 +110,7 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) { RuleProvider { BackingPropertyNamingRule() }, RuleProvider { BinaryExpressionWrappingRule() }, RuleProvider { BlankLineBeforeDeclarationRule() }, + RuleProvider { BlankLineBetweenWhenConditions() }, RuleProvider { BlockCommentInitialStarAlignmentRule() }, RuleProvider { ChainMethodContinuationRule() }, RuleProvider { ChainWrappingRule() }, diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule.kt index 6358ba657e..2bdabde834 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/AnnotationRule.kt @@ -118,17 +118,21 @@ public class AnnotationRule : visitAnnotationList(node, emit, autoCorrect) visitFileAnnotationList(node, emit, autoCorrect) } + ANNOTATED_EXPRESSION, MODIFIER_LIST -> { visitAnnotationList(node, emit, autoCorrect) } + ANNOTATION -> { // Annotation array // @[...] visitAnnotation(node, emit, autoCorrect) } + ANNOTATION_ENTRY -> { visitAnnotationEntry(node, emit, autoCorrect) } + TYPE_ARGUMENT_LIST -> { visitTypeArgumentList(node, emit, autoCorrect) } @@ -147,8 +151,10 @@ public class AnnotationRule : when { node.elementType == ANNOTATED_EXPRESSION -> indentConfig.siblingIndentOf(node) + node.hasAnnotationBeforeConstructor() -> indentConfig.siblingIndentOf(node.treeParent) + else -> indentConfig.parentIndentOf(node) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt index 4b0db9ca09..23e04dc4ac 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ArgumentListWrappingRule.kt @@ -147,7 +147,6 @@ public class ArgumentListWrappingRule : // 2 // ) child.treeParent.hasTypeArgumentListInFront() -> -1 - // IDEA quirk: // foo // .bar = Baz( @@ -161,7 +160,6 @@ public class ArgumentListWrappingRule : // 2 // ) child.treeParent.isPartOfDotQualifiedAssignmentExpression() -> -1 - else -> 0 }.let { if (child.treeParent.isOnSameLineAsControlFlowKeyword()) { @@ -198,6 +196,7 @@ public class ArgumentListWrappingRule : } } } + VALUE_ARGUMENT, RPAR, -> { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBetweenWhenConditions.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBetweenWhenConditions.kt new file mode 100644 index 0000000000..5978a9d6f1 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBetweenWhenConditions.kt @@ -0,0 +1,149 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHEN_ENTRY +import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHITE_SPACE +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.children +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfig +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.EditorConfigProperty +import com.pinterest.ktlint.rule.engine.core.api.indent +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace +import com.pinterest.ktlint.rule.engine.core.api.lastChildLeafOrSelf +import com.pinterest.ktlint.rule.engine.core.api.nextLeaf +import com.pinterest.ktlint.rule.engine.core.api.prevCodeSibling +import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe +import com.pinterest.ktlint.ruleset.standard.StandardRule +import org.ec4j.core.model.PropertyType +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +/** + * The Kotlin Coding Conventions suggest to consider using a blank line after a multiline when-condition + * (https://kotlinlang.org/docs/coding-conventions.html#control-flow-statements) which behavior is managed via '.editorconfig' property + * `ij_kotlin_line_break_after_multiline_when_entry`. + * + * Ktlint uses the property `ij_kotlin_line_break_after_multiline_when_entry` to consistently add/remove blank line between all + * when-conditions in the when-statement depending on whether the statement contains at least one multiline when-condition. + */ +@SinceKtlint("1.2.0", EXPERIMENTAL) +public class BlankLineBetweenWhenConditions : + StandardRule( + id = "blank-line-between-when-conditions", + usesEditorConfigProperties = setOf(LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY), + ) { + private var lineBreakAfterWhenCondition = LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY.defaultValue + + override fun beforeFirstNode(editorConfig: EditorConfig) { + lineBreakAfterWhenCondition = editorConfig[LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY] + } + + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + if (node.elementType == ElementType.WHEN) { + visitWhenStatement(node, autoCorrect, emit) + } + } + + private fun visitWhenStatement( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + val hasMultilineWhenCondition = node.hasAnyMultilineWhenCondition() + if (hasMultilineWhenCondition && lineBreakAfterWhenCondition) { + addBlankLinesBetweenWhenConditions(node, autoCorrect, emit) + } else { + removeBlankLinesBetweenWhenConditions(node, autoCorrect, emit) + } + } + + private fun addBlankLinesBetweenWhenConditions( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .children() + .filter { it.elementType == WHEN_ENTRY } + // Blank lines should only be added *between* when-conditions, so first when-condition is to be skipped + .drop(1) + .forEach { whenEntry -> + whenEntry + .findWhitespaceAfterPreviousCodeSibling() + ?.takeUnless { it.containsBlankLine() } + ?.let { whitespaceBeforeWhenEntry -> + emit( + whitespaceBeforeWhenEntry.startOffset + 1, + "Add a blank line between all when-condition in case at least one multiline when-condition is found in the " + + "statement", + true, + ) + if (autoCorrect) { + whitespaceBeforeWhenEntry.upsertWhitespaceBeforeMe("\n${whenEntry.indent()}") + } + } + } + } + + private fun ASTNode.containsBlankLine(): Boolean = elementType == WHITE_SPACE && text.count { it == '\n' } > 1 + + private fun ASTNode.hasAnyMultilineWhenCondition() = children().any { it.elementType == WHEN_ENTRY && it.textContains('\n') } + + private fun ASTNode.findWhitespaceAfterPreviousCodeSibling() = + prevCodeSibling() + ?.lastChildLeafOrSelf() + ?.nextLeaf { it.isWhiteSpace() } + + private fun removeBlankLinesBetweenWhenConditions( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + node + .children() + .filter { it.elementType == WHEN_ENTRY } + // Blank lines should only be removed *between* when-conditions, so first when-condition is to be skipped + .drop(1) + .forEach { whenEntry -> + whenEntry + .findWhitespaceAfterPreviousCodeSibling() + ?.takeIf { it.containsBlankLine() } + ?.let { whitespaceBeforeWhenEntry -> + emit( + whitespaceBeforeWhenEntry.startOffset + 1, + "Unexpected blank lines between when-condition if all when-conditions are single lines", + true, + ) + if (autoCorrect) { + whitespaceBeforeWhenEntry.upsertWhitespaceBeforeMe("\n${whenEntry.indent(includeNewline = false)}") + } + } + } + } + + public companion object { + private val BOOLEAN_VALUES_SET = setOf("true", "false") + + public val LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY: EditorConfigProperty = + EditorConfigProperty( + type = + PropertyType.LowerCasingPropertyType( + "ij_kotlin_line_break_after_multiline_when_entry", + "Defines whether a blank line is to be added after a when entry. Contrary to default IDEA formatting, " + + "ktlint adds the blank line between all when-conditions if the when-statement contains at least one " + + "multiline when-condition. Or, it removes all blank lines between the when-conditions if the when-statement " + + "does not contain any multiline when-condition.", + PropertyType.PropertyValueParser.BOOLEAN_VALUE_PARSER, + BOOLEAN_VALUES_SET, + ), + defaultValue = true, + ) + } +} + +public val BLANK_LINE_BETWEEN_WHEN_CONDITIONS_RULE_ID: RuleId = BlankLineBetweenWhenConditions().ruleId diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ChainWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ChainWrappingRule.kt index c3dfa8fa76..3183473bad 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ChainWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ChainWrappingRule.kt @@ -138,7 +138,9 @@ public class ChainWrappingRule : // change the indent of the next line prevLeaf } + nextLeaf.isWhiteSpaceWithoutNewline() -> nextLeaf + else -> null } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ContextReceiverWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ContextReceiverWrappingRule.kt index 9350dbc541..b08338807f 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ContextReceiverWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ContextReceiverWrappingRule.kt @@ -67,6 +67,7 @@ public class ContextReceiverWrappingRule : when { node.elementType == CONTEXT_RECEIVER_LIST -> visitContextReceiverList(node, autoCorrect, emit) + node.elementType == TYPE_ARGUMENT_LIST && node.isPartOf(CONTEXT_RECEIVER) -> visitContextReceiverTypeArgumentList(node, autoCorrect, emit) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRule.kt index 4c7c2a9524..19dbf6fe87 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionNamingRule.kt @@ -154,9 +154,11 @@ public class FunctionNamingRule : ANNOTATION -> { it.containsAnnotationEntryWithIdentifierIn(excludeWhenAnnotatedWith) } + ANNOTATION_ENTRY -> { it.annotationEntryName() in excludeWhenAnnotatedWith } + else -> false } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseBracingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseBracingRule.kt index f84024c13a..d5e8ac318d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseBracingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/IfElseBracingRule.kt @@ -104,12 +104,15 @@ public class IfElseBracingRule : this == null -> { false } + this.elementType == BLOCK -> { true } + this.elementType == IF -> { findChildByType(THEN).hasBracing() || findChildByType(ELSE).hasBracing() } + else -> { this.firstChildNode.hasBracing() } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ImportOrderingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ImportOrderingRule.kt index 666fa46e45..9c179dfe48 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ImportOrderingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ImportOrderingRule.kt @@ -222,6 +222,7 @@ public class ImportOrderingRule : value, "Import layout must contain at least one entry of a wildcard symbol (*)", ) + value == "idea" -> { LOGGER.warn { "`idea` is deprecated! Please use `*,java.**,javax.**,kotlin.**,^` instead to ensure that the Kotlin IDE " + @@ -232,6 +233,7 @@ public class ImportOrderingRule : IDEA_PATTERN, ) } + value == "ascii" -> { LOGGER.warn { "`ascii` is deprecated! Please use `*` instead to ensure that the Kotlin IDE plugin recognizes the value" @@ -241,6 +243,7 @@ public class ImportOrderingRule : ASCII_PATTERN, ) } + else -> try { PropertyType.PropertyValue.valid( diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveCommentsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveCommentsRule.kt index 0d1b1a04b4..503d04a567 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveCommentsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoConsecutiveCommentsRule.kt @@ -131,6 +131,7 @@ public class NoConsecutiveCommentsRule : KDOC_START, -> true + else -> false } @@ -142,6 +143,7 @@ public class NoConsecutiveCommentsRule : KDOC_END, -> true + else -> false } @@ -149,10 +151,13 @@ public class NoConsecutiveCommentsRule : private fun ASTNode.commentType() = when (this.elementType) { EOL_COMMENT -> "an EOL comment" + BLOCK_COMMENT -> "a block comment" + KDOC_START, KDOC_END, -> "a KDoc" + else -> this.elementType.toString().lowercase() } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoTrailingSpacesRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoTrailingSpacesRule.kt index 4dc21993e6..77886232a0 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoTrailingSpacesRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoTrailingSpacesRule.kt @@ -48,6 +48,7 @@ public class NoTrailingSpacesRule : StandardRule("no-trailing-spaces") { // Do not change the last line as it contains the indentation of the next element except // when it is an EOL comment which may also not contain trailing spaces line + line.hasTrailingSpace() -> { val modifiedLine = line.trimEnd() val firstTrailingSpaceOffset = violationOffset + modifiedLine.length @@ -55,6 +56,7 @@ public class NoTrailingSpacesRule : StandardRule("no-trailing-spaces") { violated = true modifiedLine } + else -> line } violationOffset += line.length + 1 diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoUnusedImportsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoUnusedImportsRule.kt index c53ca9738b..83ffd8135d 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoUnusedImportsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/NoUnusedImportsRule.kt @@ -58,6 +58,7 @@ public class NoUnusedImportsRule : StandardRule("no-unused-imports") { val packageDirective = node.psi as KtPackageDirective packageName = packageDirective.qualifiedName } + IMPORT_DIRECTIVE -> { val importPath = (node.psi as KtImportDirective).importPath!! if (imports.containsKey(importPath)) { @@ -70,11 +71,13 @@ public class NoUnusedImportsRule : StandardRule("no-unused-imports") { imports[importPath] = node } } + DOT_QUALIFIED_EXPRESSION -> { if (node.isExpressionForStaticImportWithExistingParentImport()) { parentExpressions.add(node.text.substringBeforeLast("(")) } } + MARKDOWN_LINK -> { node .psi @@ -85,6 +88,7 @@ public class NoUnusedImportsRule : StandardRule("no-unused-imports") { ref.add(Reference(linkText.split('.').last(), false)) } } + REFERENCE_EXPRESSION, OPERATION_REFERENCE -> { if (!node.isPartOf(IMPORT_DIRECTIVE)) { val identifier = @@ -106,6 +110,7 @@ public class NoUnusedImportsRule : StandardRule("no-unused-imports") { } } } + BY_KEYWORD -> foundByKeyword = true } } @@ -208,11 +213,13 @@ public class NoUnusedImportsRule : StandardRule("no-unused-imports") { ?.takeIf { it.isWhiteSpaceWithNewline() } ?.let { it.treeParent.removeChild(it) } } + treeParent.lastChildNode == this -> { prevSibling() ?.takeIf { it.isWhiteSpaceWithNewline() } ?.let { it.treeParent.removeChild(it) } } + else -> { nextLeaf(true) ?.takeIf { it.isWhiteSpaceWithNewline() } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRule.kt index 8ca7fcc5a2..3e0a0610dc 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListSpacingRule.kt @@ -105,6 +105,7 @@ public class ParameterListSpacingRule : replaceWithSingleSpace(el, emit, autoCorrect) } } + COMMA -> { // Comma must be followed by whitespace el @@ -112,6 +113,7 @@ public class ParameterListSpacingRule : ?.takeIf { it.elementType != WHITE_SPACE } ?.let { addMissingWhiteSpaceAfterMe(el, emit, autoCorrect) } } + VALUE_PARAMETER -> { valueParameterCount += 1 visitValueParameter(el, emit, autoCorrect) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt index dc6074217c..764debc605 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/ParameterListWrappingRule.kt @@ -132,12 +132,18 @@ public class ParameterListWrappingRule : private fun ASTNode.needToWrapParameterList() = when { hasNoParameters() -> false + codeStyle != ktlint_official && isPartOfFunctionLiteralInNonKtlintOfficialCodeStyle() -> false + codeStyle == ktlint_official && containsAnnotatedParameter() -> true + codeStyle == ktlint_official && isPartOfFunctionLiteralStartingOnSameLineAsClosingParenthesisOfPrecedingReferenceExpression() -> false + textContains('\n') -> true + isOnLineExceedingMaxLineLength() -> true + else -> false } @@ -219,7 +225,6 @@ public class ParameterListWrappingRule : // param2: R // ) child.treeParent.isFunWithTypeParameterListInFront() -> -1 - else -> 0 }.let { if (child.elementType == VALUE_PARAMETER) { @@ -252,6 +257,7 @@ public class ParameterListWrappingRule : } } } + VALUE_PARAMETER, RPAR, -> { @@ -312,9 +318,12 @@ public class ParameterListWrappingRule : private fun errorMessage(node: ASTNode) = when (node.elementType) { LPAR -> """Unnecessary newline before "("""" + VALUE_PARAMETER -> "Parameter should start on a newline" + RPAR -> """Missing newline before ")"""" + else -> throw UnsupportedOperationException() } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt index 23fdbc8aa8..2a0da216ef 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/PropertyNamingRule.kt @@ -50,9 +50,11 @@ public class PropertyNamingRule : StandardRule("property-naming") { property.hasConstModifier() -> { visitConstProperty(identifier, emit) } + property.hasCustomGetter() || property.isTopLevelValue() || property.isObjectValue() -> { // Can not reliably determine whether the value is immutable or not } + else -> { visitNonConstProperty(identifier, emit) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundCurlyRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundCurlyRule.kt index 9412ca36a1..5a9e620dc3 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundCurlyRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundCurlyRule.kt @@ -151,12 +151,14 @@ public class SpacingAroundCurlyRule : node.upsertWhitespaceAfterMe(" ") } } + !spacingBefore -> { emit(node.startOffset, "Missing spacing before \"${node.text}\"", true) if (autoCorrect) { node.upsertWhitespaceBeforeMe(" ") } } + !spacingAfter -> { emit(node.startOffset + 1, "Missing spacing after \"${node.text}\"", true) if (autoCorrect) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundDoubleColonRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundDoubleColonRule.kt index 0c339c11e4..1462c3d322 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundDoubleColonRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundDoubleColonRule.kt @@ -28,7 +28,9 @@ public class SpacingAroundDoubleColonRule : StandardRule("double-colon-spacing") var removeSingleWhiteSpace = false val spacingBefore = when { - node.isPartOf(CLASS_LITERAL_EXPRESSION) && prevLeaf is PsiWhiteSpace -> true // Clazz::class + node.isPartOf(CLASS_LITERAL_EXPRESSION) && prevLeaf is PsiWhiteSpace -> true + + // Clazz::class node.isPartOf(CALLABLE_REFERENCE_EXPRESSION) && prevLeaf is PsiWhiteSpace -> // String::length, ::isOdd if (node.treePrev == null) { // compose(length, ::isOdd), val predicate = ::isOdd removeSingleWhiteSpace = true @@ -36,6 +38,7 @@ public class SpacingAroundDoubleColonRule : StandardRule("double-colon-spacing") } else { // String::length, List::isEmpty !prevLeaf.textContains('\n') } + else -> false } val spacingAfter = nextLeaf is PsiWhiteSpace @@ -47,12 +50,14 @@ public class SpacingAroundDoubleColonRule : StandardRule("double-colon-spacing") nextLeaf!!.treeParent.removeChild(nextLeaf) } } + spacingBefore -> { emit(prevLeaf!!.startOffset, "Unexpected spacing before \"${node.text}\"", true) if (autoCorrect) { prevLeaf.removeSelf(removeSingleWhiteSpace) } } + spacingAfter -> { emit(nextLeaf!!.startOffset, "Unexpected spacing after \"${node.text}\"", true) if (autoCorrect) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundOperatorsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundOperatorsRule.kt index 87ddecddf4..cb8959633c 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundOperatorsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundOperatorsRule.kt @@ -89,12 +89,14 @@ public class SpacingAroundOperatorsRule : StandardRule("op-spacing") { node.upsertWhitespaceAfterMe(" ") } } + !spacingBefore -> { emit(node.startOffset, "Missing spacing before \"${node.text}\"", true) if (autoCorrect) { node.upsertWhitespaceBeforeMe(" ") } } + !spacingAfter -> { emit(node.startOffset + node.textLength, "Missing spacing after \"${node.text}\"", true) if (autoCorrect) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundParensRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundParensRule.kt index 194d02cc25..33cf472e18 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundParensRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundParensRule.kt @@ -71,12 +71,14 @@ public class SpacingAroundParensRule : StandardRule("paren-spacing") { nextLeaf!!.treeParent.removeChild(nextLeaf) } } + spacingBefore -> { emit(prevLeaf!!.startOffset, "Unexpected spacing before \"${node.text}\"", true) if (autoCorrect) { prevLeaf.treeParent.removeChild(prevLeaf) } } + spacingAfter -> { emit(node.startOffset + 1, "Unexpected spacing after \"${node.text}\"", true) if (autoCorrect) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundRangeOperatorRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundRangeOperatorRule.kt index bcd25ee1e4..d4293263e5 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundRangeOperatorRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundRangeOperatorRule.kt @@ -30,12 +30,14 @@ public class SpacingAroundRangeOperatorRule : StandardRule("range-spacing") { nextLeaf.node.treeParent.removeChild(nextLeaf.node) } } + prevLeaf is PsiWhiteSpace -> { emit(prevLeaf.node.startOffset, "Unexpected spacing before \"${node.elementTypeDescription()}\"", true) if (autoCorrect) { prevLeaf.node.treeParent.removeChild(prevLeaf.node) } } + nextLeaf is PsiWhiteSpace -> { emit(nextLeaf.node.startOffset, "Unexpected spacing after \"${node.elementTypeDescription()}\"", true) if (autoCorrect) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundSquareBracketsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundSquareBracketsRule.kt index 08ff2a9c88..81bc8f8fb2 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundSquareBracketsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/SpacingAroundSquareBracketsRule.kt @@ -38,6 +38,7 @@ public class SpacingAroundSquareBracketsRule : StandardRule("square-brackets-spa // fun foo() {} false } + COLLECTION_LITERAL_EXPRESSION -> { // Allow: // @Foo( @@ -49,6 +50,7 @@ public class SpacingAroundSquareBracketsRule : StandardRule("square-brackets-spa // @Foo(fooBar = ["foo", "bar" ]) node.elementType == RBRACKET && prevLeaf.isWhiteSpaceWithoutNewline() } + else -> { prevLeaf.isWhiteSpaceWithoutNewline() } @@ -76,12 +78,14 @@ public class SpacingAroundSquareBracketsRule : StandardRule("square-brackets-spa nextLeaf!!.treeParent.removeChild(nextLeaf) } } + spacingBefore -> { emit(prevLeaf!!.startOffset, "Unexpected spacing before '${node.text}'", true) if (autoCorrect) { prevLeaf.treeParent.removeChild(prevLeaf) } } + spacingAfter -> { emit(node.startOffset + 1, "Unexpected spacing after '${node.text}'", true) if (autoCorrect) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRule.kt index 3eb12370f7..6d46186d12 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/StringTemplateIndentRule.kt @@ -78,6 +78,7 @@ public class StringTemplateIndentRule : wrongIndentChar = "\t" wrongIndentDescription = "tab" } + IndentConfig.IndentStyle.TAB -> { wrongIndentChar = " " wrongIndentDescription = "space" diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TrailingCommaOnCallSiteRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TrailingCommaOnCallSiteRule.kt index 1a91d539e6..85e8b090f6 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TrailingCommaOnCallSiteRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TrailingCommaOnCallSiteRule.kt @@ -168,6 +168,7 @@ public class TrailingCommaOnCallSiteRule : this.removeChild(trailingCommaNode) } } + TrailingCommaState.MISSING -> if (isTrailingCommaAllowed) { val prevNode = inspectNode.prevCodeLeaf()!! @@ -185,6 +186,7 @@ public class TrailingCommaOnCallSiteRule : } } } + TrailingCommaState.REDUNDANT -> { emit( trailingCommaNode!!.startOffset, @@ -195,6 +197,7 @@ public class TrailingCommaOnCallSiteRule : this.removeChild(trailingCommaNode) } } + TrailingCommaState.NOT_EXISTS -> Unit } } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TrailingCommaOnDeclarationSiteRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TrailingCommaOnDeclarationSiteRule.kt index 59106a484d..cf07130557 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TrailingCommaOnDeclarationSiteRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TrailingCommaOnDeclarationSiteRule.kt @@ -302,6 +302,7 @@ public class TrailingCommaOnDeclarationSiteRule : this.removeChild(trailingCommaNode) } } + TrailingCommaState.MISSING -> if (isTrailingCommaAllowed) { val leafBeforeArrowOrNull = leafBeforeArrowOrNull() @@ -361,6 +362,7 @@ public class TrailingCommaOnDeclarationSiteRule : } } } + TrailingCommaState.REDUNDANT -> { emit( trailingCommaNode!!.startOffset, @@ -371,6 +373,7 @@ public class TrailingCommaOnDeclarationSiteRule : this.removeChild(trailingCommaNode) } } + TrailingCommaState.NOT_EXISTS -> Unit } } @@ -380,15 +383,19 @@ public class TrailingCommaOnDeclarationSiteRule : element.parent is KtFunctionLiteral -> { isMultiline(element.parent) } + element is KtFunctionLiteral -> { containsLineBreakInLeavesRange(element.valueParameterList!!, element.arrow!!) } + element is KtWhenEntry -> { containsLineBreakInLeavesRange(element.firstChild, element.arrow!!) } + element is KtDestructuringDeclaration -> { containsLineBreakInLeavesRange(element.lPar!!, element.rPar!!) } + element is KtValueArgumentList && element.children.size == 1 && element.anyDescendantOfType() -> { @@ -399,9 +406,11 @@ public class TrailingCommaOnDeclarationSiteRule : val lastChild = element.collectDescendantsOfType().last() containsLineBreakInLeavesRange(lastChild.rightBracket!!, element.rightParenthesis!!) } + element is KtParameterList && element.parameters.isEmpty() -> { false } + else -> { element.textContains('\n') } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TryCatchFinallySpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TryCatchFinallySpacingRule.kt index bee881f08d..13938a6547 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TryCatchFinallySpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TryCatchFinallySpacingRule.kt @@ -64,6 +64,7 @@ public class TryCatchFinallySpacingRule : BLOCK -> { visitBlock(node, emit, autoCorrect) } + CATCH, FINALLY -> { visitClause(node, emit, autoCorrect) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRule.kt index 9848ea1197..3f9c2dbf57 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeArgumentListSpacingRule.kt @@ -56,6 +56,7 @@ public class TypeArgumentListSpacingRule : visitFunctionDeclaration(node, autoCorrect, emit) visitInsideTypeArgumentList(node, autoCorrect, emit) } + ElementType.SUPER_TYPE_LIST, ElementType.SUPER_EXPRESSION -> visitInsideTypeArgumentList(node, autoCorrect, emit) } diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeParameterListSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeParameterListSpacingRule.kt index abad5abd0b..250182acd2 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeParameterListSpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/TypeParameterListSpacingRule.kt @@ -270,6 +270,7 @@ public class TypeParameterListSpacingRule : ) { when { node.text == " " -> Unit + node.textContains('\n') -> { emit( node.startOffset, @@ -280,6 +281,7 @@ public class TypeParameterListSpacingRule : (node as LeafPsiElement).rawReplaceWithText(" ") } } + else -> { emit( node.startOffset, diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBetweenWhenConditionsTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBetweenWhenConditionsTest.kt new file mode 100644 index 0000000000..828fca3d87 --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBetweenWhenConditionsTest.kt @@ -0,0 +1,247 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.ruleset.standard.rules.BlankLineBetweenWhenConditions.Companion.LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import com.pinterest.ktlint.test.LintViolation +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class BlankLineBetweenWhenConditionsTest { + private val blankLineAfterWhenConditionRuleAssertThat = assertThatRule { BlankLineBetweenWhenConditions() } + + @Test + fun `Given a when-statement with single line when-conditions only then do no reformat`() { + val code = + """ + val foo = + when (bar) { + BAR1 -> "bar1" + BAR2 -> "bar2" + else -> null + } + """.trimIndent() + blankLineAfterWhenConditionRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a when-statement with single line when-conditions only which are separated by a blank line then remove the blank lines`() { + val code = + """ + val foo = + when (bar) { + BAR1 -> "bar1" + + BAR2 -> "bar2" + + else -> null + } + """.trimIndent() + val formattedCode = + """ + val foo = + when (bar) { + BAR1 -> "bar1" + BAR2 -> "bar2" + else -> null + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + blankLineAfterWhenConditionRuleAssertThat(code) + .hasLintViolations( + LintViolation(4, 1, "Unexpected blank lines between when-condition if all when-conditions are single lines"), + LintViolation(6, 1, "Unexpected blank lines between when-condition if all when-conditions are single lines"), + ).isFormattedAs(formattedCode) + } + + @Nested + inner class `Given when statement with multiline when-condition` { + @Nested + inner class `Given linebreaks have to be added after when-condition` { + @Test + fun `Given a when-statement with a single line when-condition after a multiline when-condition then add a blank line between the when-conditions`() { + val code = + """ + val foo = + when (bar) { + BAR1 -> "bar1" + BAR2 -> + "bar2" + else -> null + } + """.trimIndent() + val formattedCode = + """ + val foo = + when (bar) { + BAR1 -> "bar1" + + BAR2 -> + "bar2" + + else -> null + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + blankLineAfterWhenConditionRuleAssertThat(code) + .hasLintViolations( + LintViolation(4, 1, "Add a blank line between all when-condition in case at least one multiline when-condition is found in the statement"), + LintViolation(6, 1, "Add a blank line between all when-condition in case at least one multiline when-condition is found in the statement"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a when-statement with a single line when-condition before a multiline when-condition then add a blank line between the when-conditions`() { + val code = + """ + val foo = + when (bar) { + BAR1 -> "bar1" + BAR2 -> "bar2" + else -> + null + } + """.trimIndent() + val formattedCode = + """ + val foo = + when (bar) { + BAR1 -> "bar1" + + BAR2 -> "bar2" + + else -> + null + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + blankLineAfterWhenConditionRuleAssertThat(code) + .hasLintViolations( + LintViolation(4, 1, "Add a blank line between all when-condition in case at least one multiline when-condition is found in the statement"), + LintViolation(5, 1, "Add a blank line between all when-condition in case at least one multiline when-condition is found in the statement"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given xx a when-condition preceded by a comment and the when-condition needs to be preceded by blank line then add this line before the comment`() { + val code = + """ + val foo = + when (bar) { + BAR1 -> + "bar1" + BAR2 -> + "bar2" + else -> + null + } + """.trimIndent() + val formattedCode = + """ + val foo = + when (bar) { + BAR1 -> + "bar1" + + BAR2 -> + "bar2" + + else -> + null + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + blankLineAfterWhenConditionRuleAssertThat(code) + .hasLintViolations( + LintViolation(5, 1, "Add a blank line between all when-condition in case at least one multiline when-condition is found in the statement"), + LintViolation(7, 1, "Add a blank line between all when-condition in case at least one multiline when-condition is found in the statement"), + ).isFormattedAs(formattedCode) + } + + @Test + fun `Given a when-condition preceded by a comment and the when-condition needs to be preceded by blank line then add this line before the comment`() { + val code = + """ + val foo = + when (bar) { + BAR -> "bar" + // Some comment + else -> + null + } + """.trimIndent() + val formattedCode = + """ + val foo = + when (bar) { + BAR -> "bar" + + // Some comment + else -> + null + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + blankLineAfterWhenConditionRuleAssertThat(code) + .hasLintViolation(4, 1, "Add a blank line between all when-condition in case at least one multiline when-condition is found in the statement") + .isFormattedAs(formattedCode) + } + } + + @Nested + inner class `Given no linebreaks may be added after when-condition` { + @Test + fun `Given a when-statement with a single line when-condition after a multiline when-condition then add a blank line between the when-conditions`() { + val code = + """ + val foo = + when (bar) { + BAR1 -> "bar1" + BAR2 -> + "bar2" + else -> null + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + blankLineAfterWhenConditionRuleAssertThat(code) + .withEditorConfigOverride(LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY to false) + .hasNoLintViolations() + } + + @Test + fun `Given a when-statement with a single line when-condition before a multiline when-condition then add a blank line between the when-conditions`() { + val code = + """ + val foo = + when (bar) { + BAR1 -> "bar1" + BAR2 -> "bar2" + else -> + null + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + blankLineAfterWhenConditionRuleAssertThat(code) + .withEditorConfigOverride(LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY to false) + .hasNoLintViolations() + } + + @Test + fun `Given a when-condition preceded by a comment and the when-condition needs to be preceded by blank line then add this line before the comment`() { + val code = + """ + val foo = + when (bar) { + BAR -> "bar" + // Some comment + else -> + null + } + """.trimIndent() + @Suppress("ktlint:standard:argument-list-wrapping", "ktlint:standard:max-line-length") + blankLineAfterWhenConditionRuleAssertThat(code) + .withEditorConfigOverride(LINE_BREAK_AFTER_WHEN_CONDITION_PROPERTY to false) + .hasNoLintViolations() + } + } + } +}