/
CyclomaticComplexMethod.kt
118 lines (103 loc) · 5.08 KB
/
CyclomaticComplexMethod.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package io.gitlab.arturbosch.detekt.rules.complexity
import io.github.detekt.metrics.CyclomaticComplexity
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.Metric
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import io.gitlab.arturbosch.detekt.api.ThresholdedCodeSmell
import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
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.KtNamedFunction
import org.jetbrains.kotlin.psi.KtReturnExpression
import org.jetbrains.kotlin.psi.KtWhenExpression
/**
* Complex methods are hard to understand and read. It might not be obvious what side-effects a complex method has.
* Prefer splitting up complex methods into smaller methods that are in turn easier to understand.
* Smaller methods can also be named much clearer which leads to improved readability of the code.
*
* This rule uses McCabe's Cyclomatic Complexity (MCC) metric to measure the number of
* linearly independent paths through a function's source code (https://www.ndepend.com/docs/code-metrics#CC).
* The higher the number of independent paths, the more complex a method is.
* Complex methods use too many of the following statements.
* Each one of them adds one to the complexity count.
*
* - __Conditional statements__ - `if`, `else if`, `when`
* - __Jump statements__ - `continue`, `break`
* - __Loops__ - `for`, `while`, `do-while`, `forEach`
* - __Operators__ `&&`, `||`, `?:`
* - __Exceptions__ - `catch`, `use`
* - __Scope Functions__ - `let`, `run`, `with`, `apply`, and `also` ->
* [Reference](https://kotlinlang.org/docs/scope-functions.html)
*/
@ActiveByDefault(since = "1.0.0")
class CyclomaticComplexMethod(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
"CyclomaticComplexMethod",
Severity.Maintainability,
"Prefer splitting up complex methods into smaller, easier to test methods.",
Debt.TWENTY_MINS
)
override val defaultRuleIdAliases: Set<String> = setOf("ComplexMethod")
@Configuration("McCabe's Cyclomatic Complexity (MCC) number for a method.")
private val threshold: Int by config(defaultValue = 15)
@Configuration("Ignores a complex method if it only contains a single when expression.")
private val ignoreSingleWhenExpression: Boolean by config(false)
@Configuration("Whether to ignore simple (braceless) when entries.")
private val ignoreSimpleWhenEntries: Boolean by config(false)
@Configuration("Whether to ignore functions which are often used instead of an `if` or `for` statement.")
private val ignoreNestingFunctions: Boolean by config(false)
@Configuration("Comma separated list of function names which add complexity.")
private val nestingFunctions: Set<String> by config(DEFAULT_NESTING_FUNCTIONS) { it.toSet() }
override fun visitNamedFunction(function: KtNamedFunction) {
if (ignoreSingleWhenExpression && hasSingleWhenExpression(function.bodyExpression)) {
return
}
val complexity = CyclomaticComplexity.calculate(function) {
this.ignoreSimpleWhenEntries = this@CyclomaticComplexMethod.ignoreSimpleWhenEntries
this.ignoreNestingFunctions = this@CyclomaticComplexMethod.ignoreNestingFunctions
this.nestingFunctions = this@CyclomaticComplexMethod.nestingFunctions
}
if (complexity >= threshold) {
report(
ThresholdedCodeSmell(
issue,
Entity.atName(function),
Metric("MCC", complexity, threshold),
"The function ${function.nameAsSafeName} appears to be too complex " +
"based on Cyclomatic Complexity (complexity: $complexity). " +
"Defined complexity threshold for methods is set to '$threshold'"
)
)
}
}
private fun hasSingleWhenExpression(bodyExpression: KtExpression?): Boolean = when {
bodyExpression is KtBlockExpression && bodyExpression.statements.size == 1 -> {
val statement = bodyExpression.statements.single()
statement is KtWhenExpression || statement.returnsWhenExpression()
}
// the case where function-expression syntax is used: `fun test() = when { ... }`
bodyExpression is KtWhenExpression -> true
else -> false
}
private fun KtExpression.returnsWhenExpression() =
this is KtReturnExpression && this.returnedExpression is KtWhenExpression
companion object {
val DEFAULT_NESTING_FUNCTIONS = listOf(
"also",
"apply",
"forEach",
"isNotNull",
"ifNull",
"let",
"run",
"use",
"with",
)
}
}