/
SpreadOperator.kt
119 lines (112 loc) · 5.2 KB
/
SpreadOperator.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
package io.gitlab.arturbosch.detekt.rules.performance
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.internal.ActiveByDefault
import org.jetbrains.kotlin.descriptors.ConstructorDescriptor
import org.jetbrains.kotlin.descriptors.ParameterDescriptor
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtValueArgument
import org.jetbrains.kotlin.psi.KtValueArgumentList
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.CompileTimeConstantUtils
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.components.isVararg
/**
* In most cases using a spread operator causes a full copy of the array to be created before calling a method.
* This has a very high performance penalty. Benchmarks showing this performance penalty can be seen here:
* https://sites.google.com/a/athaydes.com/renato-athaydes/posts/kotlinshiddencosts-benchmarks
*
* The Kotlin compiler since v1.1.60 has an optimization that skips the array copy when an array constructor
* function is used to create the arguments that are passed to the vararg parameter. When type resolution is enabled in
* detekt this case will not be flagged by the rule since it doesn't suffer the performance penalty of an array copy.
*
* <noncompliant>
* val strs = arrayOf("value one", "value two")
* val foo = bar(*strs)
*
* fun bar(vararg strs: String) {
* strs.forEach { println(it) }
* }
* </noncompliant>
*
* <compliant>
* // array copy skipped in this case since Kotlin 1.1.60
* val foo = bar(*arrayOf("value one", "value two"))
*
* // array not passed so no array copy is required
* val foo2 = bar("value one", "value two")
*
* fun bar(vararg strs: String) {
* strs.forEach { println(it) }
* }
* </compliant>
*/
@ActiveByDefault(since = "1.0.0")
class SpreadOperator(config: Config = Config.empty) : Rule(config) {
override val issue: Issue = Issue(
"SpreadOperator",
Severity.Performance,
"In most cases using a spread operator causes a full copy of the array to be created before calling a " +
"method. This may result in a performance penalty.",
Debt.TWENTY_MINS
)
override fun visitValueArgumentList(list: KtValueArgumentList) {
super.visitValueArgumentList(list)
list.arguments
.filter { it.isSpread }
.filterNotNull()
.forEach { checkCanSkipArrayCopy(it, list) }
}
// Check for non type resolution case if call vararg argument is exactly the vararg parameter.
// In this case Kotlin 1.1.60+ does not create an additional copy.
// Note: this does not check the control flow like the type solution case does.
// It will not report corner cases like shadowed variables.
// This is okay as our users are encouraged to use type resolution for better results.
private fun isSimplePassThroughVararg(arg: KtValueArgument): Boolean {
val argumentName = arg.getArgumentExpression()?.text
return arg.getStrictParentOfType<KtNamedFunction>()
?.valueParameters
?.any { it.isVarArg && it.name == argumentName } == true
}
private fun checkCanSkipArrayCopy(arg: KtValueArgument, argsList: KtValueArgumentList) {
if (bindingContext == BindingContext.EMPTY) {
if (isSimplePassThroughVararg(arg)) {
return
}
report(CodeSmell(issue, Entity.from(argsList), issue.description))
} else {
if (arg.canSkipArrayCopyForSpreadArgument()) {
return
}
report(
CodeSmell(
issue,
Entity.from(argsList),
"Used in this way a spread operator causes a full copy of the array to be created before " +
"calling a method. This may result in a performance penalty."
)
)
}
}
/**
* Checks if an array copy can be skipped for this usage of the spread operator. If not, an array copy is required
* for this usage of the spread operator, which will have a performance impact.
*/
private fun KtValueArgument.canSkipArrayCopyForSpreadArgument(): Boolean {
val resolvedCall = getArgumentExpression().getResolvedCall(bindingContext) ?: return false
val calleeDescriptor = resolvedCall.resultingDescriptor
if (calleeDescriptor is ParameterDescriptor && calleeDescriptor.isVararg) {
return true // As of Kotlin 1.1.60 passing varargs parameters to vararg calls does not create a new copy
}
return calleeDescriptor is ConstructorDescriptor ||
CompileTimeConstantUtils.isArrayFunctionCall(resolvedCall) ||
DescriptorUtils.getFqName(calleeDescriptor).asString() == "kotlin.arrayOfNulls"
}
}