Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Rule: BracesOnIfStatements with configurable parameters #5700

Merged
merged 35 commits into from Feb 21, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f69c758
Initial commit
VitalyVPinchuk Jan 13, 2023
1ca4daa
Rework
VitalyVPinchuk Jan 17, 2023
8f4d25b
Rework
VitalyVPinchuk Jan 18, 2023
d04ac51
Rework
VitalyVPinchuk Jan 20, 2023
5075b39
Add braces check on if statements
VitalyVPinchuk Jan 23, 2023
747b158
Start rewriting tests
TWiStErRob Jan 25, 2023
57331f6
BROKEN COMPILER
TWiStErRob Jan 25, 2023
0fb41e1
fixes
TWiStErRob Jan 26, 2023
82adf9f
New approach
TWiStErRob Jan 26, 2023
f20db91
Fix changing `shouldCompileTestSnippets` default not taking effect.
TWiStErRob Jan 26, 2023
2172b1e
Fix infinite loop
TWiStErRob Jan 26, 2023
1b95194
Restructure everything
TWiStErRob Jan 26, 2023
77c8d7f
Fill in some numbers
TWiStErRob Jan 26, 2023
246f1ce
Merge pull request #6 from TWiStErRob/braces-history
VitalyVPinchuk Jan 27, 2023
e6b16db
Fix after merge
VitalyVPinchuk Jan 29, 2023
6559504
Deprecate MandatoryBracesIfStatements rule
VitalyVPinchuk Jan 31, 2023
788e367
Deprecate MandatoryBracesIfStatements rule
VitalyVPinchuk Feb 2, 2023
305edfb
Detekt codebase uses AssertJ, let's use those methods.
TWiStErRob Jan 28, 2023
1613f16
Speed up compile-test-snippets=true executions by compiling each code…
TWiStErRob Feb 18, 2023
bd1298e
Format
TWiStErRob Feb 18, 2023
8efea30
Remove duplicate code, and let the testCombinations method be used in…
TWiStErRob Feb 18, 2023
f9960f7
Add some more test cases (singleLine)
TWiStErRob Feb 18, 2023
a436418
Add some more test cases (multiLine)
TWiStErRob Feb 18, 2023
1309b16
Add some more test cases
TWiStErRob Feb 18, 2023
c0926c6
Fix after merge
VitalyVPinchuk Feb 2, 2023
44455ef
Merge remote-tracking branch 'upstream/braces' into braces
VitalyVPinchuk Feb 19, 2023
12bd699
Fix tests
VitalyVPinchuk Feb 19, 2023
9f6732c
Update detekt-rules-style/src/main/kotlin/io/gitlab/arturbosch/detekt…
VitalyVPinchuk Feb 19, 2023
006ae3d
Add some more test cases and fix recursion
TWiStErRob Feb 20, 2023
9d2b90f
Group nested tests into @Nested
TWiStErRob Feb 20, 2023
aa76321
Remove trailing whitespace
TWiStErRob Feb 20, 2023
beb8656
Documentation of rule and internals.
TWiStErRob Feb 20, 2023
446a9cb
Simplify duplicate code.
TWiStErRob Feb 20, 2023
d172b9b
Fix detekt
TWiStErRob Feb 20, 2023
4d97f6c
Add missing tests for necessary policy
TWiStErRob Feb 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions config/detekt/detekt.yml
Expand Up @@ -149,6 +149,10 @@ potential-bugs:
active: true

style:
VitalyVPinchuk marked this conversation as resolved.
Show resolved Hide resolved
BracesOnIfStatements:
active: true
singleLine: 'consistent'
multiLine: 'consistent'
VitalyVPinchuk marked this conversation as resolved.
Show resolved Hide resolved
CanBeNonNullable:
active: true
CascadingCallWrapping:
Expand Down
6 changes: 4 additions & 2 deletions detekt-core/src/main/resources/default-detekt-config.yml
Expand Up @@ -500,6 +500,10 @@ style:
active: true
AlsoCouldBeApply:
active: false
BracesOnIfStatements:
active: false
singleLine: 'never'
multiLine: 'always'
CanBeNonNullable:
active: false
CascadingCallWrapping:
Expand Down Expand Up @@ -598,8 +602,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
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
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()
}
Expand Up @@ -100,6 +100,7 @@ class SuspendFunWithCoroutineScopeReceiverSpec(val env: KotlinCoreEnvironment) {
val code = """
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds

suspend fun foo() = coroutineScope {
Expand All @@ -117,6 +118,7 @@ class SuspendFunWithCoroutineScopeReceiverSpec(val env: KotlinCoreEnvironment) {
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope

suspend fun Long.foo() = coroutineScope {
launch {
Expand Down
@@ -0,0 +1,222 @@
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.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtIfExpression

/**
VitalyVPinchuk marked this conversation as resolved.
Show resolved Hide resolved
* This rule detects `if` statements which do not comply with the specified rules.
* Keeping braces consistent would improve readability and avoid possible errors.
*
VitalyVPinchuk marked this conversation as resolved.
Show resolved Hide resolved
* 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.
*
* SingleLine if-statement has no line break (\n):
* ```
* if (a) b else c
* ```
* MultiLine if-statement has at least one line break (\n):
* ```
* 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
* }
* </noncompliant>
*
* <compliant>
VitalyVPinchuk marked this conversation as resolved.
Show resolved Hide resolved
* // 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
* </compliant>
*/
class BracesOnIfStatements(config: Config = Config.empty) : Rule(config) {
VitalyVPinchuk marked this conversation as resolved.
Show resolved Hide resolved

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) {
VitalyVPinchuk marked this conversation as resolved.
Show resolved Hide resolved
super.visitIfExpression(expression)
TWiStErRob marked this conversation as resolved.
Show resolved Hide resolved
if (expression.parent.parent is KtIfExpression) 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) {
when (policy) {
BracePolicy.Always -> {
list.filter { !hasBraces(it) }.report(policy)
}

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

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

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

private fun List<KtExpression>.report(policy: BracePolicy) {
this.forEach { report(it, policy) }
}

private fun report(violator: KtExpression, policy: BracePolicy) {
val iff = violator.parent.parent 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))
}

private fun isMultiStatement(expression: KtExpression) =
expression is KtBlockExpression && expression.statements.size > 1
TWiStErRob marked this conversation as resolved.
Show resolved Hide resolved

private fun policy(expression: KtExpression): BracePolicy =
if (expression.textContains('\n')) multiLine else singleLine
VitalyVPinchuk marked this conversation as resolved.
Show resolved Hide resolved

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("|")}")
}
}
}
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 @@ -76,7 +75,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.