From 33b22a2e61a4831f1eb6804cb6e0ee36f639cf5f Mon Sep 17 00:00:00 2001 From: Toshiaki Kameyama Date: Thu, 12 Mar 2020 18:28:03 +0900 Subject: [PATCH] Add file annotations rule --- .../com/pinterest/ktlint/core/ast/package.kt | 5 + .../ruleset/experimental/AnnotationRule.kt | 34 +- .../experimental/AnnotationRuleTest.kt | 293 +++++++++++++++++- .../com/pinterest/ktlint/test/DumpAST.kt | 4 +- .../main/kotlin/com/pinterest/ktlint/Main.kt | 1 + 5 files changed, 325 insertions(+), 12 deletions(-) diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ast/package.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ast/package.kt index d54a50b9fa..16fc972052 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ast/package.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ast/package.kt @@ -184,6 +184,8 @@ fun ASTNode.isPartOf(klass: KClass): Boolean { fun ASTNode.isPartOfString() = parent(STRING_TEMPLATE, strict = false) != null +fun ASTNode?.isWhiteSpace() = + this != null && elementType == WHITE_SPACE fun ASTNode?.isWhiteSpaceWithNewline() = this != null && elementType == WHITE_SPACE && textContains('\n') fun ASTNode?.isWhiteSpaceWithoutNewline() = @@ -228,3 +230,6 @@ fun ASTNode.visit(enter: (node: ASTNode) -> Unit, exit: (node: ASTNode) -> Unit) this.getChildren(null).forEach { it.visit(enter, exit) } exit(this) } + +fun ASTNode.lineNumber(): Int? = + this.psi.containingFile?.viewProvider?.document?.getLineNumber(this.startOffset)?.let { it + 1 } diff --git a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRule.kt b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRule.kt index 2fc82fb2f8..d59fe27d05 100644 --- a/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRule.kt +++ b/ktlint-ruleset-experimental/src/main/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRule.kt @@ -8,13 +8,20 @@ import com.pinterest.ktlint.core.ast.ElementType.VALUE_ARGUMENT import com.pinterest.ktlint.core.ast.ElementType.VALUE_PARAMETER import com.pinterest.ktlint.core.ast.children import com.pinterest.ktlint.core.ast.isPartOf +import com.pinterest.ktlint.core.ast.isPartOfComment +import com.pinterest.ktlint.core.ast.isWhiteSpace +import com.pinterest.ktlint.core.ast.lineNumber +import com.pinterest.ktlint.core.ast.nextSibling +import com.pinterest.ktlint.core.ast.prevSibling import com.pinterest.ktlint.core.ast.upsertWhitespaceBeforeMe import org.jetbrains.kotlin.com.intellij.lang.ASTNode import org.jetbrains.kotlin.com.intellij.psi.PsiComment import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.PsiWhiteSpaceImpl import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespaceAndComments import org.jetbrains.kotlin.psi.psiUtil.nextLeaf /** @@ -30,6 +37,7 @@ class AnnotationRule : Rule("annotation") { "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" + const val fileAnnotationsShouldBeSeparated = "File annotations should be separated from packages with a blank line" } override fun visit( @@ -62,7 +70,8 @@ class AnnotationRule : Rule("annotation") { .take(annotations.size) .toList() - val noWhiteSpaceAfterAnnotation = whiteSpaces.isEmpty() || whiteSpaces.last().nextSibling is KtAnnotationEntry + val noWhiteSpaceAfterAnnotation = node.elementType != FILE_ANNOTATION_LIST && + (whiteSpaces.isEmpty() || whiteSpaces.last().nextSibling is KtAnnotationEntry) if (noWhiteSpaceAfterAnnotation) { emit( annotations.last().endOffset - 1, @@ -106,6 +115,29 @@ class AnnotationRule : Rule("annotation") { } } } + + if (node.elementType == FILE_ANNOTATION_LIST) { + val lineNumber = node.lineNumber() + val next = node.nextSibling { + !it.isWhiteSpace() && it.textLength > 0 && !(it.isPartOfComment() && it.lineNumber() == lineNumber) + } + val nextLineNumber = next?.lineNumber() + if (lineNumber != null && nextLineNumber != null) { + val diff = nextLineNumber - lineNumber + if (diff < 2) { + emit(0, fileAnnotationsShouldBeSeparated, true) + if (autoCorrect) { + if (diff == 0) { + node.psi.getNextSiblingIgnoringWhitespaceAndComments(withItself = false)?.node + ?.prevSibling { it.isWhiteSpace() } + ?.let { (it as? LeafPsiElement)?.delete() } + next.treeParent.addChild(PsiWhiteSpaceImpl("\n"), next) + } + next.treeParent.addChild(PsiWhiteSpaceImpl("\n"), next) + } + } + } + } } private fun getNewlineWithIndent(modifierListRoot: ASTNode): String { diff --git a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRuleTest.kt b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRuleTest.kt index 355a17eaa4..53c1159d0d 100644 --- a/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRuleTest.kt +++ b/ktlint-ruleset-experimental/src/test/kotlin/com/pinterest/ktlint/ruleset/experimental/AnnotationRuleTest.kt @@ -467,9 +467,9 @@ class AnnotationRuleTest { @Path("bar") bar: String, @Body body: Foo ): Completable - + fun foo2(@Query("include") include: String? = null, @QueryMap fields: Map = emptyMap()): Single - + fun foo3(@Path("fooId") fooId: String): Completable } """.trimIndent() @@ -487,9 +487,9 @@ class AnnotationRuleTest { @Path("bar") bar: String, @Body body: Foo ): Completable - + fun foo2(@Query("include") include: String? = null, @QueryMap fields: Map = emptyMap()): Single - + fun foo3(@Path("fooId") fooId: String): Completable } """.trimIndent() @@ -557,10 +557,10 @@ class AnnotationRuleTest { val aProperty: Map<@Ann("test") Int, @JvmSuppressWildcards(true) (String) -> Int?> val bProperty: Map< @Ann String, - @Ann("test") Int, + @Ann("test") Int, @JvmSuppressWildcards(true) (String) -> Int? > - + fun doSomething() { funWithGenericsCall<@JvmSuppressWildcards(true) Int>() } @@ -575,10 +575,10 @@ class AnnotationRuleTest { val aProperty: Map<@Ann("test") Int, @JvmSuppressWildcards(true) (String) -> Int?> val bProperty: Map< @Ann String, - @Ann("test") Int, + @Ann("test") Int, @JvmSuppressWildcards(true) (String) -> Int? > - + fun doSomething() { funWithGenericsCall<@JvmSuppressWildcards(true) Int>() } @@ -597,8 +597,285 @@ class AnnotationRuleTest { ).isEqualTo( """ @file:JvmName("FooClass") + + package foo.bar + """.trimIndent() + ) + } + + @Test + fun `format file annotations should be separated with a blank line 1`() { + assertThat( + AnnotationRule().format( + """ + @file:JvmName package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + """ + @file:JvmName + package foo.bar + """.trimIndent() ) } + + @Test + fun `format file annotations should be separated with a blank line 2`() { + assertThat( + AnnotationRule().format( + """ + @file:JvmName + package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + """ + @file:JvmName + + package foo.bar + + """.trimIndent() + ) + } + + @Test + fun `format file annotations should be separated with a blank line 3`() { + assertThat( + AnnotationRule().format( + """ + @file:JvmName + fun foo() {} + + """.trimIndent() + ) + ).isEqualTo( + """ + @file:JvmName + + fun foo() {} + + """.trimIndent() + ) + } + + @Test + fun `format file annotations should be separated with a blank line 4`() { + assertThat( + AnnotationRule().format( + """ + @file:JvmName // comment + package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + """ + @file:JvmName // comment + + package foo.bar + + """.trimIndent() + ) + } + + @Test + fun `format file annotations should be separated with a blank line 5`() { + assertThat( + AnnotationRule().format( + """ + @file:JvmName /* comment */ package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + """ + @file:JvmName /* comment */ + + package foo.bar + + """.trimIndent() + ) + } + + @Test + fun `format file annotations should be separated with a blank line 6`() { + assertThat( + AnnotationRule().format( + """ + @file:JvmName + // comment + package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + """ + @file:JvmName + + // comment + package foo.bar + + """.trimIndent() + ) + } + + @Test + fun `lint file annotations should be separated with a blank line 1`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + listOf( + LintError(1, 1, "annotation", AnnotationRule.fileAnnotationsShouldBeSeparated) + ) + ) + } + + @Test + fun `lint file annotations should be separated with a blank line 2`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName + package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + listOf( + LintError(1, 1, "annotation", AnnotationRule.fileAnnotationsShouldBeSeparated) + ) + ) + } + + @Test + fun `lint file annotations should be separated with a blank line 3`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName + fun foo() {} + + """.trimIndent() + ) + ).isEqualTo( + listOf( + LintError(1, 1, "annotation", AnnotationRule.fileAnnotationsShouldBeSeparated) + ) + ) + } + + @Test + fun `lint file annotations should be separated with a blank line 4`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName // comment + package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + listOf( + LintError(1, 1, "annotation", AnnotationRule.fileAnnotationsShouldBeSeparated) + ) + ) + } + + @Test + fun `lint file annotations should be separated with a blank line 5`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName /* comment */ package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + listOf( + LintError(1, 1, "annotation", AnnotationRule.fileAnnotationsShouldBeSeparated) + ) + ) + } + + @Test + fun `lint file annotations should be separated with a blank line 6`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName + // comment + package foo.bar + + """.trimIndent() + ) + ).isEqualTo( + listOf( + LintError(1, 1, "annotation", AnnotationRule.fileAnnotationsShouldBeSeparated) + ) + ) + } + + @Test + fun `lint file annotations should be separated with a blank line 7`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName + + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint file annotations should be separated with a blank line 8`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName + + package foo.bar + + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint file annotations should be separated with a blank line 9`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName + + + package foo.bar + + """.trimIndent() + ) + ).isEmpty() + } + + @Test + fun `lint file annotations should be separated with a blank line 10`() { + assertThat( + AnnotationRule().lint( + """ + @file:JvmName + + fun foo() {} + + """.trimIndent() + ) + ).isEmpty() + } } diff --git a/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/DumpAST.kt b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/DumpAST.kt index b40b95eb34..1733f59f56 100644 --- a/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/DumpAST.kt +++ b/ktlint-test/src/main/kotlin/com/pinterest/ktlint/test/DumpAST.kt @@ -4,6 +4,7 @@ import com.pinterest.ktlint.core.Rule import com.pinterest.ktlint.core.ast.ElementType import com.pinterest.ktlint.core.ast.isRoot import com.pinterest.ktlint.core.ast.lastChildLeafOrSelf +import com.pinterest.ktlint.core.ast.lineNumber import com.pinterest.ktlint.test.internal.Color import com.pinterest.ktlint.test.internal.color import java.io.PrintStream @@ -84,9 +85,6 @@ class DumpAST @JvmOverloads constructor( return if (elementTypeSet.contains(name)) name else elementType.className + "." + elementType } - private fun ASTNode.lineNumber() = - this.psi.containingFile?.viewProvider?.document?.getLineNumber(this.startOffset)?.let { it + 1 } - private fun colorClassName(className: String): String { val name = className.substringAfterLast(".") return className.substring(0, className.length - name.length).dim() + name diff --git a/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt b/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt index ddeea606dc..a51429cfb9 100644 --- a/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt +++ b/ktlint/src/main/kotlin/com/pinterest/ktlint/Main.kt @@ -1,4 +1,5 @@ @file:JvmName("Main") + package com.pinterest.ktlint import com.pinterest.ktlint.core.KtLint