Skip to content

Commit

Permalink
Create rule+tests to lint/format annotations for members/types
Browse files Browse the repository at this point in the history
  • Loading branch information
cable729 committed Oct 25, 2018
1 parent c52ba69 commit ca731f1
Show file tree
Hide file tree
Showing 2 changed files with 343 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.github.shyiko.ktlint.ruleset.standard

import com.github.shyiko.ktlint.core.Rule
import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.psiUtil.children
import org.jetbrains.kotlin.psi.psiUtil.startOffset

class MemberOrTypeAnnotationRule : Rule("member-or-type-annotation") {

companion object {
const val multipleAnnotationsOnSameLineAsAnnotatedConstructErrorMessage =
"Multiple annotations should not be placed on the same line as the annotated construct"
const val annotationsWithParametersAreNotOnSeparateLinesErrorMessage =
"Annotations with parameters should all be placed on separate lines prior to the annotated construct"
}

override fun visit(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
val root = node.children().firstOrNull { it.elementType == KtNodeTypes.MODIFIER_LIST }
?: return

val annotations = root.children()
.mapNotNull { it.psi as? KtAnnotationEntry }
.toList()
check(!annotations.isEmpty()) { "Annotations list should not be empty" }

// Join the nodes that immediately follow the annotations (whitespace), then add the final whitespace
// if it's not a child of root. This happens when a new line separates the annotations from the annotated
// construct. In the following example, there are no whitespace children of root, but root's next sibling is the
// new line whitespace.
//
// @JvmField
// val s: Any
//
val whiteSpaces = (annotations.asSequence().map { it.nextSibling } + root.treeNext)
.filterIsInstance<PsiWhiteSpaceImpl>()
.take(annotations.size)
.toList()

val multipleAnnotationsOnSameLineAsAnnotatedConstruct =
annotations.size > 1 && !whiteSpaces.last().textContains('\n')
val annotationsWithParametersAreNotOnSeparateLines =
annotations.any { it.valueArgumentList != null } &&
!whiteSpaces.all { it.textContains('\n') }

if (multipleAnnotationsOnSameLineAsAnnotatedConstruct) {
emit(
annotations.first().startOffset,
multipleAnnotationsOnSameLineAsAnnotatedConstructErrorMessage,
true
)
}
if (annotationsWithParametersAreNotOnSeparateLines) {
emit(
annotations.first().startOffset,
annotationsWithParametersAreNotOnSeparateLinesErrorMessage,
true
)
}

if (autoCorrect) {
val nodeBeforeAnnotations = root.treeParent.treePrev as? PsiWhiteSpace
// If there is no whitespace before the annotation, the annotation is the first
// text in the file
val newLineWithIndent = nodeBeforeAnnotations?.text ?: "\n"

if (annotationsWithParametersAreNotOnSeparateLines) {
whiteSpaces.forEach {
it.rawReplaceWithText(newLineWithIndent)
}
} else if (multipleAnnotationsOnSameLineAsAnnotatedConstruct) {
whiteSpaces.last().rawReplaceWithText(newLineWithIndent)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package com.github.shyiko.ktlint.ruleset.standard

import com.github.shyiko.ktlint.core.LintError
import com.github.shyiko.ktlint.test.format
import com.github.shyiko.ktlint.test.lint
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.Test

class MemberOrTypeAnnotationRuleTest {

@Test
fun `lint single annotation may be placed on line before annotated construct`() {
assertThat(
MemberOrTypeAnnotationRule().lint(
"""
@FunctionalInterface class A {
@JvmField
var x: String
}
""".trimIndent()
)
).isEmpty()
}

@Test
fun `format single annotation may be placed on line before annotated construct`() {
val code =
"""
@FunctionalInterface class A {
@JvmField
var x: String
}
""".trimIndent()
assertThat(MemberOrTypeAnnotationRule().format(code)).isEqualTo(code)
}

@Test
fun `lint single annotation may be placed on same line as annotated construct`() {
assertThat(
MemberOrTypeAnnotationRule().lint(
"""
@FunctionalInterface class A {
@JvmField var x: String
@Test fun myTest() {}
}
""".trimIndent()
)
).isEmpty()
}

@Test
fun `format single annotation may be placed on same line as annotated construct`() {
val code =
"""
@FunctionalInterface class A {
@JvmField var x: String
@Test fun myTest() {}
}
""".trimIndent()
assertThat(MemberOrTypeAnnotationRule().format(code)).isEqualTo(code)
}

@Test
fun `lint multiple annotations should not be placed on same line as annotated construct`() {
assertThat(
MemberOrTypeAnnotationRule().lint(
"""
class A {
@JvmField @Volatile var x: String
@JvmField @Volatile
var y: String
}
""".trimIndent()
)
).containsExactly(
LintError(
2, 5, "member-or-type-annotation",
MemberOrTypeAnnotationRule.multipleAnnotationsOnSameLineAsAnnotatedConstructErrorMessage
)
)
}

@Test
fun `format multiple annotations should not be placed on same line as annotated construct`() {
assertThat(
MemberOrTypeAnnotationRule().format(
"""
class A {
@JvmField @Volatile var x: String
@JvmField @Volatile
var y: String
}
""".trimIndent()
)
).isEqualTo(
"""
class A {
@JvmField @Volatile
var x: String
@JvmField @Volatile
var y: String
}
""".trimIndent()
)
}

@Test
fun `format multiple annotations should not be placed on same line as annotated construct (with no previous whitespace)`() {
assertThat(MemberOrTypeAnnotationRule().format("@JvmField @Volatile var x: String"))
.isEqualTo(
"""
@JvmField @Volatile
var x: String
""".trimIndent()
)
}

@Test
fun `format multiple annotations should not be placed on same line as annotated construct (with no previous indent)`() {
assertThat(
MemberOrTypeAnnotationRule().format(
"""
@JvmField @Volatile var x: String
""".trimIndent()
)
).isEqualTo(
"""
@JvmField @Volatile
var x: String
""".trimIndent()
)
}

@Test
fun `lint annotations with params should not be placed on same line before annotated construct`() {
assertThat(
MemberOrTypeAnnotationRule().lint(
"""
class A {
@JvmName("xJava") var x: String
@JvmName("yJava")
var y: String
}
""".trimIndent()
)
).containsExactly(
LintError(
2, 5, "member-or-type-annotation",
MemberOrTypeAnnotationRule.annotationsWithParametersAreNotOnSeparateLinesErrorMessage
)
)
}

@Test
fun `format annotations with params should not be placed on same line before annotated construct`() {
assertThat(
MemberOrTypeAnnotationRule().format(
"""
class A {
@JvmName("xJava") var x: String
@JvmName("yJava")
var y: String
}
""".trimIndent()
)
).isEqualTo(
"""
class A {
@JvmName("xJava")
var x: String
@JvmName("yJava")
var y: String
}
""".trimIndent()
)
}

@Test
fun `lint multiple annotations with params should not be placed on same line before annotated construct`() {
assertThat(
MemberOrTypeAnnotationRule().lint(
"""
@Retention(SOURCE) @Target(FUNCTION, PROPERTY_SETTER, FIELD) annotation class A
@Retention(SOURCE)
@Target(FUNCTION, PROPERTY_SETTER, FIELD)
annotation class B
""".trimIndent()
)
).containsExactly(
LintError(
1, 1, "member-or-type-annotation",
MemberOrTypeAnnotationRule.multipleAnnotationsOnSameLineAsAnnotatedConstructErrorMessage
),
LintError(
1, 1, "member-or-type-annotation",
MemberOrTypeAnnotationRule.annotationsWithParametersAreNotOnSeparateLinesErrorMessage
)
)
}

@Test
fun `format multiple annotations with params should not be placed on same line before annotated construct`() {
assertThat(
MemberOrTypeAnnotationRule().format(
"""
@Retention(SOURCE) @Target(FUNCTION, PROPERTY_SETTER, FIELD) annotation class A
@Retention(SOURCE)
@Target(FUNCTION, PROPERTY_SETTER, FIELD)
annotation class B
""".trimIndent()
)
).isEqualTo(
"""
@Retention(SOURCE)
@Target(FUNCTION, PROPERTY_SETTER, FIELD)
annotation class A
@Retention(SOURCE)
@Target(FUNCTION, PROPERTY_SETTER, FIELD)
annotation class B
""".trimIndent()
)
}

@Test
fun `lint annotation after keyword`() {
assertThat(
MemberOrTypeAnnotationRule().lint(
"""
class A {
private @Test fun myTest() {}
}
""".trimIndent()
)
).isEmpty()
}

@Test
fun `format annotation after keyword`() {
val code =
"""
class A {
private @Test fun myTest() {}
}
""".trimIndent()
assertThat(MemberOrTypeAnnotationRule().format(code)).isEqualTo(code)
}
}

0 comments on commit ca731f1

Please sign in to comment.