/
ReturnCount.kt
124 lines (108 loc) · 4.46 KB
/
ReturnCount.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
119
120
121
122
123
124
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.SplitPattern
import io.gitlab.arturbosch.detekt.api.config
import io.gitlab.arturbosch.detekt.api.internal.ActiveByDefault
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.rules.parentsOfTypeUntil
import io.gitlab.arturbosch.detekt.rules.yieldStatementsSkippingGuardClauses
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtReturnExpression
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
/**
* Restrict the number of return methods allowed in methods.
*
* Having many exit points in a function can be confusing and impacts readability of the
* code.
*
* <noncompliant>
* fun foo(i: Int): String {
* when (i) {
* 1 -> return "one"
* 2 -> return "two"
* else -> return "other"
* }
* }
* </noncompliant>
*
* <compliant>
* fun foo(i: Int): String {
* return when (i) {
* 1 -> "one"
* 2 -> "two"
* else -> "other"
* }
* }
* </compliant>
*/
@ActiveByDefault(since = "1.0.0")
class ReturnCount(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.Style,
"Restrict the number of return statements in methods.",
Debt.TEN_MINS
)
@Configuration("define the maximum number of return statements allowed per function")
private val max: Int by config(2)
@Configuration("define a free-form comma separated list of function names to be ignored by this check")
private val excludedFunctions: SplitPattern by config("equals") { SplitPattern(it) }
@Configuration("if labeled return statements should be ignored")
private val excludeLabeled: Boolean by config(false)
@Configuration("if labeled return from a lambda should be ignored")
private val excludeReturnFromLambda: Boolean by config(true)
@Configuration("if true guard clauses at the beginning of a method should be ignored")
private val excludeGuardClauses: Boolean by config(false)
override fun visitNamedFunction(function: KtNamedFunction) {
super.visitNamedFunction(function)
if (!shouldBeIgnored(function)) {
val numberOfReturns = countReturnStatements(function)
if (numberOfReturns > max) {
report(
CodeSmell(
issue,
Entity.atName(function),
"Function ${function.name} has $numberOfReturns return statements " +
"which exceeds the limit of $max."
)
)
}
}
}
private fun shouldBeIgnored(function: KtNamedFunction) = excludedFunctions.contains(function.name)
private fun countReturnStatements(function: KtNamedFunction): Int {
fun KtReturnExpression.isExcluded(): Boolean = when {
excludeLabeled && labeledExpression != null -> true
excludeReturnFromLambda && isNamedReturnFromLambda() -> true
else -> false
}
val statements = if (excludeGuardClauses) {
function.yieldStatementsSkippingGuardClauses<KtReturnExpression>()
} else {
function.bodyBlockExpression?.statements?.asSequence().orEmpty()
}
return statements.flatMap { it.collectDescendantsOfType<KtReturnExpression>().asSequence() }
.filterNot { it.isExcluded() }
.count { it.getParentOfType<KtNamedFunction>(true) == function }
}
private fun KtReturnExpression.isNamedReturnFromLambda(): Boolean {
val label = this.labeledExpression
if (label != null) {
return this.parentsOfTypeUntil<KtCallExpression, KtNamedFunction>()
.map { it.calleeExpression }
.filterIsInstance<KtNameReferenceExpression>()
.map { it.text }
.any { it in label.text }
}
return false
}
}