From d57e654bf4e69b2df4f47be528c28c124ca32df5 Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Sat, 19 Mar 2022 12:12:05 +0100 Subject: [PATCH] Add new experimental rule to check consistent spacing before the start of the body expression or body block This rule is required for implementing #1341. --- CHANGELOG.md | 2 + README.md | 1 + .../ExperimentalRuleSetProvider.kt | 3 +- .../FunctionStartOfBodySpacingRule.kt | 135 +++++++++ .../FunctionStartOfBodySpacingRuleTest.kt | 256 ++++++++++++++++++ 5 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRule.kt create mode 100644 ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRuleTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a4219cda..1264095b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Added +- Add experimental rule for consistent spacing before the start of the function body (`function-start-of-body-spacing`) ([#1341](https://github.com/pinterest/ktlint/issues/1341)) + ### Fixed ### Changed diff --git a/README.md b/README.md index 274a232cff..c0f4a9d44f 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ by passing the `--experimental` flag to `ktlint`. - `experimental:annotation-spacing`: Annotations should be separated by the annotated declaration by a single line break - `experimental:double-colon-spacing`: No spaces around `::` - `experimental:fun-keyword-spacing`: Consistent spacing after the fun keyword +- `experimental:function-start-of-body-spacing`: Consistent spacing before start of function body - `experimental:function-type-reference-spacing`: Consistent spacing in the type reference before a function - `experimental:modifier-list-spacing`: Consistent spacing between modifiers in and after the last modifier in a modifier list - `experimental:spacing-around-angle-brackets`: No spaces around angle brackets diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt index 7a4f4decab..372b708547 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/ExperimentalRuleSetProvider.kt @@ -30,6 +30,7 @@ public class ExperimentalRuleSetProvider : RuleSetProvider { FunctionTypeReferenceSpacingRule(), ModifierListSpacingRule(), CommentWrappingRule(), - KdocWrappingRule() + KdocWrappingRule(), + FunctionStartOfBodySpacingRule() ) } diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRule.kt new file mode 100644 index 0000000000..4b50487124 --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRule.kt @@ -0,0 +1,135 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType +import com.pinterest.ktlint.core.ast.ElementType.FUN +import com.pinterest.ktlint.core.ast.nextLeaf +import com.pinterest.ktlint.core.ast.prevLeaf +import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe +import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement + +/** + * Lints and formats the spacing after the fun keyword + */ +public class FunctionStartOfBodySpacingRule : Rule("function-start-of-body-spacing") { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == FUN) { + node + .findChildByType(ElementType.EQ) + ?.let { visitFunctionFollowedByBodyExpression(node, emit, autoCorrect) } + + node + .findChildByType(ElementType.BLOCK) + ?.let { visitFunctionFollowedByBodyBlock(node, emit, autoCorrect) } + } + } + + private fun visitFunctionFollowedByBodyExpression( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + fixWhiteSpaceBeforeAssignmentOfBodyExpression(node, emit, autoCorrect) + fixWhiteSpaceBetweenAssignmentAndBodyExpression(node, emit, autoCorrect) + } + + private fun fixWhiteSpaceBeforeAssignmentOfBodyExpression( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + node + .findChildByType(ElementType.EQ) + ?.let { assignmentExpression -> + assignmentExpression + .prevLeaf(includeEmpty = true) + ?.takeIf { it.elementType == ElementType.WHITE_SPACE } + .let { whiteSpaceBeforeAssignment -> + if (whiteSpaceBeforeAssignment == null) { + emit(assignmentExpression.startOffset, "Expected a single white space before assignment of expression body", true) + if (autoCorrect) { + (assignmentExpression as LeafPsiElement).upsertWhitespaceBeforeMe(" ") + } + } else if (whiteSpaceBeforeAssignment.text != " ") { + emit(whiteSpaceBeforeAssignment.startOffset, "Unexpected whitespace", true) + if (autoCorrect) { + (assignmentExpression as LeafPsiElement).upsertWhitespaceBeforeMe(" ") + } + } + } + } + } + + private fun fixWhiteSpaceBetweenAssignmentAndBodyExpression( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + node + .findChildByType(ElementType.EQ) + ?.let { assignmentExpression -> + assignmentExpression + .nextLeaf(includeEmpty = true) + ?.takeIf { it.elementType == ElementType.WHITE_SPACE } + .let { whiteSpaceAfterAssignment -> + if (whiteSpaceAfterAssignment == null) { + emit( + assignmentExpression.startOffset, + "Expected a single white space between assignment and expression body on same line", + true + ) + if (autoCorrect) { + (assignmentExpression as LeafPsiElement).upsertWhitespaceAfterMe(" ") + } + } else if (whiteSpaceAfterAssignment.text != " " && !whiteSpaceAfterAssignment.textContains('\n')) { + emit(whiteSpaceAfterAssignment.startOffset, "Unexpected whitespace", true) + if (autoCorrect) { + (assignmentExpression as LeafPsiElement).upsertWhitespaceAfterMe(" ") + } + } + } + } + } + + private fun visitFunctionFollowedByBodyBlock( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + node + .findChildByType(ElementType.BLOCK) + ?.let { block -> + block + .prevLeaf(includeEmpty = true) + ?.takeIf { it.elementType == ElementType.WHITE_SPACE } + .let { whiteSpaceBeforeExpressionBlock -> + if (whiteSpaceBeforeExpressionBlock == null) { + emit(block.startOffset, "Expected a single white space before start of function body", true) + if (autoCorrect) { + if (whiteSpaceBeforeExpressionBlock == null) { + (block.firstChildNode as LeafPsiElement).upsertWhitespaceBeforeMe(" ") + } else { + (whiteSpaceBeforeExpressionBlock as LeafElement).rawReplaceWithText(" ") + } + } + } else if (whiteSpaceBeforeExpressionBlock.text != " ") { + emit(whiteSpaceBeforeExpressionBlock.startOffset, "Unexpected whitespace", true) + if (autoCorrect) { + if (whiteSpaceBeforeExpressionBlock == null) { + (block.firstChildNode as LeafPsiElement).upsertWhitespaceBeforeMe(" ") + } else { + (whiteSpaceBeforeExpressionBlock as LeafElement).rawReplaceWithText(" ") + } + } + } + } + } + } +} diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRuleTest.kt new file mode 100644 index 0000000000..d039efe0bd --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionStartOfBodySpacingRuleTest.kt @@ -0,0 +1,256 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.LintError +import com.pinterest.ktlint.test.format +import com.pinterest.ktlint.test.lint +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +class FunctionStartOfBodySpacingRuleTest { + @Nested + @DisplayName("Given a function signature followed by an expression body") + inner class ExpressionBody { + @Test + fun `Given that the signature contains required spacing then do not reformat`() { + val code = + """ + fun foo() = "some-result" + fun bar(): String = "some-result" + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).isEmpty() + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(code) + } + + @Nested + @DisplayName("Given the spacing before the equality sign") + inner class SpacingBeforeEqualitySignInExpressionBody { + @Test + fun `Given that no space is found before the equals sign then reformat`() { + val code = + """ + fun foo()= "some-result" + fun bar(): String= "some-result" + """.trimIndent() + val formattedCode = + """ + fun foo() = "some-result" + fun bar(): String = "some-result" + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).containsExactly( + LintError( + 1, + 10, + "function-start-of-body-spacing", + "Expected a single white space before assignment of expression body" + ), + LintError( + 2, + 18, + "function-start-of-body-spacing", + "Expected a single white space before assignment of expression body" + ) + ) + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given that multiple spaces are found before the equals sign then reformat`() { + val code = + """ + fun foo() = "some-result" + fun bar(): String = "some-result" + """.trimIndent() + val formattedCode = + """ + fun foo() = "some-result" + fun bar(): String = "some-result" + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).containsExactly( + LintError(1, 10, "function-start-of-body-spacing", "Unexpected whitespace"), + LintError(2, 18, "function-start-of-body-spacing", "Unexpected whitespace") + ) + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given that newline is found before the equals sign then reformat`() { + val code = + """ + fun foo() + = "some-result" + fun bar(): String + = "some-result" + """.trimIndent() + val formattedCode = + """ + fun foo() = "some-result" + fun bar(): String = "some-result" + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).containsExactly( + LintError(1, 10, "function-start-of-body-spacing", "Unexpected whitespace"), + LintError(3, 18, "function-start-of-body-spacing", "Unexpected whitespace") + ) + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(formattedCode) + } + } + + @Nested + @DisplayName("Given the spacing after the equality sign") + inner class SpacingAfterEqualitySignInExpressionBody { + @Test + fun `Given that no space is found between the equals sign and expression body on the same line then reformat`() { + val code = + """ + fun foo() ="some-result" + fun bar(): String ="some-result" + """.trimIndent() + val formattedCode = + """ + fun foo() = "some-result" + fun bar(): String = "some-result" + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).containsExactly( + LintError(1, 11, "function-start-of-body-spacing", "Expected a single white space between assignment and expression body on same line"), + LintError(2, 19, "function-start-of-body-spacing", "Expected a single white space between assignment and expression body on same line") + ) + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given that multiple space are found between the equals sign and expression body on the same line then reformat`() { + val code = + """ + fun foo() = "some-result" + fun bar(): String = "some-result" + """.trimIndent() + val formattedCode = + """ + fun foo() = "some-result" + fun bar(): String = "some-result" + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).containsExactly( + LintError(1, 12, "function-start-of-body-spacing", "Unexpected whitespace"), + LintError(2, 20, "function-start-of-body-spacing", "Unexpected whitespace") + ) + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given that a newline is found between the equals sign and expression body then do not reformat`() { + val code = + """ + fun foo() = + "some-result" + fun bar(): String = + "some-result" + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).isEmpty() + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(code) + } + } + } + + @Nested + @DisplayName("Given a function signature followed by a body block") + inner class BodyBlock { + @Test + fun `Given that the signature contains required spacing then do not reformat`() { + val code = + """ + fun foo() { + // do something + } + fun bar(): String { + return "some-result" + } + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).isEmpty() + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given that no space is found before the body block then reformat`() { + val code = + """ + fun foo(){ + // do something + } + fun bar(): String{ + return "some-result" + } + """.trimIndent() + val formattedCode = + """ + fun foo() { + // do something + } + fun bar(): String { + return "some-result" + } + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).containsExactly( + LintError(1, 10, "function-start-of-body-spacing", "Expected a single white space before start of function body"), + LintError(4, 18, "function-start-of-body-spacing", "Expected a single white space before start of function body") + ) + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given that multiple spaces are found before the body block then reformat`() { + val code = + """ + fun foo() { + // do something + } + fun bar(): String { + return "some-result" + } + """.trimIndent() + val formattedCode = + """ + fun foo() { + // do something + } + fun bar(): String { + return "some-result" + } + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).containsExactly( + LintError(1, 10, "function-start-of-body-spacing", "Unexpected whitespace"), + LintError(4, 18, "function-start-of-body-spacing", "Unexpected whitespace") + ) + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given that newline is found before the body block then reformat`() { + val code = + """ + fun foo() + { + // do something + } + fun bar(): String + { + return "some-result" + } + """.trimIndent() + val formattedCode = + """ + fun foo() { + // do something + } + fun bar(): String { + return "some-result" + } + """.trimIndent() + assertThat(FunctionStartOfBodySpacingRule().lint(code)).containsExactly( + LintError(1, 10, "function-start-of-body-spacing", "Unexpected whitespace"), + LintError(5, 18, "function-start-of-body-spacing", "Unexpected whitespace") + ) + assertThat(FunctionStartOfBodySpacingRule().format(code)).isEqualTo(formattedCode) + } + } +}