-
Notifications
You must be signed in to change notification settings - Fork 314
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(detekt-rules): Add a rule to enforce empty lines after blocks
Signed-off-by: Sebastian Schuberth <[email protected]>
- Loading branch information
1 parent
fabe6c8
commit ae8b9b9
Showing
4 changed files
with
250 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
detekt-rules/src/test/kotlin/OrtEmptyLineAfterBlockTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
}) |