Skip to content

Commit

Permalink
Add new experimental rule ParameterListSpacingRule (#1411)
Browse files Browse the repository at this point in the history
* Add new experimental rule ParameterListSpacingRule

This rule is required for implementing the rewrite function signature rule #1341
  • Loading branch information
paul-dingemans authored Apr 26, 2022
1 parent 835558f commit 07e1828
Show file tree
Hide file tree
Showing 5 changed files with 657 additions and 18 deletions.
23 changes: 6 additions & 17 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,7 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## Unreleased (0.46.0)

### API Changes & RuleSet providers

### Added
- Add experimental rule for unexpected spacing between function name and opening parenthesis (`spacing-between-function-name-and-opening-parenthesis`) ([#1341](https://github.com/pinterest/ktlint/issues/1341))

### Fixed

### Changed

### Removed


## Unreleased (0.45.0)
## Unreleased

### API Changes & RuleSet providers

Expand All @@ -27,16 +13,19 @@ If you are not an API user nor a RuleSet provider, then you can safely skip this
An AssertJ style API for testing KtLint rules ([#1444](https://github.com/pinterest/ktlint/issues/1444)) has been added. Usage of this API is encouraged in favor of using the old RuleExtension API. For more information, see [KtLintAssertThat API]( https://github.com/pinterest/ktlint/blob/master/ktlint-test/README.MD)

### Added
- Add experimental rule for unexpected spacing between function name and opening parenthesis (`spacing-between-function-name-and-opening-parenthesis`) ([#1341](https://github.com/pinterest/ktlint/issues/1341))
- Add experimental rule for unexpected spacing in the parameter list (`parameter-list-spacing`) ([#1341](https://github.com/pinterest/ktlint/issues/1341))
- Do not add a space after the typealias name (`type-parameter-list-spacing`) ([#1435](https://github.com/pinterest/ktlint/issues/1435))

### Fixed
- Do not add a space after the typealias name (`type-parameter-list-spacing`) ([#1435](https://github.com/pinterest/ktlint/issues/1435))

### Changed

* Set Kotlin development version to `1.6.21` and Kotlin version to `1.6.21`.

### Removed

### Removed

## [0.45.2] - 2022-04-06

### Fixed
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ by passing the `--experimental` flag to `ktlint`.
- `experimental:multiline-if-else`: Braces required for multiline if/else statements
- `experimental:no-empty-first-line-in-method-block`: No leading empty lines in method blocks
- `experimental:package-name`: No underscores in package names
- `experimental:parameter-list-spacing`: Consistent spacing inside the parameter list
- `experimental:unnecessary-parentheses-before-trailing-lambda`: An empty parentheses block before a lambda is redundant. For example `some-string".count() { it == '-' }`
### Spacing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class ExperimentalRuleSetProvider : RuleSetProvider {
ModifierListSpacingRule(),
CommentWrappingRule(),
KdocWrappingRule(),
SpacingBetweenFunctionNameAndOpeningParenthesisRule()
SpacingBetweenFunctionNameAndOpeningParenthesisRule(),
ParameterListSpacingRule()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.ANNOTATION_ENTRY
import com.pinterest.ktlint.core.ast.ElementType.COLON
import com.pinterest.ktlint.core.ast.ElementType.COMMA
import com.pinterest.ktlint.core.ast.ElementType.MODIFIER_LIST
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER
import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.children
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.prevCodeSibling
import com.pinterest.ktlint.core.ast.prevLeaf
import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement

/**
* Ensures consistent spacing inside the parameter list. This rule partly overlaps with other rules like spacing around
* comma's and colons. However, it does have a more complete view on the higher concept of the parameter-list without
* interfering of the parameter-list-wrapping rule.
*/
public class ParameterListSpacingRule : Rule("parameter-list-spacing") {
override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == VALUE_PARAMETER_LIST) {
visitValueParameterList(node, emit, autoCorrect)
}
}

private fun visitValueParameterList(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
require(node.elementType == VALUE_PARAMETER_LIST)
val countValueParameters =
node
.children()
.count { it.elementType == VALUE_PARAMETER }
var valueParameterCount = 0
val iterator =
node
.children()
// Store elements in list before changing them as otherwise only one element is being changed
.toList()
.iterator()
while (iterator.hasNext()) {
val el = iterator.next()
when (el.elementType) {
WHITE_SPACE -> {
if (countValueParameters == 0) {
removeUnexpectedWhiteSpace(el, emit, autoCorrect)
} else if (valueParameterCount == 0 && el.isNotIndent()) {
// whitespace before first parameter
removeUnexpectedWhiteSpace(el, emit, autoCorrect)
} else if (valueParameterCount == countValueParameters && el.isNotIndent()) {
// whitespace after the last parameter
removeUnexpectedWhiteSpace(el, emit, autoCorrect)
} else if (el.nextCodeSibling()?.elementType == COMMA) {
// No whitespace between parameter name and comma allowed
removeUnexpectedWhiteSpace(el, emit, autoCorrect)
} else if (el.elementType == WHITE_SPACE && el.isNotIndent() && el.isNotSingleSpace()) {
require(el.prevCodeSibling()?.elementType == COMMA)
replaceWithSingleSpace(el, emit, autoCorrect)
}
}
COMMA -> {
// Comma must be followed by whitespace
val nextSibling =
el
.nextSibling { true }
?.elementType
if (nextSibling != WHITE_SPACE) {
addMissingWhiteSpaceAfterMe(el, emit, autoCorrect)
}
}
VALUE_PARAMETER -> {
valueParameterCount += 1
visitValueParameter(el, emit, autoCorrect)
}
}
}
}

private fun visitValueParameter(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
visitModifierList(node, emit, autoCorrect)
removeWhiteSpaceBetweenParameterIdentifierAndColon(node, emit, autoCorrect)
fixWhiteSpaceAfterColonInParameter(node, emit, autoCorrect)
}

private fun visitModifierList(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
val modifierList = node.findChildByType(MODIFIER_LIST) ?: return
removeWhiteSpaceBetweenModifiersInList(modifierList, emit, autoCorrect)
removeWhiteSpaceBetweenModifierListAndParameterIdentifier(modifierList, emit, autoCorrect)
}

private fun removeWhiteSpaceBetweenModifiersInList(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
require(node.elementType == MODIFIER_LIST)
node
.children()
.filter { it.elementType == WHITE_SPACE }
// Store elements in list before changing them as otherwise only the first whitespace is being changed
.toList()
.forEach { visitWhiteSpaceAfterModifier(it, emit, autoCorrect) }
}

private fun visitWhiteSpaceAfterModifier(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
node
.takeUnless {
// Ignore when the modifier is an annotation which is placed on a separate line
it.isIndent() && it.getPrecedingModifier()?.elementType == ANNOTATION_ENTRY
}
?.takeIf { it.isNotSingleSpace() }
?.let { replaceWithSingleSpace(it, emit, autoCorrect) }
}

private fun removeWhiteSpaceBetweenModifierListAndParameterIdentifier(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
require(node.elementType == MODIFIER_LIST)
node
.nextSibling { true }
?.takeIf { it.elementType == WHITE_SPACE }
?.let { visitWhiteSpaceAfterModifier(it, emit, autoCorrect) }
}

private fun removeWhiteSpaceBetweenParameterIdentifierAndColon(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
node
.findChildByType(COLON)
?.prevLeaf()
?.takeIf { it.elementType == WHITE_SPACE }
?.let { whiteSpaceBeforeColon ->
removeUnexpectedWhiteSpace(whiteSpaceBeforeColon, emit, autoCorrect)
}
}

private fun fixWhiteSpaceAfterColonInParameter(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
val colonNode = node.findChildByType(COLON) ?: return
colonNode
.nextLeaf()
?.takeIf { it.elementType == WHITE_SPACE }
.let { whiteSpaceAfterColon ->
if (whiteSpaceAfterColon == null) {
addMissingWhiteSpaceAfterMe(colonNode, emit, autoCorrect)
} else if (whiteSpaceAfterColon.isIndent() || whiteSpaceAfterColon.isNotSingleSpace()) {
replaceWithSingleSpace(whiteSpaceAfterColon, emit, autoCorrect)
}
}
}

private fun addMissingWhiteSpaceAfterMe(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
require(node.elementType == COLON || node.elementType == COMMA)
emit(node.startOffset, "Whitespace after '${node.text}' is missing", true)
if (autoCorrect) {
(node as LeafElement).upsertWhitespaceAfterMe(" ")
}
}

private fun ASTNode.isNotIndent(): Boolean = !isIndent()

private fun ASTNode.isIndent(): Boolean {
require(elementType == WHITE_SPACE)
return text.startsWith("\n")
}

private fun ASTNode.isNotSingleSpace(): Boolean {
require(elementType == WHITE_SPACE)
return text != " "
}

private fun removeUnexpectedWhiteSpace(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
emit(node.startOffset, "Unexpected whitespace", true)
if (autoCorrect) {
(node as LeafElement).rawRemove()
}
}

private fun replaceWithSingleSpace(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
emit(node.startOffset, "Expected a single space", true)
if (autoCorrect) {
(node as LeafPsiElement).replaceWithText(" ")
}
}

private fun ASTNode.getPrecedingModifier(): ASTNode? {
return prevCodeSibling()
?.let { prevCodeSibling ->
if (prevCodeSibling.elementType == MODIFIER_LIST) {
prevCodeSibling.lastChildNode
} else {
require(prevCodeSibling.treeParent.elementType == MODIFIER_LIST)
prevCodeSibling
}
}
}
}
Loading

0 comments on commit 07e1828

Please sign in to comment.