forked from pinterest/ktlint
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add experimental rule to lint/format the spacing after the type param…
…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
Showing
3 changed files
with
341 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
...src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
213 changes: 213 additions & 0 deletions
213
...test/kotlin/com/pinterest/ktlint/ruleset/experimental/TypeParameterListSpacingRuleTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |