From cfa6414f26554ee3c04a6234fa7bd73c812ae991 Mon Sep 17 00:00:00 2001 From: paul-dingemans Date: Tue, 23 May 2023 18:22:41 +0200 Subject: [PATCH] Add new experimental rule `blank-line-before-declaration`. This rule requires a blank line before class, function or property declarations Closes #1939 --- CHANGELOG.md | 2 + .../snapshot/docs/rules/experimental.md | 50 +++ .../baseline/BaselineReporterProvider.kt | 1 + .../checkstyle/CheckStyleReporterProvider.kt | 1 + .../reporter/core/api/ReporterProviderV2.kt | 1 + .../cli/reporter/html/HtmlReporterProvider.kt | 1 + .../cli/reporter/json/JsonReporterProvider.kt | 1 + .../cli/reporter/sarif/SarifReporter.kt | 1 + .../pinterest/ktlint/core/ReporterProvider.kt | 1 + .../rule/engine/core/api/ASTNodeExtension.kt | 1 + .../engine/internal/KotlinPsiFileFactory.kt | 1 + .../engine/internal/PositionInTextLocator.kt | 1 + .../internal/rulefilter/RunAfterRuleFilter.kt | 1 + .../ktlint/rule/engine/api/KtLintTest.kt | 1 + .../engine/internal/RuleProviderSorterTest.kt | 1 + .../ThreadSafeEditorConfigCacheTest.kt | 1 + .../standard/StandardRuleSetProvider.kt | 2 + .../rules/BlankLineBeforeDeclarationRule.kt | 113 ++++++ .../BlankLineBeforeDeclarationRuleTest.kt | 346 ++++++++++++++++++ 19 files changed, 527 insertions(+) create mode 100644 ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBeforeDeclarationRule.kt create mode 100644 ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBeforeDeclarationRuleTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e13b00a42..df69350b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/). ### Added +* Add new experimental rule `blank-line-before-declaration`. This rule requires a blank line before class, function or property declarations ([#1939](https://github.com/pinterest/ktlint/issues/1939)) + ### Removed ### Fixed diff --git a/documentation/snapshot/docs/rules/experimental.md b/documentation/snapshot/docs/rules/experimental.md index af632fb4ea..f080eea89b 100644 --- a/documentation/snapshot/docs/rules/experimental.md +++ b/documentation/snapshot/docs/rules/experimental.md @@ -8,6 +8,56 @@ ktlint_experimental=enabled ``` Also see [enable/disable specific rules](../configuration-ktlint/#disabled-rules). +## Blank line before declarations + +Requires a blank line before any class or function declaration. No blank line is required between the class signature and the first declaration in the class. In a similar way, a blank line is required before any list of top level or class properties. No blank line is required before local properties or between consecutive properties. + +=== "[:material-heart:](#) Ktlint" + + ```kotlin + const val foo1 = "foo1" + + class FooBar { + val foo2 = "foo2" + val foo3 = "foo3" + + fun bar1() { + val foo4 = "foo4" + val foo5 = "foo5" + } + + fun bar2() = "bar" + + val foo6 = "foo3" + val foo7 = "foo4" + + enum class Foo {} + } + ``` + +=== "[:material-heart-off-outline:](#) Disallowed" + + ```kotlin + const val foo1 = "foo1" + class FooBar { + val foo2 = "foo2" + val foo3 = "foo3" + fun bar1() { + val foo4 = "foo4" + val foo5 = "foo5" + } + fun bar2() = "bar" + val foo6 = "foo3" + val foo7 = "foo4" + enum class Foo {} + } + ``` + +Rule id: `blank-line-before-declaration` (`standard` rule set) + +!!! Note +This rule is only run when `ktlint_code_style` is set to `ktlint_official` or when the rule is enabled explicitly. + ## Discouraged comment location Detect discouraged comment locations (no autocorrect). diff --git a/ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/BaselineReporterProvider.kt b/ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/BaselineReporterProvider.kt index d508be4087..94f359b981 100644 --- a/ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/BaselineReporterProvider.kt +++ b/ktlint-cli-reporter-baseline/src/main/kotlin/com/pinterest/ktlint/cli/reporter/baseline/BaselineReporterProvider.kt @@ -5,6 +5,7 @@ import java.io.PrintStream public class BaselineReporterProvider : ReporterProviderV2 { override val id: String = "baseline" + override fun get( out: PrintStream, opt: Map, diff --git a/ktlint-cli-reporter-checkstyle/src/main/kotlin/com/pinterest/ktlint/cli/reporter/checkstyle/CheckStyleReporterProvider.kt b/ktlint-cli-reporter-checkstyle/src/main/kotlin/com/pinterest/ktlint/cli/reporter/checkstyle/CheckStyleReporterProvider.kt index ebfadd6fff..fbfa7ac60f 100644 --- a/ktlint-cli-reporter-checkstyle/src/main/kotlin/com/pinterest/ktlint/cli/reporter/checkstyle/CheckStyleReporterProvider.kt +++ b/ktlint-cli-reporter-checkstyle/src/main/kotlin/com/pinterest/ktlint/cli/reporter/checkstyle/CheckStyleReporterProvider.kt @@ -5,6 +5,7 @@ import java.io.PrintStream public class CheckStyleReporterProvider : ReporterProviderV2 { override val id: String = "checkstyle" + override fun get( out: PrintStream, opt: Map, diff --git a/ktlint-cli-reporter-core/src/main/kotlin/com/pinterest/ktlint/cli/reporter/core/api/ReporterProviderV2.kt b/ktlint-cli-reporter-core/src/main/kotlin/com/pinterest/ktlint/cli/reporter/core/api/ReporterProviderV2.kt index aa7b1ca215..af2c5ed49b 100644 --- a/ktlint-cli-reporter-core/src/main/kotlin/com/pinterest/ktlint/cli/reporter/core/api/ReporterProviderV2.kt +++ b/ktlint-cli-reporter-core/src/main/kotlin/com/pinterest/ktlint/cli/reporter/core/api/ReporterProviderV2.kt @@ -11,6 +11,7 @@ import java.io.Serializable */ public interface ReporterProviderV2 : Serializable { public val id: String + public fun get( out: PrintStream, opt: Map, diff --git a/ktlint-cli-reporter-html/src/main/kotlin/com/pinterest/ktlint/cli/reporter/html/HtmlReporterProvider.kt b/ktlint-cli-reporter-html/src/main/kotlin/com/pinterest/ktlint/cli/reporter/html/HtmlReporterProvider.kt index 80b32fde41..90036a6f09 100644 --- a/ktlint-cli-reporter-html/src/main/kotlin/com/pinterest/ktlint/cli/reporter/html/HtmlReporterProvider.kt +++ b/ktlint-cli-reporter-html/src/main/kotlin/com/pinterest/ktlint/cli/reporter/html/HtmlReporterProvider.kt @@ -29,6 +29,7 @@ import java.io.PrintStream public class HtmlReporterProvider : ReporterProviderV2 { override val id: String = "html" + override fun get( out: PrintStream, opt: Map, diff --git a/ktlint-cli-reporter-json/src/main/kotlin/com/pinterest/ktlint/cli/reporter/json/JsonReporterProvider.kt b/ktlint-cli-reporter-json/src/main/kotlin/com/pinterest/ktlint/cli/reporter/json/JsonReporterProvider.kt index 8ab1f8cb54..5ea3512152 100644 --- a/ktlint-cli-reporter-json/src/main/kotlin/com/pinterest/ktlint/cli/reporter/json/JsonReporterProvider.kt +++ b/ktlint-cli-reporter-json/src/main/kotlin/com/pinterest/ktlint/cli/reporter/json/JsonReporterProvider.kt @@ -5,6 +5,7 @@ import java.io.PrintStream public class JsonReporterProvider : ReporterProviderV2 { override val id: String = "json" + override fun get( out: PrintStream, opt: Map, diff --git a/ktlint-cli-reporter-sarif/src/main/kotlin/com/pinterest/ktlint/cli/reporter/sarif/SarifReporter.kt b/ktlint-cli-reporter-sarif/src/main/kotlin/com/pinterest/ktlint/cli/reporter/sarif/SarifReporter.kt index 330cfb7928..1e52241b22 100644 --- a/ktlint-cli-reporter-sarif/src/main/kotlin/com/pinterest/ktlint/cli/reporter/sarif/SarifReporter.kt +++ b/ktlint-cli-reporter-sarif/src/main/kotlin/com/pinterest/ktlint/cli/reporter/sarif/SarifReporter.kt @@ -23,6 +23,7 @@ import kotlin.io.path.pathString import kotlin.io.path.relativeToOrSelf private const val SRCROOT = "%SRCROOT%" + internal fun String.sanitize(): String = this.replace(File.separatorChar, '/') .let { diff --git a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ReporterProvider.kt b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ReporterProvider.kt index 5773899b5d..935584fa8b 100644 --- a/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ReporterProvider.kt +++ b/ktlint-core/src/main/kotlin/com/pinterest/ktlint/core/ReporterProvider.kt @@ -12,6 +12,7 @@ import java.io.Serializable @Deprecated("Deprecated since ktlint 0.49.0. Custom reporters have to be migrated to ReporterV2. See changelog 0.49.") public interface ReporterProvider : Serializable { public val id: String + public fun get( out: PrintStream, opt: Map, diff --git a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/ASTNodeExtension.kt b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/ASTNodeExtension.kt index 8563bab456..47f7d5a7e6 100644 --- a/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/ASTNodeExtension.kt +++ b/ktlint-rule-engine-core/src/main/kotlin/com/pinterest/ktlint/rule/engine/core/api/ASTNodeExtension.kt @@ -235,6 +235,7 @@ public fun ASTNode?.isWhiteSpaceWithNewline(): Boolean = this != null && element public fun ASTNode?.isWhiteSpaceWithoutNewline(): Boolean = this != null && elementType == WHITE_SPACE && !textContains('\n') public fun ASTNode.isRoot(): Boolean = elementType == ElementType.FILE + public fun ASTNode.isLeaf(): Boolean = firstChildNode == null /** diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/KotlinPsiFileFactory.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/KotlinPsiFileFactory.kt index 52b7224050..3bd694f0dd 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/KotlinPsiFileFactory.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/KotlinPsiFileFactory.kt @@ -103,6 +103,7 @@ private class LoggerFactory : DiagnosticLogger.Factory { message: String?, t: Throwable?, ) {} + override fun error( message: String?, vararg details: String?, diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/PositionInTextLocator.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/PositionInTextLocator.kt index 3f501e9ced..5f96bbf34a 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/PositionInTextLocator.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/PositionInTextLocator.kt @@ -49,6 +49,7 @@ private class SegmentTree( } fun get(i: Int): Segment = segments[i] + fun indexOf(v: Int): Int = binarySearch(v, 0, segments.size - 1) private fun binarySearch( diff --git a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilter.kt b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilter.kt index 1271978c37..e8dcc49428 100644 --- a/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilter.kt +++ b/ktlint-rule-engine/src/main/kotlin/com/pinterest/ktlint/rule/engine/internal/rulefilter/RunAfterRuleFilter.kt @@ -166,6 +166,7 @@ internal class RunAfterRuleFilter : RuleFilter { } }.toSet() } + private fun RuleProvider.canRunWith(loadedRuleIds: Set): Boolean = this .runAfterRules diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintTest.kt index 13bf1476ed..cea02f93a4 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/api/KtLintTest.kt @@ -609,6 +609,7 @@ private data class RuleExecutionCall( val classIdentifier: String? = null, ) { enum class RuleMethod { BEFORE_FIRST, BEFORE_CHILDREN, VISIT, AFTER_CHILDREN, AFTER_LAST } + enum class VisitNodeType { ROOT, CHILD } } diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorterTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorterTest.kt index 91d4152cf1..e9db9eb535 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorterTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/RuleProviderSorterTest.kt @@ -455,6 +455,7 @@ class RuleProviderSorterTest { visitorModifiers, ) { constructor(ruleId: RuleId, visitorModifier: VisitorModifier) : this(ruleId, setOf(visitorModifier)) + override fun beforeVisitChildNodes( node: ASTNode, autoCorrect: Boolean, diff --git a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt index ca45cac1c6..a28a6f8b64 100644 --- a/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt +++ b/ktlint-rule-engine/src/test/kotlin/com/pinterest/ktlint/rule/engine/internal/ThreadSafeEditorConfigCacheTest.kt @@ -86,6 +86,7 @@ class ThreadSafeEditorConfigCacheTest { const val SOME_PROPERTY = "some-property" private fun String.resource() = Resource.Resources.ofPath(Paths.get(this), StandardCharsets.UTF_8) + val FILE_1: Resource = "/some/path/to/file/1".resource() val FILE_2: Resource = "/some/path/to/file/2".resource() diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt index 04c51c4aeb..5d14f8b16c 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/StandardRuleSetProvider.kt @@ -6,6 +6,7 @@ import com.pinterest.ktlint.rule.engine.core.api.RuleSetId import com.pinterest.ktlint.ruleset.standard.rules.AnnotationRule import com.pinterest.ktlint.ruleset.standard.rules.AnnotationSpacingRule import com.pinterest.ktlint.ruleset.standard.rules.ArgumentListWrappingRule +import com.pinterest.ktlint.ruleset.standard.rules.BlankLineBeforeDeclarationRule import com.pinterest.ktlint.ruleset.standard.rules.BlockCommentInitialStarAlignmentRule import com.pinterest.ktlint.ruleset.standard.rules.ChainWrappingRule import com.pinterest.ktlint.ruleset.standard.rules.ClassNamingRule @@ -88,6 +89,7 @@ public class StandardRuleSetProvider : RuleProvider { AnnotationRule() }, RuleProvider { AnnotationSpacingRule() }, RuleProvider { ArgumentListWrappingRule() }, + RuleProvider { BlankLineBeforeDeclarationRule() }, RuleProvider { BlockCommentInitialStarAlignmentRule() }, RuleProvider { ChainWrappingRule() }, RuleProvider { ClassNamingRule() }, diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBeforeDeclarationRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBeforeDeclarationRule.kt new file mode 100644 index 0000000000..aaa5e1979e --- /dev/null +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBeforeDeclarationRule.kt @@ -0,0 +1,113 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.ElementType +import com.pinterest.ktlint.rule.engine.core.api.ElementType.BLOCK +import com.pinterest.ktlint.rule.engine.core.api.ElementType.CLASS +import com.pinterest.ktlint.rule.engine.core.api.ElementType.FUN +import com.pinterest.ktlint.rule.engine.core.api.ElementType.LBRACE +import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.ElementType.PROPERTY_ACCESSOR +import com.pinterest.ktlint.rule.engine.core.api.Rule +import com.pinterest.ktlint.rule.engine.core.api.RuleId +import com.pinterest.ktlint.rule.engine.core.api.indent +import com.pinterest.ktlint.rule.engine.core.api.isPartOfComment +import com.pinterest.ktlint.rule.engine.core.api.isWhiteSpace +import com.pinterest.ktlint.rule.engine.core.api.nextCodeSibling +import com.pinterest.ktlint.rule.engine.core.api.prevCodeSibling +import com.pinterest.ktlint.rule.engine.core.api.upsertWhitespaceBeforeMe +import com.pinterest.ktlint.ruleset.standard.StandardRule +import org.jetbrains.kotlin.com.intellij.lang.ASTNode +import org.jetbrains.kotlin.psi.psiUtil.siblings + +/** + * Insert a blank line before declarations. No blank line is inserted before between the class signature and the first declaration in the + * class. Also, no blank lines are inserted between consecutive properties. + */ +public class BlankLineBeforeDeclarationRule : + StandardRule("blank-line-before-declaration"), + Rule.Experimental, + Rule.OfficialCodeStyle { + override fun beforeVisitChildNodes( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + when (node.elementType) { + CLASS, + FUN, + PROPERTY, + PROPERTY_ACCESSOR, + -> + visitDeclaration(node, autoCorrect, emit) + } + } + + private fun visitDeclaration( + node: ASTNode, + autoCorrect: Boolean, + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) { + if (node == node.firstCodeSiblingInClassBodyOrNull()) { + // Allow missing blank line between class signature and first code sibling in class body: + // Class Foo { + // fun bar() {} + // } + return + } + + if (node.isConsecutiveProperty()) { + // Allow consecutive properties: + // val foo = "foo" + // val bar = "bar" + return + } + + if (node.isLocalProperty()) { + // Allow: + // fun foo() { + // bar() + // val foobar = "foobar" + // } + return + } + + node + .siblings(false) + .takeWhile { it.isWhiteSpace() || it.isPartOfComment() } + .lastOrNull() + ?.let { previous -> + when { + !previous.isWhiteSpace() -> previous + !previous.text.startsWith("\n\n") -> node + else -> null + }?.let { insertBeforeNode -> + emit(insertBeforeNode.startOffset, "Expected a blank line for this declaration", true) + if (autoCorrect) { + insertBeforeNode.upsertWhitespaceBeforeMe("\n".plus(node.indent())) + } + } + } + } + + private fun ASTNode.firstCodeSiblingInClassBodyOrNull() = + treeParent + .takeIf { it.elementType == ElementType.CLASS_BODY } + ?.findChildByType(LBRACE) + ?.nextCodeSibling() + + private fun ASTNode.isConsecutiveProperty() = + takeIf { it.propertyRelated() } + ?.prevCodeSibling() + ?.let { it.propertyRelated() || it.treeParent.propertyRelated() } + ?: false + + private fun ASTNode.isLocalProperty() = + takeIf { it.propertyRelated() } + ?.treeParent + ?.let { it.elementType == BLOCK } + ?: false + + private fun ASTNode.propertyRelated() = elementType == PROPERTY || elementType == PROPERTY_ACCESSOR +} + +public val BLANK_LINE_BEFORE_DECLARATION_RULE_ID: RuleId = BlankLineBeforeDeclarationRule().ruleId diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBeforeDeclarationRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBeforeDeclarationRuleTest.kt new file mode 100644 index 0000000000..b9dbe531ea --- /dev/null +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/rules/BlankLineBeforeDeclarationRuleTest.kt @@ -0,0 +1,346 @@ +package com.pinterest.ktlint.ruleset.standard.rules + +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CODE_STYLE_PROPERTY +import com.pinterest.ktlint.rule.engine.core.api.editorconfig.CodeStyleValue +import com.pinterest.ktlint.test.KtLintAssertThat +import com.pinterest.ktlint.test.LintViolation +import org.junit.jupiter.api.Test + +class BlankLineBeforeDeclarationRuleTest { + private val blankLineBeforeDeclarationRuleAssertThat = + KtLintAssertThat.assertThatRule( + provider = { BlankLineBeforeDeclarationRule() }, + editorConfigProperties = setOf(CODE_STYLE_PROPERTY to CodeStyleValue.ktlint_official), + ) + + @Test + fun `Given some consecutive classes not separated by a blank line then insert a blank line in between`() { + val code = + """ + class Foo + class Bar + """.trimIndent() + val formattedCode = + """ + class Foo + + class Bar + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given some consecutive classes separated with an annotation before the second class then insert a blank line before the annotation`() { + val code = + """ + class Foo + @FooBar + class Bar + """.trimIndent() + val formattedCode = + """ + class Foo + + @FooBar + class Bar + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given some consecutive classes separated with a kdoc before the second class then insert a blank line before the kdoc`() { + val code = + """ + class Foo + /** + * Some KDOC + */ + class Bar + """.trimIndent() + val formattedCode = + """ + class Foo + + /** + * Some KDOC + */ + class Bar + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given some consecutive classes separated with a block comment before the second class then insert a blank line before the kdoc`() { + val code = + """ + class Foo + /* + * Some comment + */ + class Bar + """.trimIndent() + val formattedCode = + """ + class Foo + + /* + * Some comment + */ + class Bar + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given some consecutive classes separated with EOL comments before the second class then insert a blank line before the EOL-comments`() { + val code = + """ + class Foo + // Some comment 1 + // Some comment 2 + class Bar + """.trimIndent() + val formattedCode = + """ + class Foo + + // Some comment 1 + // Some comment 2 + class Bar + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given some consecutive functions not separated by a blank line then insert a blank line in between`() { + val code = + """ + fun foo() {} + fun bar() {} + """.trimIndent() + val formattedCode = + """ + fun foo() {} + + fun bar() {} + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given some consecutive functions separated with an annotation before the second function then insert a blank line before the annotation`() { + val code = + """ + fun foo() {} + @FooBar + fun bar() {} + """.trimIndent() + val formattedCode = + """ + fun foo() {} + + @FooBar + fun bar() {} + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given some consecutive functions separated with a kdoc before the second function then insert a blank line before the annotation`() { + val code = + """ + fun foo() {} + /** + * Some KDOC + */ + fun bar() {} + """.trimIndent() + val formattedCode = + """ + fun foo() {} + + /** + * Some KDOC + */ + fun bar() {} + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given some consecutive functions separated with a block comment before the second function then insert a blank line before the annotation`() { + val code = + """ + fun foo() {} + /* + * Some KDOC + */ + fun bar() {} + """.trimIndent() + val formattedCode = + """ + fun foo() {} + + /* + * Some KDOC + */ + fun bar() {} + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given some consecutive functions separated with EOL comments before the second function then insert a blank line before the annotation`() { + val code = + """ + fun foo() {} + // Some comment 1 + // Some comment 2 + fun bar() {} + """.trimIndent() + val formattedCode = + """ + fun foo() {} + + // Some comment 1 + // Some comment 2 + fun bar() {} + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolation(2, 1, "Expected a blank line for this declaration") + .isFormattedAs(formattedCode) + } + + @Test + fun `Given a kotlin script with some consecutive functions separated with EOL comments before the second function then insert a blank line before the annotation`() { + val code = + """ + tasks.withType().configureEach {} + + // Some comment 1 + // Some comment 2 + tasks.withType().configureEach {} + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .asKotlinScript() + .hasNoLintViolations() + } + + @Test + fun `Given a function as first code sibling inside a class body then do not insert a blank line between the class signature and this function`() { + val code = + """ + class Foo { + fun Bar() {} + } + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given some consecutive properties not separated by a blank line then do not insert a blank line in between`() { + val code = + """ + val foo1 = "foo1" + val foo2: String + get() = "foo2" + var foo3: String = "foo3" + set(value) { + field = value.repeat(2) + } + var foo4 = "foo4" + var foo5: String by Delegate() + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given a function containing a property declaration after a statement then do not insert a blank line before the declaration`() { + val code = + """ + fun foo() { + bar() + val bar = "bar" + } + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `xx`() { + val code = + """ + fun foo( + emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit, + ) {} + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Given some consecutive declarations`() { + val code = + """ + const val foo1 = "foo1" + class FooBar { + val foo2 = "foo2" + val foo3 = "foo3" + fun bar1() { + val foo4 = "foo4" + val foo5 = "foo5" + } + fun bar2() = "bar" + val foo6 = "foo3" + val foo7 = "foo4" + enum class Foo {} + } + """.trimIndent() + val formattedCode = + """ + const val foo1 = "foo1" + + class FooBar { + val foo2 = "foo2" + val foo3 = "foo3" + + fun bar1() { + val foo4 = "foo4" + val foo5 = "foo5" + } + + fun bar2() = "bar" + + val foo6 = "foo3" + val foo7 = "foo4" + + enum class Foo {} + } + """.trimIndent() + blankLineBeforeDeclarationRuleAssertThat(code) + .hasLintViolations( + LintViolation(2, 1, "Expected a blank line for this declaration"), + LintViolation(5, 5, "Expected a blank line for this declaration"), + LintViolation(9, 5, "Expected a blank line for this declaration"), + LintViolation(10, 5, "Expected a blank line for this declaration"), + LintViolation(12, 5, "Expected a blank line for this declaration"), + ).isFormattedAs(formattedCode) + } +}