-
-
Notifications
You must be signed in to change notification settings - Fork 755
/
UselessCallOnNotNull.kt
135 lines (122 loc) · 5.69 KB
/
UselessCallOnNotNull.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
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.ActiveByDefault
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtQualifiedExpression
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
import org.jetbrains.kotlin.psi.ValueArgument
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.util.getType
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.types.ErrorType
import org.jetbrains.kotlin.types.isNullable
/*
* Rule adapted from Kotlin's IntelliJ plugin:
* https://github.com/JetBrains/kotlin/blob/f5d0a38629e7d2e7017ee645dc4d4bee60614e93/idea/src/org/jetbrains/kotlin/idea/inspections/collections/UselessCallOnNotNullInspection.kt
*/
/**
* The Kotlin stdlib provides some functions that are designed to operate on references that may be null. These
* functions can also be called on non-nullable references or on collections or sequences that are known to be empty -
* the calls are redundant in this case and can be removed or should be changed to a call that does not check whether
* the value is null or not.
*
* <noncompliant>
* val testList = listOf("string").orEmpty()
* val testList2 = listOf("string").orEmpty().map { _ }
* val testList3 = listOfNotNull("string")
* val testString = ""?.isNullOrBlank()
* </noncompliant>
*
* <compliant>
* val testList = listOf("string")
* val testList2 = listOf("string").map { }
* val testList3 = listOf("string")
* val testString = ""?.isBlank()
* </compliant>
*/
@RequiresTypeResolution
@ActiveByDefault(since = "1.2.0")
class UselessCallOnNotNull(config: Config = Config.empty) : Rule(config) {
override val issue: Issue = Issue(
"UselessCallOnNotNull",
Severity.Performance,
"This call on a non-null reference may be reduced or removed. " +
"Some calls are intended to be called on nullable collection or text types (e.g. `String?`)." +
"When this call is used on a reference to a non-null type " +
"(e.g. `String`) it is redundant and will have no effect, so it can be removed.",
Debt.FIVE_MINS
)
@Suppress("ReturnCount")
override fun visitQualifiedExpression(expression: KtQualifiedExpression) {
super.visitQualifiedExpression(expression)
if (bindingContext == BindingContext.EMPTY) return
val safeExpression = expression as? KtSafeQualifiedExpression
val notNullType = expression.receiverExpression.getType(bindingContext)?.isNullable() == false
if (notNullType || safeExpression != null) {
resolveCallForExpression(expression)
}
}
private fun resolveCallForExpression(expression: KtQualifiedExpression) {
val resolvedCall = expression.getResolvedCall(bindingContext) ?: return
val fqName = resolvedCall.resultingDescriptor.fqNameOrNull() ?: return
val conversion = uselessFqNames[fqName]
if (conversion != null) {
val shortName = fqName.shortName().asString()
val message = if (conversion.replacementName == null) {
"Remove redundant call to $shortName"
} else {
"Replace $shortName with ${conversion.replacementName}"
}
report(CodeSmell(issue, Entity.from(expression), message))
}
}
override fun visitCallExpression(expression: KtCallExpression) {
super.visitCallExpression(expression)
val resolvedCall = expression.getResolvedCall(bindingContext) ?: return
val fqName = resolvedCall.resultingDescriptor.fqNameOrNull()
if (fqName == listOfNotNull) {
val varargs = resolvedCall.valueArguments.entries.single().value.arguments
if (varargs.all { it.isNullable() == false }) {
report(CodeSmell(issue, Entity.from(expression), "Replace listOfNotNull with listOf"))
}
}
}
/**
* Determines whether this [ValueArgument] is nullable, returning null if its type cannot be
* determined.
*/
private fun ValueArgument.isNullable(): Boolean? {
val wrapperType = getArgumentExpression()?.getType(bindingContext) ?: return null
val type = if (getSpreadElement() != null) {
// in case of a spread operator (`*list`),
// we actually want to get the generic parameter from the collection
wrapperType.arguments.first().type
} else {
wrapperType
}
return type
.takeUnless { it is ErrorType }
?.isNullable()
}
private data class Conversion(val replacementName: String? = null)
companion object {
private val uselessFqNames = mapOf(
FqName("kotlin.collections.orEmpty") to Conversion(),
FqName("kotlin.sequences.orEmpty") to Conversion(),
FqName("kotlin.text.orEmpty") to Conversion(),
FqName("kotlin.text.isNullOrEmpty") to Conversion("isEmpty"),
FqName("kotlin.text.isNullOrBlank") to Conversion("isBlank"),
FqName("kotlin.collections.isNullOrEmpty") to Conversion("isEmpty")
)
private val listOfNotNull = FqName("kotlin.collections.listOfNotNull")
}
}