/
ExplicitCollectionElementAccessMethod.kt
121 lines (106 loc) · 5.42 KB
/
ExplicitCollectionElementAccessMethod.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
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.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.rules.fqNameOrNull
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.load.java.isFromJava
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelector
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.types.error.ErrorType
import org.jetbrains.kotlin.types.typeUtil.supertypes
/**
* In Kotlin functions `get` or `set` can be replaced with the shorter operator — `[]`,
* see [Indexed access operator](https://kotlinlang.org/docs/operator-overloading.html#indexed-access-operator).
* Prefer the usage of the indexed access operator `[]` for map or list element access or insert methods.
*
* <noncompliant>
* val map = mutableMapOf<String, String>()
* map.put("key", "value")
* val value = map.get("key")
* </noncompliant>
*
* <compliant>
* val map = mutableMapOf<String, String>()
* map["key"] = "value"
* val value = map["key"]
* </compliant>
*/
@RequiresTypeResolution
class ExplicitCollectionElementAccessMethod(config: Config = Config.empty) : Rule(config) {
override val issue: Issue =
Issue(
"ExplicitCollectionElementAccessMethod",
Severity.Style,
"Prefer usage of the indexed access operator [] for map element access or insert methods.",
Debt.FIVE_MINS
)
override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) {
super.visitDotQualifiedExpression(expression)
val call = expression.selectorExpression as? KtCallExpression ?: return
if (isIndexGetterRecommended(call) || isIndexSetterRecommended(call)) {
report(CodeSmell(issue, Entity.from(expression), issue.description))
}
}
private fun isIndexGetterRecommended(expression: KtCallExpression): Boolean {
val getter =
if (expression.calleeExpression?.text == "get") expression.getFunctionDescriptor()
else null
if (getter == null) return false
return canReplace(getter) && shouldReplace(getter)
}
private fun isIndexSetterRecommended(expression: KtCallExpression): Boolean =
when (expression.calleeExpression?.text) {
"set" -> {
val setter = expression.getFunctionDescriptor()
if (setter == null) false
else canReplace(setter) && shouldReplace(setter)
}
// `put` isn't an operator function, but can be replaced with indexer when the caller is Map.
"put" -> isCallerMap(expression)
else -> false
} && unusedReturnValue(expression)
private fun KtCallExpression.getFunctionDescriptor(): FunctionDescriptor? =
getResolvedCall(bindingContext)?.resultingDescriptor as? FunctionDescriptor
private fun canReplace(function: FunctionDescriptor): Boolean {
// Can't use index operator when insufficient information is available to infer type variable.
// For now, this is an incomplete check and doesn't report edge cases (e.g. inference using return type).
val genericParameterTypeNames = function.valueParameters.map { it.original.type.toString() }.toSet()
val typeParameterNames = function.typeParameters.map { it.name.asString() }
if (!genericParameterTypeNames.containsAll(typeParameterNames)) return false
return function.isOperator
}
private fun shouldReplace(function: FunctionDescriptor): Boolean {
// The intent of kotlin operation functions is to support indexed accessed, so should always be replaced.
if (!function.isFromJava) return true
// It does not always make sense for all Java get/set functions to be replaced by index accessors.
// Only recommend known collection types.
val javaClass = function.containingDeclaration as? ClassDescriptor ?: return false
return javaClass.fqNameSafe.asString() in setOf(
"java.util.ArrayList",
"java.util.HashMap",
"java.util.LinkedHashMap"
)
}
private fun isCallerMap(expression: KtCallExpression): Boolean {
val caller = expression.getQualifiedExpressionForSelector()?.receiverExpression
val type = caller.getResolvedCall(bindingContext)?.resultingDescriptor?.returnType
if (type == null || type is ErrorType) return false // There is no caller or it can't be resolved.
val mapName = "kotlin.collections.Map"
return type.fqNameOrNull()?.asString() == mapName ||
type.supertypes().any { it.fqNameOrNull()?.asString() == mapName }
}
private fun unusedReturnValue(expression: KtCallExpression): Boolean =
expression.parent.parent is KtBlockExpression
}