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 15cd63b314..6f1b615fb7 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 @@ -156,6 +156,7 @@ public fun ASTNode.parent(elementType: IElementType, strict: Boolean = true): AS return null } +// TODO in ktlint 0.49 deprecate and replace with "ASTNode.parent(strict: Boolean = true, p: (ASTNode) -> Boolean): ASTNode?" public fun ASTNode.parent(p: (ASTNode) -> Boolean, strict: Boolean = true): ASTNode? { var n: ASTNode? = if (strict) this.treeParent else this while (n != null) { diff --git a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRule.kt b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRule.kt index 7089f3cd8b..37b337b03a 100644 --- a/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRule.kt +++ b/ktlint-ruleset-standard/src/main/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRule.kt @@ -1,11 +1,14 @@ package com.pinterest.ktlint.ruleset.standard import com.pinterest.ktlint.core.Rule +import com.pinterest.ktlint.core.ast.ElementType.CLASS_BODY +import com.pinterest.ktlint.core.ast.ElementType.ENUM_ENTRY import com.pinterest.ktlint.core.ast.ElementType.OBJECT_KEYWORD import com.pinterest.ktlint.core.ast.ElementType.SEMICOLON import com.pinterest.ktlint.core.ast.isWhiteSpace -import com.pinterest.ktlint.core.ast.nextCodeLeaf +import com.pinterest.ktlint.core.ast.lastChildLeafOrSelf import com.pinterest.ktlint.core.ast.nextLeaf +import com.pinterest.ktlint.core.ast.parent import com.pinterest.ktlint.core.ast.prevCodeLeaf import com.pinterest.ktlint.core.ast.prevLeaf import com.pinterest.ktlint.core.ast.upsertWhitespaceAfterMe @@ -16,7 +19,6 @@ import org.jetbrains.kotlin.kdoc.psi.api.KDoc import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtDoWhileExpression -import org.jetbrains.kotlin.psi.KtEnumEntry import org.jetbrains.kotlin.psi.KtIfExpression import org.jetbrains.kotlin.psi.KtLoopExpression import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType @@ -32,7 +34,7 @@ public class NoSemicolonsRule : Rule("no-semi") { } val nextLeaf = node.nextLeaf() val prevCodeLeaf = node.prevCodeLeaf() - if (nextLeaf.doesNotRequirePreSemi() && prevCodeLeaf.doesNotRequirePostSemi()) { + if (nextLeaf.doesNotRequirePreSemi() && isNoSemicolonRequiredAfter(node)) { emit(node.startOffset, "Unnecessary semicolon", true) if (autoCorrect) { val prevLeaf = node.prevLeaf(true) @@ -72,15 +74,16 @@ public class NoSemicolonsRule : Rule("no-semi") { return false } - private fun ASTNode?.doesNotRequirePostSemi(): Boolean { - if (this == null) { - return true - } - if (this.elementType == OBJECT_KEYWORD) { + private fun isNoSemicolonRequiredAfter(node: ASTNode): Boolean { + val prevCodeLeaf = + node.prevCodeLeaf() + ?: return true + if (prevCodeLeaf.elementType == OBJECT_KEYWORD) { // https://github.com/pinterest/ktlint/issues/281 return false } - val parent = this.treeParent?.psi + + val parent = prevCodeLeaf.treeParent?.psi if (parent is KtLoopExpression && parent !is KtDoWhileExpression && parent.body == null) { // https://github.com/pinterest/ktlint/issues/955 return false @@ -88,11 +91,21 @@ public class NoSemicolonsRule : Rule("no-semi") { if (parent is KtIfExpression && parent.then == null) { return false } - if (parent is KtEnumEntry) { - return this.nextCodeLeaf()?.nextCodeLeaf() == - parent.parent.lastChild + // In case of an enum entry the semicolon (e.g. the node) is a direct child node of enum entry + if (node.treeParent.elementType == ENUM_ENTRY) { + return node.isLastCodeLeafBeforeClosingOfClassBody() } return true } + + private fun ASTNode?.isLastCodeLeafBeforeClosingOfClassBody() = + getLastCodeLeafBeforeClosingOfClassBody() == this + + private fun ASTNode?.getLastCodeLeafBeforeClosingOfClassBody() = + this + ?.parent(CLASS_BODY) + ?.lastChildLeafOrSelf() + ?.prevCodeLeaf() + } diff --git a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRuleTest.kt b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRuleTest.kt index 84c1d129ef..bb4278447a 100644 --- a/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRuleTest.kt +++ b/ktlint-ruleset-standard/src/test/kotlin/com/pinterest/ktlint/ruleset/standard/NoSemicolonsRuleTest.kt @@ -293,6 +293,37 @@ class NoSemicolonsRuleTest { ONE, TWO; val foo = "foo" } + enum class E3 { + ONE, TWO; + private companion object { + val foo = "foo" + } + } + """.trimIndent() + noSemicolonsRuleAssertThat(code).hasNoLintViolations() + } + + @Test + fun `Issue 1733 - Given an enumeration with primary constructor and the list of values is followed by a code element then do not return a lint error`() { + val code = + """ + enum class E1(bar: String) { + ONE("one"), + TWO("two"); + fun fn() {} + } + enum class E2(bar: String) { + ONE("one"), + TWO("two"); + val foo = "foo" + } + enum class E3(bar: String) { + ONE("one"), + TWO("two"); + private companion object { + val foo = "foo" + } + } """.trimIndent() noSemicolonsRuleAssertThat(code).hasNoLintViolations() } @@ -306,6 +337,11 @@ class NoSemicolonsRuleTest { // comment ; } + enum class Foo(bar: String) { + ONE("one") + // comment + ; + } """.trimIndent() val formattedCode = """ @@ -313,10 +349,16 @@ class NoSemicolonsRuleTest { ONE // comment } + enum class Foo(bar: String) { + ONE("one") + // comment + } """.trimIndent() noSemicolonsRuleAssertThat(code) - .hasLintViolation(4, 5, "Unnecessary semicolon") - .isFormattedAs(formattedCode) + .hasLintViolations( + LintViolation(4, 5, "Unnecessary semicolon"), + LintViolation(9, 5, "Unnecessary semicolon"), + ).isFormattedAs(formattedCode) } @Test @@ -348,6 +390,36 @@ class NoSemicolonsRuleTest { LintViolation(15, 5, "Unnecessary semicolon"), ) } + + @Test + fun `Given an enumeration with a primary constructor and the list of values is closed with a semicolon not followed by statements then do return a lint error`() { + val code = + """ + enum class Foo1(bar: String) { + A(1), + B(2); + } + enum class Foo2(bar: String) { + A(1), + B(2) // comment + ; + } + enum class Foo3(bar: String) { + ; + } + enum class Foo4(bar: String) { + // comment + ; + } + """.trimIndent() + noSemicolonsRuleAssertThat(code) + .hasLintViolations( + LintViolation(3, 9, "Unnecessary semicolon"), + LintViolation(8, 5, "Unnecessary semicolon"), + LintViolation(11, 5, "Unnecessary semicolon"), + LintViolation(15, 5, "Unnecessary semicolon"), + ) + } } @Test