Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental rule to lint/format the spacing after the type parameter list in a function signature #1366

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Please welcome [paul-dingemans](https://github.com/paul-dingemans) as an officia
- 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))
- Add experimental rule for unnecessary parentheses in function call followed by lambda ([#1068](https://github.com/pinterest/ktlint/issues/1068))
- Add experimental rule for incorrect spacing after a type parameter list (`type-parameter-list-spacing`) ([#1366](https://github.com/pinterest/ktlint/pull/1366))
- Add experimental rule to detect discouraged comment locations (`discouraged-comment-location`) ([#1365](https://github.com/pinterest/ktlint/pull/1365))
- Add rule to check spacing after fun keyword (`fun-keyword-spacing`) ([#1362](https://github.com/pinterest/ktlint/pull/1362))
- Add experimental rules for unnecessary spacing between modifiers in and after the last modifier in a modifier list ([#1361](https://github.com/pinterest/ktlint/pull/1361))
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ by passing the `--experimental` flag to `ktlint`.
- `experimental:spacing-around-angle-brackets`: No spaces around angle brackets
- `experimental:spacing-between-declarations-with-annotations`: Declarations with annotations should be separated by a blank line
- `experimental:spacing-between-declarations-with-comments`: Declarations with comments should be separated by a blank line
- `experimental:type-parameter-list-spacing`: Spacing after a type parameter list in function and class declarations
- `experimental:unary-op-spacing`: No spaces around unary operators

### Wrapping
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlin.reflect.KClass
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.CompositeElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
import org.jetbrains.kotlin.com.intellij.psi.tree.IElementType
Expand Down Expand Up @@ -183,6 +184,12 @@ fun ASTNode.isPartOf(klass: KClass<out PsiElement>): Boolean {
return false
}

public fun ASTNode.isPartOfCompositeElementOfType(iElementType: IElementType) =
parent(findCompositeElementOfType(iElementType))?.elementType == iElementType

public fun findCompositeElementOfType(iElementType: IElementType): (ASTNode) -> Boolean =
{ it.elementType == iElementType || it !is CompositeElement }

fun ASTNode.isPartOfString() =
parent(STRING_TEMPLATE, strict = false) != null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class ExperimentalRuleSetProvider : RuleSetProvider {
SpacingAroundUnaryOperatorRule(),
AnnotationSpacingRule(),
UnnecessaryParenthesesBeforeTrailingLambdaRule(),
TypeParameterListSpacingRule(),
TypeArgumentListSpacingRule(),
BlockCommentInitialStarAlignmentRule(),
DiscouragedCommentLocationRule(),
FunKeywordSpacingRule(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.CALL_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.GT
import com.pinterest.ktlint.core.ast.ElementType.LAMBDA_ARGUMENT
import com.pinterest.ktlint.core.ast.ElementType.LT
import com.pinterest.ktlint.core.ast.ElementType.SUPER_EXPRESSION
import com.pinterest.ktlint.core.ast.ElementType.SUPER_TYPE_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_REFERENCE
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.findCompositeElementOfType
import com.pinterest.ktlint.core.ast.isPartOfCompositeElementOfType
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.nextSibling
import com.pinterest.ktlint.core.ast.parent
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.prevSibling
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

/**
* Lints and formats the spacing before and after the angle brackets of a type argument list.
*/
public class TypeArgumentListSpacingRule : Rule("type-argument-list-spacing") {
override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
when (node.elementType) {
TYPE_ARGUMENT_LIST -> {
visitFunctionDeclaration(node, autoCorrect, emit)
visitInsideTypeArgumentList(node, autoCorrect, emit)
}
SUPER_TYPE_LIST, SUPER_EXPRESSION ->
visitInsideTypeArgumentList(node, autoCorrect, emit)
}
}

private fun visitFunctionDeclaration(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
// No whitespace expected before type argument list of function call
// val list = listOf <String>()
node
.prevLeaf(includeEmpty = true)
?.takeIf { it.elementType == WHITE_SPACE }
?.let { noWhitespaceExpected(it, autoCorrect, emit) }

// No whitespace expected after type argument list of function call
// val list = listOf<String> ()
node
.takeUnless {
// unless it is part of a type reference:
// fun foo(): List<Foo> { ... }
// var bar: List<Bar> = emptyList()
it.isPartOfCompositeElementOfType(TYPE_REFERENCE)
}
?.takeUnless {
// unless it is part of a call expression followed by lambda:
// bar<Foo> { ... }
it.isPartOfCallExpressionFolledByLambda()
}
?.lastChildNode
?.nextLeaf(includeEmpty = true)
?.takeIf { it.elementType == WHITE_SPACE }
?.let { noWhitespaceExpected(it, autoCorrect, emit) }
}

private fun visitInsideTypeArgumentList(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
// No whitespace expected after opening angle bracket of type argument list
// val list = listOf< String>()
node
.findChildByType(LT)
?.nextSibling { true }
?.takeIf { it.elementType == WHITE_SPACE }
?.let { noWhitespaceExpected(it, autoCorrect, emit) }

// No whitespace expected before closing angle bracket of type argument list
// val list = listOf<String >()
node
.findChildByType(GT)
?.prevSibling { true }
?.takeIf { it.elementType == WHITE_SPACE }
?.let { noWhitespaceExpected(it, 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 ASTNode.isPartOfCallExpressionFolledByLambda(): Boolean =
parent(findCompositeElementOfType(CALL_EXPRESSION))
?.takeIf { it.elementType == CALL_EXPRESSION }
?.findChildByType(LAMBDA_ARGUMENT)
.let { it != null }
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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.GT
import com.pinterest.ktlint.core.ast.ElementType.LT
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.nextLeaf
import com.pinterest.ktlint.core.ast.nextSibling
import com.pinterest.ktlint.core.ast.prevLeaf
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

/**
* Lints and formats the spacing before and after the angle brackets of a type parameter list.
*/
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) {
visitClassDeclaration(node, autoCorrect, emit)
} else {
visitFunctionDeclaration(node, autoCorrect, emit)
}
visitInsideTypeParameterList(node, autoCorrect, emit)
}

private fun visitClassDeclaration(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
// 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) {
// Single space expect before (modifier list of) constructor
// class Bar<T> constructor(...)
// class Bar<T> actual constructor(...)
// class Bar<T> @SomeAnnotation constructor(...)
singleSpaceExpected(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 { singleSpaceExpected(it, autoCorrect, emit) }
}

private fun visitFunctionDeclaration(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
// Single space expected before type parameter list of function
// fun<T> foo(...)
node
.prevLeaf(includeEmpty = true)
?.let { prevLeaf ->
if (prevLeaf.elementType == WHITE_SPACE) {
singleSpaceExpected(prevLeaf, autoCorrect, emit)
} else {
singleSpaceExpected(node.firstChildNode, autoCorrect, emit)
}
}

// Single space expected after type parameter list of function
// fun <T>foo(...)
// fun <T>List<T>foo(...)
node
.lastChildNode
.nextLeaf(includeEmpty = true)
?.let { nextSibling ->
singleSpaceExpected(nextSibling, autoCorrect, emit)
}
}

private fun visitInsideTypeParameterList(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
node
.findChildByType(LT)
?.nextSibling { true }
?.takeIf { it.elementType == WHITE_SPACE }
?.let { noWhitespaceExpected(it, autoCorrect, emit) }

node
.findChildByType(GT)
?.prevSibling { true }
?.takeIf { it.elementType == WHITE_SPACE }
?.let { noWhitespaceExpected(it, 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 singleSpaceExpected(
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(" ")
}
}
}
}
}
}
Loading