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 (#1366)

Rule op-spacing should not handle characters like '<', '>' and '*' when not used as operator. This logic is moved to the TypeParameterListSpacingRule and new rule TypeArgumentListSpacingRule. 

This rule is required to create a rule which can rewrite the function signature automatically as is described in #1341
  • Loading branch information
paul-dingemans authored Mar 13, 2022
1 parent 2098cff commit 57e0cbe
Show file tree
Hide file tree
Showing 13 changed files with 712 additions and 53 deletions.
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

0 comments on commit 57e0cbe

Please sign in to comment.