forked from detekt/detekt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
UnnamedParameterUse.kt
151 lines (138 loc) · 5.21 KB
/
UnnamedParameterUse.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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package io.gitlab.arturbosch.detekt.rules.bugs
import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Config
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.config
import io.gitlab.arturbosch.detekt.api.internal.Configuration
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.load.java.isFromJava
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtValueArgument
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.components.isVararg
import org.jetbrains.kotlin.resolve.calls.util.getParameterForArgument
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
/**
* Reports usage of unnamed parameter. Passing parameters without name can cause issue when parameters order of same
* type changes. And code gets error prone as it gets easy to mix up parameters of the same type
*
* <noncompliant>
* fun log(enabled: Boolean, shouldLog: Boolean) {
* if (shouldLog) println(enabled)
* }
* fun test() {
* log(false, true)
* }
*
* // allowAdjacentDifferentTypeParams = false
* fun log(msg: String, shouldLog: Boolean) {
* println(msg)
* }
* fun test() {
* log("test", true)
* }
*
* // allowSingleParamUse = false and allowAdjacentDifferentTypeParams = false
* fun log(msg: String) {
* println(msg)
* }
* fun test() {
* log("test")
* }
* </noncompliant>
*
* <compliant>
* fun log(enabled: Boolean, shouldLog: Boolean) {
* if (shouldLog) println(enabled)
* }
* fun test() {
* log(enabled = false, shouldLog = true)
* }
*
* // allowAdjacentDifferentTypeParams = true
* fun log(msg: String, shouldLog: Boolean) {
* println(msg)
* }
* fun test() {
* log("test", true)
* }
*
* // allowSingleParamUse = true
* fun log(msg: String) {
* println(msg)
* }
* fun test() {
* log("test")
* }
* </compliant>
*/
@RequiresTypeResolution
class UnnamedParameterUse(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
"Passing no named parameters can cause issue when parameters order change",
)
@Configuration("Allow adjacent unnamed params when type of parameters can not be assigned to each other")
val allowAdjacentDifferentTypeParams: Boolean by config(true)
@Configuration("Allow single unnamed parameter use")
val allowSingleParamUse: Boolean by config(true)
@Suppress("ReturnCount")
override fun visitCallExpression(expression: KtCallExpression) {
super.visitCallExpression(expression)
val valueArgumentList = expression.valueArgumentList ?: return
if (valueArgumentList.arguments.isEmpty()) {
return
}
val callDescriptor = expression.getResolvedCall(bindingContext) ?: return
if ((callDescriptor.resultingDescriptor as? CallableMemberDescriptor)?.isFromJava == true) {
return
}
val paramDescriptorToArgumentMap: Map<KtValueArgument, ValueParameterDescriptor?> =
valueArgumentList.arguments.associateWith {
callDescriptor.getParameterForArgument(it)
}
if (allowSingleParamUse && paramDescriptorToArgumentMap.values.distinct().size <= 1) {
return
}
val namedArgumentList = valueArgumentList.arguments.map {
val isNamedOrVararg = it.isNamed() || paramDescriptorToArgumentMap[it]?.isVararg == true
// No name parameter if it is vararg
isNamedOrVararg to it
}
if (allowAdjacentDifferentTypeParams && namedArgumentList.windowed(2)
.all {
isAdjacentUnnamedParamsAllowed(it)
}
) {
return
}
if (namedArgumentList.any { it.first.not() }) {
val target = expression.calleeExpression ?: expression
report(
CodeSmell(
issue,
Entity.from(target),
"Consider using named parameters in ${target.text} as they make usage of the function more safe."
)
)
}
}
private fun isAdjacentUnnamedParamsAllowed(it: List<Pair<Boolean, KtValueArgument>>) =
(it[0].second.text == it[1].second.text) ||
(it[0].first || it[1].first) ||
(typeCanBeAssigned(it[0].second, it[1].second).not())
@Suppress("ReturnCount")
private fun typeCanBeAssigned(firstParam: KtValueArgument, secondParam: KtValueArgument): Boolean {
val param1Type =
bindingContext[BindingContext.EXPRESSION_TYPE_INFO, firstParam.getArgumentExpression()]?.type ?: return true
val param2Type =
bindingContext[BindingContext.EXPRESSION_TYPE_INFO, secondParam.getArgumentExpression()]?.type
?: return true
return param1Type.isSubtypeOf(param2Type) || param2Type.isSubtypeOf(param1Type)
}
}