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 SpacingAroundAngleBracketsRule #769

Merged
merged 10 commits into from
Jun 9, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ExperimentalRuleSetProvider : RuleSetProvider {
EnumEntryNameCaseRule(),
SpacingAroundDoubleColonRule(),
SpacingBetweenDeclarationsWithCommentsRule(),
SpacingBetweenDeclarationsWithAnnotationsRule()
SpacingBetweenDeclarationsWithAnnotationsRule(),
SpacingAroundAngleBracketsRule()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.core.Rule
import com.pinterest.ktlint.core.ast.ElementType.FUN_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.TYPE_ARGUMENT_LIST
import com.pinterest.ktlint.core.ast.ElementType.TYPE_PARAMETER_LIST
import com.pinterest.ktlint.core.ast.ElementType.VAL_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.VAR_KEYWORD
import com.pinterest.ktlint.core.ast.ElementType.WHITE_SPACE
import com.pinterest.ktlint.core.ast.isWhiteSpaceWithoutNewline
import com.pinterest.ktlint.core.ast.nextLeaf
import com.pinterest.ktlint.core.ast.prevLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafElement

class SpacingAroundAngleBracketsRule : Rule("spacing-around-angle-brackets") {
private fun String.trimBeforeLastLine() = this.substring(this.lastIndexOf('\n'))

override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType.let { it == TYPE_PARAMETER_LIST || it == TYPE_ARGUMENT_LIST }) {
val openingBracket = node.firstChildNode
if (openingBracket != null) {
// Check for rogue spacing before an opening bracket, e.g. Map <String, Int>
val beforeLeftAngle = openingBracket.prevLeaf()
if (beforeLeftAngle?.elementType == WHITE_SPACE) {
ymittal marked this conversation as resolved.
Show resolved Hide resolved
// Ignore when the whitespace is preceded by certain keywords, e.g. fun <T> func(arg: T) {}
if (!typesOkWithPrecedingWhitespace.contains(beforeLeftAngle.prevLeaf()?.elementType)) {
emit(beforeLeftAngle.startOffset, "Unexpected spacing before \"<\"", true)
if (autoCorrect) {
beforeLeftAngle.treeParent.removeChild(beforeLeftAngle)
}
}
}

// Check for rogue spacing after an opening bracket
val afterLeftAngle = openingBracket.nextLeaf()
if (afterLeftAngle?.elementType == WHITE_SPACE) {
if (afterLeftAngle.isWhiteSpaceWithoutNewline()) {
// when spacing does not include any new lines, e.g. Map< String, Int>
emit(afterLeftAngle.startOffset, "Unexpected spacing after \"<\"", true)
if (autoCorrect) {
afterLeftAngle.treeParent.removeChild(afterLeftAngle)
}
} else {
// when spacing contains at least one new line, e.g.
// SomeGenericType<[whitespace]
//
// String, Int, String>
// gets converted to
// SomeGenericType<
// String, Int, String>
val newLineWithIndent = afterLeftAngle.text.trimBeforeLastLine()
if (autoCorrect) {
(afterLeftAngle as LeafElement).rawReplaceWithText(newLineWithIndent)
}
}
}
}

val closingBracket = node.lastChildNode
if (closingBracket != null) {
val beforeRightAngle = closingBracket.prevLeaf()
// Check for rogue spacing before a closing bracket
if (beforeRightAngle?.elementType == WHITE_SPACE) {
if (beforeRightAngle.isWhiteSpaceWithoutNewline()) {
// when spacing does not include any new lines, e.g. Map<String, Int >
emit(beforeRightAngle.startOffset, "Unexpected spacing before \">\"", true)
if (autoCorrect) {
beforeRightAngle.treeParent.removeChild(beforeRightAngle)
}
} else {
// when spacing contains at least one new line, e.g.
// SomeGenericType<String, Int, String[whitespace]
//
// >
// gets converted to
// SomeGenericType<String, Int, String
// >
val newLineWithIndent = beforeRightAngle.text.trimBeforeLastLine()
if (autoCorrect) {
(beforeRightAngle as LeafElement).rawReplaceWithText(newLineWithIndent)
}
}
}
}
}
}

companion object {
private val typesOkWithPrecedingWhitespace = setOf(VAL_KEYWORD, VAR_KEYWORD, FUN_KEYWORD)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.pinterest.ktlint.ruleset.experimental

import com.pinterest.ktlint.test.diffFileFormat
import com.pinterest.ktlint.test.diffFileLint
import com.pinterest.ktlint.test.format
import com.pinterest.ktlint.test.lint
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class SpacingAroundAngleBracketRuleTest {

@Test
fun testLint() {
assertThat(SpacingAroundAngleBracketsRule().diffFileLint("spec/spacing-around-angle-brackets/lint.kt.spec")).isEmpty()
}

@Test
fun testFormat() {
assertThat(
SpacingAroundAngleBracketsRule().diffFileFormat(
"spec/spacing-around-angle-brackets/format.kt.spec",
"spec/spacing-around-angle-brackets/format-expected.kt.spec"
)
).isEmpty()
}

@Test
fun `lint keywords which allow preceding whitespace`() {
assertThat(
SpacingAroundAngleBracketsRule().lint(
"""
public class AngleTest<B : String> {
val <T> T.exhaustive get() = this;
fun <T> compare(other: T) {}
var <T> T.exhaustive: T get() = this;
}
""".trimIndent()
)
).isEmpty()
}

@Test
fun `format reified keyword within angle brackets`() {
assertThat(
SpacingAroundAngleBracketsRule().format(
"""
interface Consumer< reified T > {
fun add(item: T)
}
""".trimIndent()
)
).isEqualTo(
"""
interface Consumer<reified T> {
fun add(item: T)
}
""".trimIndent()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// TYPE_ARGUMENT_LIST
fun main() {
var a: Map<Int, String> = mapOf()
var b: Map<Int, String> = mapOf()
var c: Map<Int, String> = mapOf()

var d: Map<
Int, String
> = mapOf()

// Indentation would be fixed by another rule
var e: Map<
Int,
String
> = mapOf()

var nested: Map<
Int,
List<String>
> = mapOf()
}

// TYPE_PARAMETER_LIST
public class AngleTest<B : String> {}

public class AngleTest<
B : String,
C : Map<
Int, List<String>
>
> {
var a = 'str'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// TYPE_ARGUMENT_LIST
fun main() {
var a: Map< Int, String> = mapOf()
var b: Map<Int, String > = mapOf()
var c: Map <Int, String> = mapOf()

var d: Map<


Int, String

> = mapOf()

// Indentation would be fixed by another rule
var e: Map<
Int,
String
> = mapOf()

var nested: Map<
Int,
List < String >
> = mapOf()
}

// TYPE_PARAMETER_LIST
public class AngleTest< B : String > {}

public class AngleTest<

B : String,
C : Map <
Int, List< String >
>

> {
var a = 'str'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
fun main() {
var a: Map< Int, String> = mapOf()
var b: Map<Int, String > = mapOf()
var c: Map <Int, String> = mapOf()

var nested: Map<Int, List < String > > = mapOf()
}

public class AngleTest< B : String > {}

// expect
// 2:14:Unexpected spacing after "<"
// 3:25:Unexpected spacing before ">"
// 4:13:Unexpected spacing before "<"
// 6:28:Unexpected spacing before "<"
// 6:30:Unexpected spacing after "<"
// 6:39:Unexpected spacing before ">"
// 6:41:Unexpected spacing before ">"
// 9:24:Unexpected spacing after "<"
// 9:35:Unexpected spacing before ">"

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.