/
NestedScopeFunctions.kt
134 lines (118 loc) · 4.57 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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package io.gitlab.arturbosch.detekt.rules.complexity
import io.github.detekt.tooling.api.FunctionMatcher
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 io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
/**
* 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>
*/
@RequiresTypeResolution
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. " +
"Function names have to be fully qualified. For example 'kotlin.apply'."
)
private val functions: List<FunctionMatcher> by config(DEFAULT_FUNCTIONS) {
it.toSet().map(FunctionMatcher::fromFunctionSignature)
}
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(
"kotlin.apply",
"kotlin.run",
"kotlin.with",
"kotlin.let",
"kotlin.also",
)
}
private inner class FunctionDepthVisitor : DetektVisitor() {
private var depth = 0
override fun visitCallExpression(expression: KtCallExpression) {
fun callSuper(): Unit = 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 {
val descriptors = resolveDescriptors()
return !descriptors.any { it.matchesScopeFunction() }
}
private fun KtCallExpression.resolveDescriptors(): List<CallableDescriptor> =
getResolvedCall(bindingContext)?.resultingDescriptor
?.let { listOf(it) + it.overriddenDescriptors }
.orEmpty()
private fun CallableDescriptor.matchesScopeFunction(): Boolean =
!functions.any { it.match(this) }
}
}