/
NestedScopeFunctions.kt
113 lines (99 loc) · 3.69 KB
/
NestedScopeFunctions.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
package io.gitlab.arturbosch.detekt.rules.complexity
import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.DetektVisitor
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.Configuration
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.psiUtil.getCallNameExpression
/**
* Although the scope functions are a way of making the code more concise, avoid overusing them: it can decrease
* your code readability and lead to errors. Avoid nesting scope functions and be careful when chaining them:
* it's easy to get confused about the current context object and the value of this or it.
*
* [Reference](https://kotlinlang.org/docs/scope-functions.html)
*
* <noncompliant>
* // Try to figure out, what changed, without knowing the details
* first.apply {
* second.apply {
* b = a
* c = b
* }
* }
* </noncompliant>
*
* <compliant>
* // 'a' is a property of current class
* // 'b' is a property of class 'first'
* // 'c' is a property of class 'second'
* first.b = this.a
* second.c = first.b
* </compliant>
*/
class NestedScopeFunctions(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.Maintainability,
"Over-using scope functions makes code confusing, hard to read and bug prone.",
Debt.FIVE_MINS
)
@Configuration("Number of nested scope functions allowed.")
private val threshold: Int by config(defaultValue = 1)
@Configuration("Set of scope function names which add complexity.")
private val functions: Set<String> by config(DEFAULT_FUNCTIONS) { it.toSet() }
override fun visitNamedFunction(function: KtNamedFunction) {
function.accept(FunctionDepthVisitor())
}
private fun report(element: KtCallExpression, depth: Int) {
val finding = ThresholdedCodeSmell(
issue,
Entity.from(element),
Metric("SIZE", depth, threshold),
"The scope function '${element.calleeExpression?.text}' is nested too deeply ('$depth'). " +
"Complexity threshold is set to '$threshold'."
)
report(finding)
}
private companion object {
val DEFAULT_FUNCTIONS = listOf(
"apply",
"run",
"with",
)
}
private inner class FunctionDepthVisitor : DetektVisitor() {
private var depth = 0
override fun visitCallExpression(expression: KtCallExpression) {
fun callSuper() = super.visitCallExpression(expression)
if (expression.isScopeFunction()) {
doWithIncrementedDepth {
reportIfOverThreshold(expression)
callSuper()
}
} else {
callSuper()
}
}
private fun doWithIncrementedDepth(block: () -> Unit) {
depth++
block()
depth--
}
private fun reportIfOverThreshold(expression: KtCallExpression) {
if (depth > threshold) {
report(expression, depth)
}
}
private fun KtCallExpression.isScopeFunction(): Boolean =
getCallNameExpression()?.text?.let { functions.contains(it) }
?: false
}
}