From 806368898e160d4f05e1b16b4b1d373f4da45cf5 Mon Sep 17 00:00:00 2001 From: Paul Dingemans Date: Sat, 22 Jan 2022 12:53:40 +0100 Subject: [PATCH] Add rule FunctionTypeReferenceSpacingRule (#1341) --- CHANGELOG.md | 1 + .../ExperimentalRuleSetProvider.kt | 3 +- .../FunctionTypeReferenceSpacingRule.kt | 73 ++++++++++++++ .../NoSpacingAfterTypeReferenceRuleTest.kt | 95 +++++++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionTypeReferenceSpacingRule.kt create mode 100644 ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/NoSpacingAfterTypeReferenceRuleTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d79b84b8d..8ea8f76831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Added - Use Gradle JVM toolchain with language version 8 to compile the project - Basic tests for CLI ([#540](https://github.com/pinterest/ktlint/issues/540)) +- Add experimental rule for unexpected spaces in a type reference before a function identifier (`function-type-reference-spacing`) ([#1341](https://github.com/pinterest/ktlint/issues/1341)) ### Fixed - Fix indentation of function literal ([#1247](https://github.com/pinterest/ktlint/issues/1247)) 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 bf5dce4255..f877a73b8c 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 @@ -20,6 +20,7 @@ public class ExperimentalRuleSetProvider : RuleSetProvider { SpacingBetweenDeclarationsWithAnnotationsRule(), SpacingAroundAngleBracketsRule(), SpacingAroundUnaryOperatorRule(), - AnnotationSpacingRule() + AnnotationSpacingRule(), + FunctionTypeReferenceSpacingRule() ) } diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionTypeReferenceSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionTypeReferenceSpacingRule.kt new file mode 100644 index 0000000000..4955b11fa8 --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/FunctionTypeReferenceSpacingRule.kt @@ -0,0 +1,73 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.FUN +import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER +import com.pinterest.ktlint.core.ast.ElementType.NULLABLE_TYPE +import com.pinterest.ktlint.core.ast.ElementType.TYPE_REFERENCE +import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE +import com.pinterest.ktlint.core.ast.nextSibling +import org.jetbrains.kotlin.com.intellij.lang.ASTNode + +public class FunctionTypeReferenceSpacingRule : Rule("function-type-reference-spacing") { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType == FUN) { + node + .findTypeReferenceBeforeFunctionIdentifier() + ?.let { typeReference -> + typeReference + .firstChildNode + .takeIf { it.elementType == NULLABLE_TYPE } + ?.let { nullableTypeElement -> + visitNodesUntilIdentifier(nullableTypeElement.firstChildNode, emit, autoCorrect) + } + + if (typeReference.elementType != NULLABLE_TYPE) { + visitNodesUntilIdentifier(typeReference, emit, autoCorrect) + } + } + } + } + + private fun ASTNode.findTypeReferenceBeforeFunctionIdentifier(): ASTNode? { + require(elementType == FUN) + var currentNode: ASTNode? = firstChildNode + while (currentNode != null && currentNode.elementType != IDENTIFIER) { + if (currentNode.elementType == TYPE_REFERENCE) { + return currentNode + } + currentNode = currentNode.nextSibling { true } + } + return null + } + + private fun visitNodesUntilIdentifier( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + var currentNode: ASTNode? = node + while (currentNode != null && currentNode.elementType != IDENTIFIER) { + val nextNode = currentNode.nextSibling { true } + removeIfNonEmptyWhiteSpace(currentNode, emit, autoCorrect) + currentNode = nextNode + } + } + + private fun removeIfNonEmptyWhiteSpace( + node: ASTNode, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + autoCorrect: Boolean + ) { + if (node.elementType == WHITE_SPACE && node.text.isNotEmpty()) { + emit(node.startOffset, "Unexpected whitespace", true) + if (autoCorrect) { + node.treeParent.removeChild(node) + } + } + } +} diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/NoSpacingAfterTypeReferenceRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/NoSpacingAfterTypeReferenceRuleTest.kt new file mode 100644 index 0000000000..d1e416d6f7 --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/NoSpacingAfterTypeReferenceRuleTest.kt @@ -0,0 +1,95 @@ +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 +import org.junit.Test + +class NoSpacingAfterTypeReferenceRuleTest { + @Test + fun `Given a function signature with whitespace after a non nullable type reference of an extension function then remove this whitespace`() { + val code = + """ + fun String .foo1() = "some-result" + fun String + .foo2() = "some-result" + """.trimIndent() + val formattedCode = + """ + fun String.foo1() = "some-result" + fun String.foo2() = "some-result" + """.trimIndent() + Assertions.assertThat( + FunctionTypeReferenceSpacingRule().lint(code) + ).containsExactly( + LintError(1, 11, "function-type-reference-spacing", "Unexpected whitespace"), + LintError(2, 11, "function-type-reference-spacing", "Unexpected whitespace"), + ) + Assertions.assertThat( + FunctionTypeReferenceSpacingRule().format(code) + ).isEqualTo(formattedCode) + } + + @Test + fun `Given a function signature with whitespace in a nullable type reference of an extension function`() { + val code = + """ + fun String ?.foo1() = "some-result" + fun String + ?.foo2() = "some-result" + """.trimIndent() + val formattedCode = + """ + fun String?.foo1() = "some-result" + fun String?.foo2() = "some-result" + """.trimIndent() + Assertions.assertThat( + FunctionTypeReferenceSpacingRule().lint(code) + ).containsExactly( + LintError(1, 11, "function-type-reference-spacing", "Unexpected whitespace"), + LintError(2, 11, "function-type-reference-spacing", "Unexpected whitespace"), + ) + Assertions.assertThat( + FunctionTypeReferenceSpacingRule().format(code) + ).isEqualTo(formattedCode) + } + + @Test + fun `Given a function signature with whitespace after a nullable type reference of an extension function`() { + val code = + """ + fun String? .foo1() = "some-result" + fun String? + .foo2() = "some-result" + """.trimIndent() + val formattedCode = + """ + fun String?.foo1() = "some-result" + fun String?.foo2() = "some-result" + """.trimIndent() + Assertions.assertThat( + FunctionTypeReferenceSpacingRule().lint(code) + ).containsExactly( + LintError(1, 12, "function-type-reference-spacing", "Unexpected whitespace"), + LintError(2, 12, "function-type-reference-spacing", "Unexpected whitespace"), + ) + Assertions.assertThat( + FunctionTypeReferenceSpacingRule().format(code) + ).isEqualTo(formattedCode) + } + + @Test + fun `Given a function signature without a type reference before the function name then do not change the signature`() { + val code = + """ + fun foo1() = "some-result" + """.trimIndent() + Assertions.assertThat( + FunctionTypeReferenceSpacingRule().lint(code) + ).isEmpty() + Assertions.assertThat( + FunctionTypeReferenceSpacingRule().format(code) + ).isEqualTo(code) + } +}