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 fa3f9aedd5..f185dcd460 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 @@ -21,6 +21,7 @@ public class ExperimentalRuleSetProvider : RuleSetProvider { SpacingAroundAngleBracketsRule(), SpacingAroundUnaryOperatorRule(), AnnotationSpacingRule(), - UnnecessaryParenthesesBeforeTrailingLambdaRule() + UnnecessaryParenthesesBeforeTrailingLambdaRule(), + TypeParameterListSpacingRule() ) } diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRule.kt new file mode 100644 index 0000000000..8b85a11176 --- /dev/null +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRule.kt @@ -0,0 +1,126 @@ +package com.pinterest.ktlint.ruleset.experimental + +import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.CLASS +import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY +import com.pinterest.ktlint.core.ast.ElementType.CONSTRUCTOR_KEYWORD +import com.pinterest.ktlint.core.ast.ElementType.PRIMARY_CONSTRUCTOR +import com.pinterest.ktlint.core.ast.ElementType.TYPE_PARAMETER_LIST +import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE +import com.pinterest.ktlint.core.ast.nextCodeSibling +import com.pinterest.ktlint.core.ast.nextSibling +import com.pinterest.ktlint.core.ast.prevSibling +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.LeafPsiElement + +public class TypeParameterListSpacingRule : Rule("type-parameter-list-spacing") { + override fun visit( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.elementType != TYPE_PARAMETER_LIST) { + return + } + + if (node.treeParent.elementType == CLASS) { + // No white space expected between class name and parameter list + // class Bar + node + .prevSibling { true } + ?.takeIf { it.elementType == WHITE_SPACE } + ?.let { noWhitespaceExpected(it, autoCorrect, emit) } + + // No white space expected between parameter type list and the constructor except when followed by compound + // constructor + // class Bar (...) + node + .nextSibling { true } + ?.takeIf { it.elementType == WHITE_SPACE && it.nextCodeSibling()?.elementType == PRIMARY_CONSTRUCTOR } + ?.let { whiteSpace -> + if (whiteSpace.nextCodeSibling()?.findChildByType(CONSTRUCTOR_KEYWORD) != null) { + // class Bar constructor(...) + // class Bar actual constructor(...) + // class Bar @SomeAnnotation constructor(...) + singleWhiteSpaceExpected(whiteSpace, autoCorrect, emit) + } else { + noWhitespaceExpected(whiteSpace, autoCorrect, emit) + } + } + + // No white space expected between parameter type list and class body when constructor is missing + // class Bar { + node + .nextSibling { true } + ?.takeIf { it.elementType == WHITE_SPACE && it.nextCodeSibling()?.elementType == CLASS_BODY } + ?.let { singleWhiteSpaceExpected(it, autoCorrect, emit) } + } else { + visitWhiteSpaceRelatedToFunction(node, autoCorrect, emit) + } + } + + private fun noWhitespaceExpected( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + if (node.text != "") { + emit( + node.startOffset, + "No whitespace expected at this position", + true + ) + if (autoCorrect) { + node.treeParent.removeChild(node) + } + } + } + + private fun singleWhiteSpaceExpected( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + when { + node.text == " " -> Unit + node.textContains('\n') -> { + emit( + node.startOffset, + "Expected a single space instead of newline", + true + ) + if (autoCorrect) { + (node as LeafPsiElement).rawReplaceWithText(" ") + } + } + else -> { + emit( + node.startOffset, + "Expected a single space", + true + ) + if (autoCorrect) { + if (node.elementType == WHITE_SPACE) { + (node as LeafPsiElement).rawReplaceWithText(" ") + } else { + (node as LeafPsiElement).upsertWhitespaceBeforeMe(" ") + } + } + } + } + } + + private fun visitWhiteSpaceRelatedToFunction( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit + ) { + node + .nextSibling { true } + ?.takeUnless { it.elementType == WHITE_SPACE && it.text == " " } + ?.let { node -> + singleWhiteSpaceExpected(node, autoCorrect, emit) + } + } +} diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRuleTest.kt new file mode 100644 index 0000000000..632553192a --- /dev/null +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRuleTest.kt @@ -0,0 +1,213 @@ +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.Test + +class TypeParameterListSpacingRuleTest { + /* Enable once https://github.com/pinterest/ktlint/pull/1365 is merged + @Test + fun `Given a type parameter list followed by a comment then it can be ignored as it will be handled by the discouraged-comment-location rule`() { + val code = + """ + fun // some-comment but it also applies to a block comment or KDoc + foo1(t: T) = "some-result" + """.trimIndent() + assertThat( + listOf(DiscouragedCommentLocationRule(), TypeParameterListSpacingRule()).lint(code) + ).containsExactly( + LintError(1, 9, "discouraged-comment-location", "No comment expected at this location") + ) + } + */ + + @Test + fun `Given a type parameter list not followed by whitespace then add a single white space`() { + val code = + """ + fun foo(t: T) = "some-result" + """.trimIndent() + val formattedCode = + """ + fun foo(t: T) = "some-result" + """.trimIndent() + assertThat(TypeParameterListSpacingRule().lint(code)).containsExactly( + LintError(1, 8, "type-parameter-list-spacing", "Expected a single space") + ) + assertThat(TypeParameterListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a type parameter list followed by multiple spaces then the redundant spaces are removed`() { + val code = + """ + fun foo(t: T) = "some-result" + """.trimIndent() + val formattedCode = + """ + fun foo(t: T) = "some-result" + """.trimIndent() + assertThat(TypeParameterListSpacingRule().lint(code)).containsExactly( + LintError(1, 8, "type-parameter-list-spacing", "Expected a single space") + ) + assertThat(TypeParameterListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a class or interface definition with a type parameter list followed by multiple spaces then the redundant spaces are removed`() { + val code = + """ + class Bar { + val bar: T? = null + } + interface foo { + fun bar(t: T) + } + """.trimIndent() + val formattedCode = + """ + class Bar { + val bar: T? = null + } + interface foo { + fun bar(t: T) + } + """.trimIndent() + assertThat(TypeParameterListSpacingRule().lint(code)).containsExactly( + LintError(1, 13, "type-parameter-list-spacing", "Expected a single space"), + LintError(4, 17, "type-parameter-list-spacing", "Expected a single space") + ) + assertThat(TypeParameterListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a class or interface definition with a type parameter list followed by a newline then replace the newline with a space`() { + val code = + """ + class Bar + { + val bar: T? = null + } + interface foo + { + fun bar(t: T) + } + """.trimIndent() + val formattedCode = + """ + class Bar { + val bar: T? = null + } + interface foo { + fun bar(t: T) + } + """.trimIndent() + assertThat(TypeParameterListSpacingRule().lint(code)).containsExactly( + LintError(1, 13, "type-parameter-list-spacing", "Expected a single space instead of newline"), + LintError(5, 17, "type-parameter-list-spacing", "Expected a single space instead of newline") + ) + assertThat(TypeParameterListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a class definition with a type parameter list followed by one space and a compound constructor then do not remove the space`() { + val code = + """ + class Foo1 constructor() {} + class Foo2 actual constructor() {} + class Foo3 private constructor() {} + class Foo4 internal constructor() {} + class Foo5 @FooBar constructor() {} + class Foo6 @FooBar internal constructor() {} + """.trimIndent() + assertThat(TypeParameterListSpacingRule().lint(code)).isEmpty() + assertThat(TypeParameterListSpacingRule().format(code)).isEqualTo(code) + } + + @Test + fun `Given a class definition with a type parameter list followed by too many spaces and a compound constructor then replace with single space`() { + val code = + """ + class Foo1 constructor() {} + class Foo2 actual constructor() {} + class Foo3 private constructor() {} + class Foo4 internal constructor() {} + class Foo5 @FooBar constructor() {} + class Foo6 @FooBar internal constructor() {} + """.trimIndent() + val formattedCode = + """ + class Foo1 constructor() {} + class Foo2 actual constructor() {} + class Foo3 private constructor() {} + class Foo4 internal constructor() {} + class Foo5 @FooBar constructor() {} + class Foo6 @FooBar internal constructor() {} + """.trimIndent() + assertThat(TypeParameterListSpacingRule().lint(code)).containsExactly( + LintError(1, 16, "type-parameter-list-spacing", "Expected a single space"), + LintError(2, 16, "type-parameter-list-spacing", "Expected a single space"), + LintError(3, 16, "type-parameter-list-spacing", "Expected a single space"), + LintError(4, 16, "type-parameter-list-spacing", "Expected a single space"), + LintError(5, 16, "type-parameter-list-spacing", "Expected a single space"), + LintError(6, 16, "type-parameter-list-spacing", "Expected a single space") + ) + assertThat(TypeParameterListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a class definition with a type parameter list followed by newlines and a compound constructor then replace with single space`() { + val code = + """ + class Foo1 + constructor() {} + class Foo2 + actual constructor() {} + class Foo3 + private constructor() {} + class Foo4 + internal constructor() {} + class Foo5 + @FooBar constructor() {} + class Foo6 + @FooBar internal constructor() {} + """.trimIndent() + val formattedCode = + """ + class Foo1 constructor() {} + class Foo2 actual constructor() {} + class Foo3 private constructor() {} + class Foo4 internal constructor() {} + class Foo5 @FooBar constructor() {} + class Foo6 @FooBar internal constructor() {} + """.trimIndent() + assertThat(TypeParameterListSpacingRule().lint(code)).containsExactly( + LintError(1, 16, "type-parameter-list-spacing", "Expected a single space instead of newline"), + LintError(3, 16, "type-parameter-list-spacing", "Expected a single space instead of newline"), + LintError(5, 16, "type-parameter-list-spacing", "Expected a single space instead of newline"), + LintError(7, 16, "type-parameter-list-spacing", "Expected a single space instead of newline"), + LintError(9, 16, "type-parameter-list-spacing", "Expected a single space instead of newline"), + LintError(11, 16, "type-parameter-list-spacing", "Expected a single space instead of newline") + ) + assertThat(TypeParameterListSpacingRule().format(code)).isEqualTo(formattedCode) + } + + @Test + fun `Given a class definition with a type parameter list followed by a parameter list then the redundant spaces are removed`() { + val code = + """ + class Bar (val t: T) + """.trimIndent() + val formattedCode = + """ + class Bar(val t: T) + """.trimIndent() + assertThat(TypeParameterListSpacingRule().lint(code)).containsExactly( + LintError(1, 10, "type-parameter-list-spacing", "No whitespace expected at this position"), + LintError(1, 14, "type-parameter-list-spacing", "No whitespace expected at this position") + ) + assertThat(TypeParameterListSpacingRule().format(code)).isEqualTo(formattedCode) + } +}