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 rule FunctionTypeReferenceSpacingRule (#1341) #1342

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)
}
}