Skip to content

Commit

Permalink
Add rule FunctionTypeReferenceSpacingRule (#1342)
Browse files Browse the repository at this point in the history
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 committed Mar 8, 2022
1 parent ea8a05f commit 8e1c5a3
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Please welcome [paul-dingemans](https://github.com/paul-dingemans) as an officia
### Added
- Use Gradle JVM toolchain with language version 8 to compile the project
- 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))

### Fixed
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,21 @@ New rules will be added into the [experimental ruleset](https://github.com/pinte
by passing the `--experimental` flag to `ktlint`.

- `experimental:annotation`: Annotation formatting - multiple annotations should be on a separate line than the annotated declaration; annotations with parameters should each be on separate lines; annotations should be followed by a space
- ``experimental:annotation-spacing``: Annotations should be separated by the annotated declaration by a single line break
- `experimental:argument-list-wrapping`: Argument list wrapping
- `experimental:enum-entry-name-case`: Enum entry names should be uppercase underscore-separated names
- `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:spacing-around-angle-brackets`: No spaces around angle brackets
- `experimental:double-colon-spacing`: No spaces around `::`
- `experimental:unary-op-spacing`: No spaces around unary operators
- `experimental:unnecessary-parentheses-before-trailing-lambda`: An empty parentheses block before a lambda is redundant. For example `some-string".count() { it == '-' }`
### Spacing
- `experimental:annotation-spacing`: Annotations should be separated by the annotated declaration by a single line break
- `experimental:double-colon-spacing`: No spaces around `::`
- `experimental:function-type-reference-spacing`: Consistent spacing in the type reference before a function
- `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:unnecessary-parentheses-before-trailing-lambda`: An empty parentheses block before a lambda is redundant. For example `some-string".count() { it == '-' }`
## EditorConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class ExperimentalRuleSetProvider : RuleSetProvider {
SpacingAroundAngleBracketsRule(),
SpacingAroundUnaryOperatorRule(),
AnnotationSpacingRule(),
FunctionTypeReferenceSpacingRule(),
UnnecessaryParenthesesBeforeTrailingLambdaRule()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.FUN
import com.pinterest.ktlint.core.ast.ElementType.IDENTIFIER
import com.pinterest.ktlint.core.ast.ElementType.NULLABLE_TYPE
import com.pinterest.ktlint.core.ast.ElementType.TYPE_REFERENCE
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.nextSibling
import org.jetbrains.kotlin.com.intellij.lang.ASTNode

public class FunctionTypeReferenceSpacingRule : Rule("function-type-reference-spacing") {
override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == FUN) {
node
.findTypeReferenceBeforeFunctionIdentifier()
?.let { typeReference ->
typeReference
.firstChildNode
.takeIf { it.elementType == NULLABLE_TYPE }
?.let { nullableTypeElement ->
visitNodesUntilIdentifier(nullableTypeElement.firstChildNode, emit, autoCorrect)
}

if (typeReference.elementType != NULLABLE_TYPE) {
visitNodesUntilIdentifier(typeReference, emit, autoCorrect)
}
}
}
}

private fun ASTNode.findTypeReferenceBeforeFunctionIdentifier(): ASTNode? {
require(elementType == FUN)
var currentNode: ASTNode? = firstChildNode
while (currentNode != null && currentNode.elementType != IDENTIFIER) {
if (currentNode.elementType == TYPE_REFERENCE) {
return currentNode
}
currentNode = currentNode.nextSibling { true }
}
return null
}

private fun visitNodesUntilIdentifier(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
var currentNode: ASTNode? = node
while (currentNode != null && currentNode.elementType != IDENTIFIER) {
val nextNode = currentNode.nextSibling { true }
removeIfNonEmptyWhiteSpace(currentNode, emit, autoCorrect)
currentNode = nextNode
}
}

private fun removeIfNonEmptyWhiteSpace(
node: ASTNode,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit,
autoCorrect: Boolean
) {
if (node.elementType == WHITE_SPACE && node.text.isNotEmpty()) {
emit(node.startOffset, "Unexpected whitespace", true)
if (autoCorrect) {
node.treeParent.removeChild(node)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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
import org.junit.jupiter.api.Test

class NoSpacingAfterTypeReferenceRuleTest {
@Test
fun `Given a function signature with whitespace after a non nullable type reference of an extension function then remove this whitespace`() {
val code =
"""
fun String .foo1() = "some-result"
fun String
.foo2() = "some-result"
""".trimIndent()
val formattedCode =
"""
fun String.foo1() = "some-result"
fun String.foo2() = "some-result"
""".trimIndent()
Assertions.assertThat(
FunctionTypeReferenceSpacingRule().lint(code)
).containsExactly(
LintError(1, 11, "function-type-reference-spacing", "Unexpected whitespace"),
LintError(2, 11, "function-type-reference-spacing", "Unexpected whitespace")
)
Assertions.assertThat(
FunctionTypeReferenceSpacingRule().format(code)
).isEqualTo(formattedCode)
}

@Test
fun `Given a function signature with whitespace in a nullable type reference of an extension function`() {
val code =
"""
fun String ?.foo1() = "some-result"
fun String
?.foo2() = "some-result"
""".trimIndent()
val formattedCode =
"""
fun String?.foo1() = "some-result"
fun String?.foo2() = "some-result"
""".trimIndent()
Assertions.assertThat(
FunctionTypeReferenceSpacingRule().lint(code)
).containsExactly(
LintError(1, 11, "function-type-reference-spacing", "Unexpected whitespace"),
LintError(2, 11, "function-type-reference-spacing", "Unexpected whitespace")
)
Assertions.assertThat(
FunctionTypeReferenceSpacingRule().format(code)
).isEqualTo(formattedCode)
}

@Test
fun `Given a function signature with whitespace after a nullable type reference of an extension function`() {
val code =
"""
fun String? .foo1() = "some-result"
fun String?
.foo2() = "some-result"
""".trimIndent()
val formattedCode =
"""
fun String?.foo1() = "some-result"
fun String?.foo2() = "some-result"
""".trimIndent()
Assertions.assertThat(
FunctionTypeReferenceSpacingRule().lint(code)
).containsExactly(
LintError(1, 12, "function-type-reference-spacing", "Unexpected whitespace"),
LintError(2, 12, "function-type-reference-spacing", "Unexpected whitespace")
)
Assertions.assertThat(
FunctionTypeReferenceSpacingRule().format(code)
).isEqualTo(formattedCode)
}

@Test
fun `Given a function signature without a type reference before the function name then do not change the signature`() {
val code =
"""
fun foo1() = "some-result"
""".trimIndent()
Assertions.assertThat(
FunctionTypeReferenceSpacingRule().lint(code)
).isEmpty()
Assertions.assertThat(
FunctionTypeReferenceSpacingRule().format(code)
).isEqualTo(code)
}
}

0 comments on commit 8e1c5a3

Please sign in to comment.