-
-
Notifications
You must be signed in to change notification settings - Fork 755
/
UseIfEmptyOrIfBlank.kt
133 lines (119 loc) · 5.86 KB
/
UseIfEmptyOrIfBlank.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
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.isElseIf
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtIfExpression
import org.jetbrains.kotlin.psi.KtPrefixExpression
import org.jetbrains.kotlin.psi.KtThisExpression
import org.jetbrains.kotlin.psi.psiUtil.blockExpressionsOrSingle
import org.jetbrains.kotlin.psi.psiUtil.getPossiblyQualifiedCallExpression
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
/**
* This rule detects `isEmpty` or `isBlank` calls to assign a default value. They can be replaced with `ifEmpty` or
* `ifBlank` calls.
*
* <noncompliant>
* fun test(list: List<Int>, s: String) {
* val a = if (list.isEmpty()) listOf(1) else list
* val b = if (list.isNotEmpty()) list else listOf(2)
* val c = if (s.isBlank()) "foo" else s
* val d = if (s.isNotBlank()) s else "bar"
* }
* </noncompliant>
*
* <compliant>
* fun test(list: List<Int>, s: String) {
* val a = list.ifEmpty { listOf(1) }
* val b = list.ifEmpty { listOf(2) }
* val c = s.ifBlank { "foo" }
* val d = s.ifBlank { "bar" }
* }
* </compliant>
*
*/
@RequiresTypeResolution
@Suppress("ReturnCount", "ComplexMethod")
class UseIfEmptyOrIfBlank(config: Config = Config.empty) : Rule(config) {
override val issue: Issue = Issue(
"UseIfEmptyOrIfBlank",
Severity.Style,
"Use `ifEmpty` or `ifBlank` instead of `isEmpty` or `isBlank` to assign a default value.",
Debt.FIVE_MINS
)
override fun visitIfExpression(expression: KtIfExpression) {
super.visitIfExpression(expression)
if (expression.isElseIf()) return
val thenExpression = expression.then ?: return
val elseExpression = expression.`else` ?: return
if (elseExpression is KtIfExpression) return
val (condition, isNegatedCondition) = expression.condition() ?: return
val conditionCallExpression = condition.getPossiblyQualifiedCallExpression() ?: return
val conditionCalleeExpression = conditionCallExpression.calleeExpression ?: return
val conditionCalleeExpressionText = conditionCalleeExpression.text
if (conditionCalleeExpressionText !in conditionFunctionShortNames) return
val replacement = conditionCallExpression.replacement() ?: return
val selfBranch = if (isNegatedCondition xor replacement.negativeCondition) thenExpression else elseExpression
val selfValueExpression = selfBranch.blockExpressionsOrSingle().singleOrNull() ?: return
if (condition is KtDotQualifiedExpression) {
if (selfValueExpression.text != condition.receiverExpression.text) return
} else if (selfValueExpression !is KtThisExpression) {
return
}
val message =
"This '$conditionCalleeExpressionText' call can be replaced with '${replacement.replacementFunctionName}'"
report(CodeSmell(issue, Entity.from(conditionCalleeExpression), message))
}
private fun KtIfExpression.condition(): Pair<KtExpression, Boolean>? {
val condition = this.condition ?: return null
return if (condition is KtPrefixExpression) {
if (condition.operationToken != KtTokens.EXCL) return null
val baseExpression = condition.baseExpression ?: return null
baseExpression to true
} else {
condition to false
}
}
private fun KtCallExpression.replacement(): Replacement? {
val descriptor = getResolvedCall(bindingContext)?.resultingDescriptor ?: return null
val receiverParameter = descriptor.dispatchReceiverParameter ?: descriptor.extensionReceiverParameter
val receiverType = receiverParameter?.type ?: return null
if (KotlinBuiltIns.isArrayOrPrimitiveArray(receiverType)) return null
val conditionCallFqName = descriptor.fqNameOrNull() ?: return null
return replacements[conditionCallFqName]
}
private data class Replacement(
val conditionFunctionFqName: FqName,
val replacementFunctionName: String,
val negativeCondition: Boolean = false
)
companion object {
private const val ifBlank = "ifBlank"
private const val ifEmpty = "ifEmpty"
private val replacements = listOf(
Replacement(FqName("kotlin.text.isBlank"), ifBlank),
Replacement(FqName("kotlin.text.isEmpty"), ifEmpty),
Replacement(FqName("kotlin.collections.List.isEmpty"), ifEmpty),
Replacement(FqName("kotlin.collections.Set.isEmpty"), ifEmpty),
Replacement(FqName("kotlin.collections.Map.isEmpty"), ifEmpty),
Replacement(FqName("kotlin.collections.Collection.isEmpty"), ifEmpty),
Replacement(FqName("kotlin.text.isNotBlank"), ifBlank, negativeCondition = true),
Replacement(FqName("kotlin.text.isNotEmpty"), ifEmpty, negativeCondition = true),
Replacement(FqName("kotlin.collections.isNotEmpty"), ifEmpty, negativeCondition = true),
Replacement(FqName("kotlin.String.isEmpty"), ifEmpty)
).associateBy { it.conditionFunctionFqName }
private val conditionFunctionShortNames = replacements.keys.map { it.shortName().asString() }.toSet()
}
}