From b830ec6c1cb43011c31d7948c3c196344abda85a Mon Sep 17 00:00:00 2001 From: Toshiaki Kameyama Date: Sun, 27 Aug 2023 07:48:03 +0900 Subject: [PATCH 1/4] Add `function-type-modifier-spacing` rule Closes #2202 --- CHANGELOG.md | 1 + .../snapshot/docs/rules/experimental.md | 20 +++++ .../api/ktlint-ruleset-standard.api | 9 ++ .../standard/StandardRuleSetProvider.kt | 2 + .../rules/FunctionTypeModifierSpacingRule.kt | 42 +++++++++ .../FunctionTypeModifierSpacingRuleTest.kt | 86 +++++++++++++++++++ 6 files changed, 160 insertions(+) create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt create mode 100644 ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRuleTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d7af7595eb..648cb1e6af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ Class "org.jetbrains.kotlin.com.intellij.treeCopyHandler" is no longer registere * Add experimental rule `function-expression-body`. This rule rewrites function bodies only contain a `return` or `throw` expression to an expression body. [#2150](https://github.com/pinterest/ktlint/issues/2150) * Add new experimental rule `statement-wrapping` which ensures function, class, or other blocks statement body doesn't start or end at starting or ending braces of the block ([#1938](https://github.com/pinterest/ktlint/issues/1938)). This rule was added in `0.50` release, but was never executed outside the unit tests. The rule is now added to the `StandardRuleSetProvider` ([#2170](https://github.com/pinterest/ktlint/issues/2170)) * Add experimental rule `chain-method-continuation` to the `ktlint_official` code style, but it can be enabled explicitly for the other code styles as well. This rule requires the operators (`.` or `?.`) for chaining method calls, to be aligned with each other. This rule is enabled by ([#1953](https://github.com/pinterest/ktlint/issues/1953)) +* Add experimental rule `function-type-modifier-spacing`. This rule enforces a single whitespace between the modifier list and the function type. [#2202](https://github.com/pinterest/ktlint/issues/2202) ### Removed diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md index 1fe3793f9c..027360e259 100644 --- a/documentation/snapshot/docs/rules/experimental.md +++ b/documentation/snapshot/docs/rules/experimental.md @@ -850,6 +850,26 @@ Rule id: `no-single-line-block-comment` (`standard` rule set) ## Spacing +### Function type modifier spacing + +Enforce a single whitespace between the modifier list and the function type. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + val foo: suspend () -> Unit = {} + suspend fun bar(baz: suspend () -> Unit) = baz() + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + val foo: suspend() -> Unit = {} + suspend fun bar(baz: suspend () -> Unit) = baz() + ``` + +Rule id: `function-type-modifier-spacing` (`standard` rule set) + ### No blank lines in list Disallow blank lines to be used in lists before the first element, between elements, and after the last element. diff --git a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api index f56a304cea..6fcc74658f 100644 --- a/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api +++ b/ktlint-ruleset-standard/api/ktlint-ruleset-standard.api @@ -272,6 +272,15 @@ public final class com/pinterest/ktlint/ruleset/standard/rules/FunctionStartOfBo public static final fun getFUNCTION_START_OF_BODY_SPACING_RULE_ID ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; } +public final class com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule : com/pinterest/ktlint/ruleset/standard/StandardRule, com/pinterest/ktlint/rule/engine/core/api/Rule$Experimental { + public fun ()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/FunctionTypeModifierSpacingRuleKt { + public static final fun getFUNCTION_TYPE_MODIFIER_SPACING_RULE ()Lcom/pinterest/ktlint/rule/engine/core/api/RuleId; +} + public final class com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeReferenceSpacingRule : 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 52f346a8b9..30f8ef18de 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 @@ -28,6 +28,7 @@ import com.pinterest.ktlint.ruleset.standard.rules.FunctionNamingRule import com.pinterest.ktlint.ruleset.standard.rules.FunctionReturnTypeSpacingRule import com.pinterest.ktlint.ruleset.standard.rules.FunctionSignatureRule import com.pinterest.ktlint.ruleset.standard.rules.FunctionStartOfBodySpacingRule +import com.pinterest.ktlint.ruleset.standard.rules.FunctionTypeModifierSpacingRule import com.pinterest.ktlint.ruleset.standard.rules.FunctionTypeReferenceSpacingRule import com.pinterest.ktlint.ruleset.standard.rules.IfElseBracingRule import com.pinterest.ktlint.ruleset.standard.rules.IfElseWrappingRule @@ -116,6 +117,7 @@ public class StandardRuleSetProvider : RuleSetProviderV3(RuleSetId.STANDARD) { RuleProvider { FunctionReturnTypeSpacingRule() }, RuleProvider { FunctionSignatureRule() }, RuleProvider { FunctionStartOfBodySpacingRule() }, + RuleProvider { FunctionTypeModifierSpacingRule() }, RuleProvider { FunctionTypeReferenceSpacingRule() }, RuleProvider { FunKeywordSpacingRule() }, RuleProvider { IfElseBracingRule() }, diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt new file mode 100644 index 0000000000..3b2747f8b2 --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt @@ -0,0 +1,42 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUNCTION_TYPE +import com.pinterest.ktlint.rule.engine.core.api.ElementType.MODIFIER_LIST +import com.pinterest.ktlint.rule.engine.core.api.ElementType.WHITE_SPACE +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.nextCodeSibling +import com.pinterest.ktlint.rule.engine.core.api.prevSibling +import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe +import com.pinterest.ktlint.ruleset.standard.StandardRule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +/** + * Lints and formats a single space between the modifier list and the function type + */ +public class FunctionTypeModifierSpacingRule : + StandardRule("function-type-modifier-spacing"), + Rule.Experimental { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + if (node.elementType != MODIFIER_LIST) return + val functionTypeNode = node.nextCodeSibling()?.takeIf { it.elementType == FUNCTION_TYPE } ?: return + if (functionTypeNode.prevSibling().isSingleSpace()) return + + emit( + functionTypeNode.startOffset, + "Expected a single space between the modifier list and the function type", + true, + ) + if (autoCorrect) { + functionTypeNode.upsertWhitespaceBeforeMe(" ") + } + } + + private fun ASTNode?.isSingleSpace(): Boolean = this != null && elementType == WHITE_SPACE && text == " " +} + +public val FUNCTION_TYPE_MODIFIER_SPACING_RULE: RuleId = FunctionTypeModifierSpacingRule().ruleId diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRuleTest.kt new file mode 100644 index 0000000000..d8174449bb --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRuleTest.kt @@ -0,0 +1,86 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import org.junit.jupiter.api.Test + +class FunctionTypeModifierSpacingRuleTest { + private val assertThatRule = assertThatRule { FunctionTypeModifierSpacingRule() } + + @Test + fun `Given no space between the modifier list and the function property type`() { + val code = + """ + val foo: suspend() -> Unit = {} + """.trimIndent() + val formattedCode = + """ + val foo: suspend () -> Unit = {} + """.trimIndent() + assertThatRule(code) + .hasLintViolation(1, 17, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given no space between the modifier list and the function parameter type`() { + val code = + """ + suspend fun bar(baz: suspend() -> Unit) = baz() + """.trimIndent() + val formattedCode = + """ + suspend fun bar(baz: suspend () -> Unit) = baz() + """.trimIndent() + assertThatRule(code) + .hasLintViolation(1, 29, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given multiple spaces between the modifier list and the function property type`() { + val code = + """ + val foo: suspend () -> Unit = {} + """.trimIndent() + val formattedCode = + """ + val foo: suspend () -> Unit = {} + """.trimIndent() + assertThatRule(code) + .hasLintViolation(1, 19, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given multiple spaces between the modifier list and the function parameter type`() { + val code = + """ + suspend fun bar(baz: suspend () -> Unit) = baz() + """.trimIndent() + val formattedCode = + """ + suspend fun bar(baz: suspend () -> Unit) = baz() + """.trimIndent() + assertThatRule(code) + .hasLintViolation(1, 33, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a single space between the modifier list and the function property type`() { + val code = + """ + val foo: suspend () -> Unit = {} + """.trimIndent() + assertThatRule(code).hasNoLintViolations() + } + + @Test + fun `Given a single space between the modifier list and the function parameter type`() { + val code = + """ + suspend fun bar(baz: suspend () -> Unit) = baz() + """.trimIndent() + assertThatRule(code).hasNoLintViolations() + } +} From d15a54c34282a26a3e852c6bd321eeff53864e6b Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Mon, 28 Aug 2023 20:29:18 +0200 Subject: [PATCH 2/4] Refactor - 'Invert' logic so that it more clear which condition have to be met before actually emitting/fixing a violation --- .../rules/FunctionTypeModifierSpacingRule.kt | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt index 3b2747f8b2..21ea65c4b9 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt @@ -22,18 +22,17 @@ public class FunctionTypeModifierSpacingRule : autoCorrect: Boolean, emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, ) { - if (node.elementType != MODIFIER_LIST) return - val functionTypeNode = node.nextCodeSibling()?.takeIf { it.elementType == FUNCTION_TYPE } ?: return - if (functionTypeNode.prevSibling().isSingleSpace()) return - - emit( - functionTypeNode.startOffset, - "Expected a single space between the modifier list and the function type", - true, - ) - if (autoCorrect) { - functionTypeNode.upsertWhitespaceBeforeMe(" ") - } + node + .takeIf { it.elementType == MODIFIER_LIST } + ?.nextCodeSibling() + ?.takeIf { it.elementType == FUNCTION_TYPE } + ?.takeUnless { it.prevSibling().isSingleSpace() } + ?.let { functionTypeNode -> + emit(functionTypeNode.startOffset, "Expected a single space between the modifier list and the function type", true) + if (autoCorrect) { + functionTypeNode.upsertWhitespaceBeforeMe(" ") + } + } } private fun ASTNode?.isSingleSpace(): Boolean = this != null && elementType == WHITE_SPACE && text == " " From dab6e9bb9ba2131113c964b7fd9a2e8fc0b4a3bb Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Mon, 28 Aug 2023 20:32:49 +0200 Subject: [PATCH 3/4] Refactor - Replace "prevSibling().isSingleSpace()" with "isPrecededBySingleSpace()" --- .../standard/rules/FunctionTypeModifierSpacingRule.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt index 21ea65c4b9..7b0c8e57d5 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRule.kt @@ -26,7 +26,7 @@ public class FunctionTypeModifierSpacingRule : .takeIf { it.elementType == MODIFIER_LIST } ?.nextCodeSibling() ?.takeIf { it.elementType == FUNCTION_TYPE } - ?.takeUnless { it.prevSibling().isSingleSpace() } + ?.takeUnless { it.isPrecededBySingleSpace() } ?.let { functionTypeNode -> emit(functionTypeNode.startOffset, "Expected a single space between the modifier list and the function type", true) if (autoCorrect) { @@ -35,7 +35,10 @@ public class FunctionTypeModifierSpacingRule : } } - private fun ASTNode?.isSingleSpace(): Boolean = this != null && elementType == WHITE_SPACE && text == " " + private fun ASTNode.isPrecededBySingleSpace(): Boolean = + prevSibling() + ?.let { it.elementType == WHITE_SPACE && it.text == " " } + ?: false } public val FUNCTION_TYPE_MODIFIER_SPACING_RULE: RuleId = FunctionTypeModifierSpacingRule().ruleId From 29041ec2c811f03a2cb9262ae63bd8a298ae97f4 Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Mon, 28 Aug 2023 20:44:03 +0200 Subject: [PATCH 4/4] Refactor - Group similar tests in nested class, reorder tests, add tests with unexpected newline --- .../FunctionTypeModifierSpacingRuleTest.kt | 185 +++++++++++------- 1 file changed, 117 insertions(+), 68 deletions(-) diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRuleTest.kt index d8174449bb..35597e7d02 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/FunctionTypeModifierSpacingRuleTest.kt @@ -1,86 +1,135 @@ package com.pinterest.ktlint.ruleset.standard.rules import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test class FunctionTypeModifierSpacingRuleTest { private val assertThatRule = assertThatRule { FunctionTypeModifierSpacingRule() } - @Test - fun `Given no space between the modifier list and the function property type`() { - val code = - """ - val foo: suspend() -> Unit = {} - """.trimIndent() - val formattedCode = - """ - val foo: suspend () -> Unit = {} - """.trimIndent() - assertThatRule(code) - .hasLintViolation(1, 17, "Expected a single space between the modifier list and the function type") - .isFormattedAs(formattedCode) - } + @Nested + inner class `Missing space before the function type` { + @Test + fun `Given no space between the modifier list and the function property type`() { + val code = + """ + val foo: suspend() -> Unit = {} + """.trimIndent() + val formattedCode = + """ + val foo: suspend () -> Unit = {} + """.trimIndent() + assertThatRule(code) + .hasLintViolation(1, 17, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } - @Test - fun `Given no space between the modifier list and the function parameter type`() { - val code = - """ - suspend fun bar(baz: suspend() -> Unit) = baz() - """.trimIndent() - val formattedCode = - """ - suspend fun bar(baz: suspend () -> Unit) = baz() - """.trimIndent() - assertThatRule(code) - .hasLintViolation(1, 29, "Expected a single space between the modifier list and the function type") - .isFormattedAs(formattedCode) + @Test + fun `Given no space between the modifier list and the function parameter type`() { + val code = + """ + suspend fun bar(baz: suspend() -> Unit) = baz() + """.trimIndent() + val formattedCode = + """ + suspend fun bar(baz: suspend () -> Unit) = baz() + """.trimIndent() + assertThatRule(code) + .hasLintViolation(1, 29, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } } - @Test - fun `Given multiple spaces between the modifier list and the function property type`() { - val code = - """ - val foo: suspend () -> Unit = {} - """.trimIndent() - val formattedCode = - """ - val foo: suspend () -> Unit = {} - """.trimIndent() - assertThatRule(code) - .hasLintViolation(1, 19, "Expected a single space between the modifier list and the function type") - .isFormattedAs(formattedCode) - } + @Nested + inner class `Exactly one space before the function type` { + @Test + fun `Given a single space between the modifier list and the function property type`() { + val code = + """ + val foo: suspend () -> Unit = {} + """.trimIndent() + assertThatRule(code).hasNoLintViolations() + } - @Test - fun `Given multiple spaces between the modifier list and the function parameter type`() { - val code = - """ - suspend fun bar(baz: suspend () -> Unit) = baz() - """.trimIndent() - val formattedCode = - """ - suspend fun bar(baz: suspend () -> Unit) = baz() - """.trimIndent() - assertThatRule(code) - .hasLintViolation(1, 33, "Expected a single space between the modifier list and the function type") - .isFormattedAs(formattedCode) + @Test + fun `Given a single space between the modifier list and the function parameter type`() { + val code = + """ + suspend fun bar(baz: suspend () -> Unit) = baz() + """.trimIndent() + assertThatRule(code).hasNoLintViolations() + } } - @Test - fun `Given a single space between the modifier list and the function property type`() { - val code = - """ - val foo: suspend () -> Unit = {} - """.trimIndent() - assertThatRule(code).hasNoLintViolations() + @Nested + inner class `Too many spaces before the function type` { + @Test + fun `Given multiple spaces between the modifier list and the function property type`() { + val code = + """ + val foo: suspend () -> Unit = {} + """.trimIndent() + val formattedCode = + """ + val foo: suspend () -> Unit = {} + """.trimIndent() + assertThatRule(code) + .hasLintViolation(1, 19, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given multiple spaces between the modifier list and the function parameter type`() { + val code = + """ + suspend fun bar(baz: suspend () -> Unit) = baz() + """.trimIndent() + val formattedCode = + """ + suspend fun bar(baz: suspend () -> Unit) = baz() + """.trimIndent() + assertThatRule(code) + .hasLintViolation(1, 33, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } } - @Test - fun `Given a single space between the modifier list and the function parameter type`() { - val code = - """ - suspend fun bar(baz: suspend () -> Unit) = baz() - """.trimIndent() - assertThatRule(code).hasNoLintViolations() + @Nested + inner class `Given unexpected newline before the function type` { + @Test + fun `Given unexpected newline between the modifier list and the function property type`() { + val code = + """ + val foo: suspend + () -> Unit = {} + """.trimIndent() + val formattedCode = + """ + val foo: suspend () -> Unit = {} + """.trimIndent() + assertThatRule(code) + .hasLintViolation(2, 10, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given unexpected newline between the modifier list and the function parameter type`() { + val code = + """ + suspend fun bar( + baz: suspend + () -> Unit + ) = baz() + """.trimIndent() + val formattedCode = + """ + suspend fun bar( + baz: suspend () -> Unit + ) = baz() + """.trimIndent() + assertThatRule(code) + .hasLintViolation(3, 10, "Expected a single space between the modifier list and the function type") + .isFormattedAs(formattedCode) + } } }