diff --git a/.detekt.yml b/.detekt.yml index ef18bb55e7cfa..5d1ff4015aa5c 100644 --- a/.detekt.yml +++ b/.detekt.yml @@ -99,6 +99,8 @@ potential-bugs: excludes: ["**/GradleModel.kt", "**/build.gradle.kts"] ORT: + OrtEmptyLineAfterBlock: + active: true OrtImportOrder: active: true excludes: ["**/clients/github-graphql/build/generated/**"] diff --git a/detekt-rules/src/main/kotlin/OrtEmptyLineAfterBlock.kt b/detekt-rules/src/main/kotlin/OrtEmptyLineAfterBlock.kt new file mode 100644 index 0000000000000..10c3142b93008 --- /dev/null +++ b/detekt-rules/src/main/kotlin/OrtEmptyLineAfterBlock.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.detekt + +import io.gitlab.arturbosch.detekt.api.CodeSmell +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.Debt +import io.gitlab.arturbosch.detekt.api.Entity +import io.gitlab.arturbosch.detekt.api.Issue +import io.gitlab.arturbosch.detekt.api.Rule +import io.gitlab.arturbosch.detekt.api.Severity + +import org.jetbrains.kotlin.com.intellij.psi.PsiElement +import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace +import org.jetbrains.kotlin.com.intellij.psi.impl.source.tree.LeafPsiElement +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtLambdaExpression +import org.jetbrains.kotlin.psi.psiUtil.allChildren + +class OrtEmptyLineAfterBlock(config: Config) : Rule(config) { + override val issue = Issue( + javaClass.simpleName, + Severity.Style, + "Reports code blocks that are not followed by an empty line", + Debt.FIVE_MINS + ) + + override fun visitBlockExpression(blockExpression: KtBlockExpression) { + super.visitBlockExpression(blockExpression) + checkExpression(blockExpression) + } + + override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { + super.visitLambdaExpression(lambdaExpression) + checkExpression(lambdaExpression) + } + + private fun checkExpression(expression: KtExpression) { + // Only care about blocks that span multiple lines. + if (!expression.hasNewLine()) return + + // Find the next expression after the block, if any. + var currentElement: PsiElement = expression + while (currentElement.nextSibling == null) { + currentElement = currentElement.parent ?: return + } + + val firstElementAfterBlock = currentElement.nextSibling ?: return + if (!firstElementAfterBlock.isNewLine()) return + + val secondElementAfterBlock = firstElementAfterBlock.nextSibling ?: return + if (secondElementAfterBlock is LeafPsiElement && secondElementAfterBlock.elementType in allowedElements) return + + if (!firstElementAfterBlock.isNewLine(2)) { + val message = "Missing empty line after block." + + val finding = CodeSmell( + issue, + // Use the message as the name to also see it in CLI output and not only in the report files. + Entity.from(expression).copy(name = message), + message + ) + + report(finding) + } + } +} + +private fun KtExpression.hasNewLine(count: Int = 1): Boolean = + allChildren.any { it.isNewLine(count) } || allChildren.any { it is KtExpression && it.hasNewLine(count) } + +private fun PsiElement.isNewLine(count: Int = 1): Boolean = this is PsiWhiteSpace && "\n".repeat(count) in text + +private val allowedElements = setOf(KtTokens.DOT, KtTokens.RBRACE, KtTokens.RPAR) diff --git a/detekt-rules/src/main/kotlin/OrtRuleSet.kt b/detekt-rules/src/main/kotlin/OrtRuleSet.kt index 1c34ad84ec714..24448b3c463e4 100644 --- a/detekt-rules/src/main/kotlin/OrtRuleSet.kt +++ b/detekt-rules/src/main/kotlin/OrtRuleSet.kt @@ -26,5 +26,13 @@ import io.gitlab.arturbosch.detekt.api.RuleSetProvider class OrtRuleSet : RuleSetProvider { override val ruleSetId: String = "ORT" - override fun instance(config: Config) = RuleSet(ruleSetId, listOf(OrtImportOrder(config), OrtPackageNaming(config))) + override fun instance(config: Config) = + RuleSet( + ruleSetId, + listOf( + OrtEmptyLineAfterBlock(config), + OrtImportOrder(config), + OrtPackageNaming(config) + ) + ) } diff --git a/detekt-rules/src/test/kotlin/OrtEmptyLineAfterBlockTest.kt b/detekt-rules/src/test/kotlin/OrtEmptyLineAfterBlockTest.kt new file mode 100644 index 0000000000000..1e60a8874c6e0 --- /dev/null +++ b/detekt-rules/src/test/kotlin/OrtEmptyLineAfterBlockTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.detekt + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.test.lint + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.collections.beEmpty +import io.kotest.matchers.collections.haveSize +import io.kotest.matchers.should + +class OrtEmptyLineAfterBlockTest : WordSpec({ + val rule = OrtEmptyLineAfterBlock(Config.empty) + + "OrtEmptyLineAfterBlock rule" should { + "succeed if an empty line is inserted after a block" { + val findings = rule.lint( + // language=Kotlin + """ + fun foo() { + if (true) { + println("Inside block.") + } + + println("This statement is valid.") + } + """.trimIndent() + ) + + findings should beEmpty() + } + + "succeed if no empty line is inserted after a one-liner block" { + val findings = rule.lint( + // language=Kotlin + """ + fun foo() { + if (true) { println("Inside block.") } + println("This statement is valid.") + } + """.trimIndent() + ) + + findings should beEmpty() + } + + "succeed if a block has no siblings" { + val findings = rule.lint( + // language=Kotlin + """ + fun foo() { + if (true) { + println("Inside block.") + } + } + """.trimIndent() + ) + + findings should beEmpty() + } + + "succeed for a chain of blocks" { + val findings = rule.lint( + // language=Kotlin + """ + fun foo() = + if (true) { + println("Inside if.") + } else { + println("Inside else.") + }.also { println("Inside also.") } + """.trimIndent() + ) + + findings should beEmpty() + } + + "fail if no empty line is inserted between a block and a call" { + val findings = rule.lint( + // language=Kotlin + """ + fun foo() { + if (true) { + println("Inside block.") + } + println("This statement is invalid.") + } + """.trimIndent() + ) + + findings should haveSize(1) + } + + "fail if no empty line is inserted between a block and another block" { + val findings = rule.lint( + // language=Kotlin + """ + fun foo() { + if (true) { + println("Inside first block.") + } + if (true) { + println("Inside second block.") + } + } + """.trimIndent() + ) + + findings should haveSize(1) + } + + "fail if no empty line is inserted after a multi-line lambda argument" { + val findings = rule.lint( + // language=Kotlin + """ + fun foo() { + require(true) { + println("Inside require.") + } + println("Outside require.") + } + """.trimIndent() + ) + + findings should haveSize(1) + } + } +})