Skip to content

Commit

Permalink
BracesOnIfStatements with configurable parameters (#5700)
Browse files Browse the repository at this point in the history
Co-authored-by: Róbert Papp (TWiStErRob) <papp.robert.s@gmail.com>
  • Loading branch information
VitalyVPinchuk and TWiStErRob committed Feb 21, 2023
1 parent 1af1e98 commit b616a2f
Show file tree
Hide file tree
Showing 9 changed files with 2,487 additions and 286 deletions.
4 changes: 4 additions & 0 deletions config/detekt/detekt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ potential-bugs:
active: true

style:
BracesOnIfStatements:
active: true
singleLine: 'consistent'
multiLine: 'consistent'
CanBeNonNullable:
active: true
CascadingCallWrapping:
Expand Down
6 changes: 4 additions & 2 deletions detekt-core/src/main/resources/default-detekt-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,10 @@ style:
active: true
AlsoCouldBeApply:
active: false
BracesOnIfStatements:
active: false
singleLine: 'never'
multiLine: 'always'
CanBeNonNullable:
active: false
CascadingCallWrapping:
Expand Down Expand Up @@ -605,8 +609,6 @@ style:
ignoreEnums: false
ignoreRanges: false
ignoreExtensionFunctions: true
MandatoryBracesIfStatements:
active: false
MandatoryBracesLoops:
active: false
MaxChainedCallsOnSameLine:
Expand Down
1 change: 1 addition & 0 deletions detekt-core/src/main/resources/deprecation.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ formatting>TrailingComma=Rule is split between `TrailingCommaOnCallSite` and `Tr
style>ForbiddenPublicDataClass=Rule migrated to `libraries` ruleset plugin
style>LibraryCodeMustSpecifyReturnType=Rule migrated to `libraries` ruleset plugin
style>LibraryEntitiesShouldNotBePublic=Rule migrated to `libraries` ruleset plugin
style>MandatoryBracesIfStatements=Use `BracesOnIfStatements` with `always` configuration instead
complexity>ComplexMethod=Rule is renamed to `CyclomaticComplexMethod` to distinguish between Cyclomatic Complexity and Cognitive Complexity
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ internal fun writeMigratedRules(): String {
style>ForbiddenPublicDataClass=Rule migrated to `libraries` ruleset plugin
style>LibraryCodeMustSpecifyReturnType=Rule migrated to `libraries` ruleset plugin
style>LibraryEntitiesShouldNotBePublic=Rule migrated to `libraries` ruleset plugin
style>MandatoryBracesIfStatements=Use `BracesOnIfStatements` with `always` configuration instead
complexity>ComplexMethod=Rule is renamed to `CyclomaticComplexMethod` to distinguish between Cyclomatic Complexity and Cognitive Complexity
""".trimIndent()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package io.gitlab.arturbosch.detekt.rules.style

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 io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtIfExpression

/**
* This rule detects `if` statements which do not comply with the specified rules.
* Keeping braces consistent will improve readability and avoid possible errors.
*
* The available options are:
* * `always`: forces braces on all `if` and `else` branches in the whole codebase.
* * `consistent`: ensures that braces are consistent within each `if`-`else if`-`else` chain.
* If there's a brace on one of the branches, all branches should have it.
* * `necessary`: forces no braces on any `if` and `else` branches in the whole codebase
* except where necessary for multi-statement branches.
* * `never`: forces no braces on any `if` and `else` branches in the whole codebase.
*
* Single-line if-statement has no line break (\n):
* ```kotlin
* if (a) b else c
* ```
* Multi-line if-statement has at least one line break (\n):
* ```kotlin
* if (a) b
* else c
* ```
*
* <noncompliant>
* // singleLine = 'never'
* if (a) { b } else { c }
*
* if (a) { b } else c
*
* if (a) b else { c; d }
*
* // multiLine = 'never'
* if (a) {
* b
* } else {
* c
* }
*
* // singleLine = 'always'
* if (a) b else c
*
* if (a) { b } else c
*
* // multiLine = 'always'
* if (a) {
* b
* } else
* c
*
* // singleLine = 'consistent'
* if (a) b else { c }
* if (a) b else if (c) d else { e }
*
* // multiLine = 'consistent'
* if (a)
* b
* else {
* c
* }
*
* // singleLine = 'necessary'
* if (a) { b } else { c; d }
*
* // multiLine = 'necessary'
* if (a) {
* b
* c
* } else if (d) {
* e
* } else {
* f
* }
* </noncompliant>
*
* <compliant>
* // singleLine = 'never'
* if (a) b else c
*
* // multiLine = 'never'
* if (a)
* b
* else
* c
*
* // singleLine = 'always'
* if (a) { b } else { c }
*
* if (a) { b } else if (c) { d }
*
* // multiLine = 'always'
* if (a) {
* b
* } else {
* c
* }
*
* if (a) {
* b
* } else if (c) {
* d
* }
*
* // singleLine = 'consistent'
* if (a) b else c
*
* if (a) { b } else { c }
*
* if (a) { b } else { c; d }
*
* // multiLine = 'consistent'
* if (a) {
* b
* } else {
* c
* }
*
* if (a) b
* else c
*
* // singleLine = 'necessary'
* if (a) b else { c; d }
*
* // multiLine = 'necessary'
* if (a) {
* b
* c
* } else if (d)
* e
* else
* f
* </compliant>
*/
class BracesOnIfStatements(config: Config = Config.empty) : Rule(config) {

override val issue = Issue(
javaClass.simpleName,
Severity.Style,
"Braces do not comply with the specified policy",
Debt.FIVE_MINS
)

@Configuration("single-line braces policy")
private val singleLine: BracePolicy by config("never") { BracePolicy.getValue(it) }

@Configuration("multi-line braces policy")
private val multiLine: BracePolicy by config("always") { BracePolicy.getValue(it) }

override fun visitIfExpression(expression: KtIfExpression) {
super.visitIfExpression(expression)

val parent = expression.parentIfCandidate()
// Ignore `else` branches, they're handled by the initial `if`'s visit.
// But let us process `then` branches and conditions, because they might be `if` themselves.
if (parent is KtIfExpression && parent.`else` === expression) return

val branches: List<KtExpression> = walk(expression)
validate(branches, policy(expression))
}

private fun walk(expression: KtExpression): List<KtExpression> {
val list = mutableListOf<KtExpression>()
var current: KtExpression? = expression
while (current is KtIfExpression) {
current.then?.let { list.add(it) }
// Don't add `if` because it's an `else if` which we treat as one unit.
current.`else`?.takeIf { it !is KtIfExpression }?.let { list.add(it) }
current = current.`else`
}
return list
}

private fun validate(list: List<KtExpression>, policy: BracePolicy) {
val violators = when (policy) {
BracePolicy.Always -> {
list.filter { !hasBraces(it) }
}

BracePolicy.Necessary -> {
list.filter { !isMultiStatement(it) && hasBraces(it) }
}

BracePolicy.Never -> {
list.filter { hasBraces(it) }
}

BracePolicy.Consistent -> {
val braces = list.count { hasBraces(it) }
val noBraces = list.count { !hasBraces(it) }
if (braces != 0 && noBraces != 0) {
list.take(1)
} else {
emptyList()
}
}
}
violators.forEach { report(it, policy) }
}

private fun report(violator: KtExpression, policy: BracePolicy) {
val iff = violator.parentIfCandidate() as KtIfExpression
val reported = when {
iff.then === violator -> iff.ifKeyword
iff.`else` === violator -> iff.elseKeyword
else -> error("Violating element (${violator.text}) is not part of this if (${iff.text})")
}
val message = when (policy) {
BracePolicy.Always -> "Missing braces on this branch, add them."
BracePolicy.Consistent -> "Inconsistent braces, make sure all branches either have or don't have braces."
BracePolicy.Necessary -> "Extra braces exist on this branch, remove them (ignore multi-statement)."
BracePolicy.Never -> "Extra braces exist on this branch, remove them."
}
report(CodeSmell(issue, Entity.from(reported ?: violator), message))
}

/**
* Returns a potential parent of the expression, that could be a [KtIfExpression].
* There's a double-indirection needed because the `then` and `else` branches
* are represented as a [org.jetbrains.kotlin.psi.KtContainerNodeForControlStructureBody].
* Also, the condition inside the `if` is in an intermediate [org.jetbrains.kotlin.psi.KtContainerNode].
* ```
* if (parent)
* / | \
* cond then else (parent)
* | | |
* expr expr expr
* ```
* @see org.jetbrains.kotlin.KtNodeTypes.CONDITION
* @see org.jetbrains.kotlin.KtNodeTypes.THEN
* @see org.jetbrains.kotlin.KtNodeTypes.ELSE
*/
private fun KtExpression.parentIfCandidate(): PsiElement? =
this.parent.parent

private fun isMultiStatement(expression: KtExpression): Boolean =
expression is KtBlockExpression && expression.statements.size > 1

private fun policy(expression: KtExpression): BracePolicy =
if (expression.textContains('\n')) multiLine else singleLine

private fun hasBraces(expression: KtExpression): Boolean =
expression is KtBlockExpression

enum class BracePolicy(val config: String) {
Always("always"),
Consistent("consistent"),
Necessary("necessary"),
Never("never");

companion object {
fun getValue(arg: String): BracePolicy =
values().singleOrNull { it.config == arg }
?: error("Unknown value $arg, allowed values are: ${values().joinToString("|")}")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
import io.gitlab.arturbosch.detekt.api.internal.DefaultRuleSetProvider
import io.gitlab.arturbosch.detekt.rules.style.optional.MandatoryBracesIfStatements
import io.gitlab.arturbosch.detekt.rules.style.optional.MandatoryBracesLoops
import io.gitlab.arturbosch.detekt.rules.style.optional.OptionalUnit
import io.gitlab.arturbosch.detekt.rules.style.optional.PreferToOverPairSyntax
Expand Down Expand Up @@ -78,7 +77,7 @@ class StyleGuideProvider : DefaultRuleSetProvider {
UnnecessaryLet(config),
MayBeConst(config),
PreferToOverPairSyntax(config),
MandatoryBracesIfStatements(config),
BracesOnIfStatements(config),
MandatoryBracesLoops(config),
NullableBooleanCheck(config),
VarCouldBeVal(config),
Expand Down

This file was deleted.

0 comments on commit b616a2f

Please sign in to comment.