Skip to content

Commit

Permalink
Add experimental rule to lint/format the spacing after the type param…
Browse files Browse the repository at this point in the history
…eter list in a function signature

This rule is required to create a rule which can rewrite the function signature automatically as is described in pinterest#1341
  • Loading branch information
Paul Dingemans committed Feb 9, 2022
1 parent 87375a0 commit fc1b478
Show file tree
Hide file tree
Showing 3 changed files with 341 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ExperimentalRuleSetProvider : RuleSetProvider {
SpacingAroundAngleBracketsRule(),
SpacingAroundUnaryOperatorRule(),
AnnotationSpacingRule(),
UnnecessaryParenthesesBeforeTrailingLambdaRule()
UnnecessaryParenthesesBeforeTrailingLambdaRule(),
TypeParameterListSpacingRule()
)
}
Original file line number Diff line number Diff line change
@@ -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 <T>
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<T> (...)
node
.nextSibling { true }
?.takeIf { it.elementType == WHITE_SPACE && it.nextCodeSibling()?.elementType == PRIMARY_CONSTRUCTOR }
?.let { whiteSpace ->
if (whiteSpace.nextCodeSibling()?.findChildByType(CONSTRUCTOR_KEYWORD) != null) {
// class Bar<T> constructor(...)
// class Bar<T> actual constructor(...)
// class Bar<T> @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<T> {
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <T> // 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 <T>foo(t: T) = "some-result"
""".trimIndent()
val formattedCode =
"""
fun <T> 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 <T> foo(t: T) = "some-result"
""".trimIndent()
val formattedCode =
"""
fun <T> 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<T> {
val bar: T? = null
}
interface foo<T> {
fun bar(t: T)
}
""".trimIndent()
val formattedCode =
"""
class Bar<T> {
val bar: T? = null
}
interface foo<T> {
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<T>
{
val bar: T? = null
}
interface foo<T>
{
fun bar(t: T)
}
""".trimIndent()
val formattedCode =
"""
class Bar<T> {
val bar: T? = null
}
interface foo<T> {
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<Bar> constructor() {}
class Foo2<Bar> actual constructor() {}
class Foo3<Bar> private constructor() {}
class Foo4<Bar> internal constructor() {}
class Foo5<Bar> @FooBar constructor() {}
class Foo6<Bar> @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<Bar> constructor() {}
class Foo2<Bar> actual constructor() {}
class Foo3<Bar> private constructor() {}
class Foo4<Bar> internal constructor() {}
class Foo5<Bar> @FooBar constructor() {}
class Foo6<Bar> @FooBar internal constructor() {}
""".trimIndent()
val formattedCode =
"""
class Foo1<Bar> constructor() {}
class Foo2<Bar> actual constructor() {}
class Foo3<Bar> private constructor() {}
class Foo4<Bar> internal constructor() {}
class Foo5<Bar> @FooBar constructor() {}
class Foo6<Bar> @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<Bar>
constructor() {}
class Foo2<Bar>
actual constructor() {}
class Foo3<Bar>
private constructor() {}
class Foo4<Bar>
internal constructor() {}
class Foo5<Bar>
@FooBar constructor() {}
class Foo6<Bar>
@FooBar internal constructor() {}
""".trimIndent()
val formattedCode =
"""
class Foo1<Bar> constructor() {}
class Foo2<Bar> actual constructor() {}
class Foo3<Bar> private constructor() {}
class Foo4<Bar> internal constructor() {}
class Foo5<Bar> @FooBar constructor() {}
class Foo6<Bar> @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 <T> (val t: T)
""".trimIndent()
val formattedCode =
"""
class Bar<T>(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)
}
}

0 comments on commit fc1b478

Please sign in to comment.