Skip to content

Commit

Permalink
feat(detekt-rules): Add a rule to enforce empty lines after blocks
Browse files Browse the repository at this point in the history
Signed-off-by: Sebastian Schuberth <[email protected]>
  • Loading branch information
sschuberth committed Jun 27, 2024
1 parent fabe6c8 commit ae8b9b9
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"]
Expand Down
93 changes: 93 additions & 0 deletions detekt-rules/src/main/kotlin/OrtEmptyLineAfterBlock.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* 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)
10 changes: 9 additions & 1 deletion detekt-rules/src/main/kotlin/OrtRuleSet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
)
}
146 changes: 146 additions & 0 deletions detekt-rules/src/test/kotlin/OrtEmptyLineAfterBlockTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* 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)
}
}
})

0 comments on commit ae8b9b9

Please sign in to comment.