/
RedundantHigherOrderMapUsage.kt
148 lines (135 loc) · 5.83 KB
/
RedundantHigherOrderMapUsage.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
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.ValueParameterDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFunctionLiteral
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtReturnExpression
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.unpackFunctionLiteral
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.bindingContextUtil.getTargetFunctionDescriptor
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.immediateSupertypes
/**
* Redundant maps add complexity to the code and accomplish nothing. They should be removed or replaced with the proper
* operator.
*
* <noncompliant>
* fun foo(list: List<Int>): List<Int> {
* return list
* .filter { it > 5 }
* .map { it }
* }
*
* fun bar(list: List<Int>): List<Int> {
* return list
* .filter { it > 5 }
* .map {
* doSomething(it)
* it
* }
* }
*
* fun baz(set: Set<Int>): List<Int> {
* return set.map { it }
* }
* </noncompliant>
*
* <compliant>
* fun foo(list: List<Int>): List<Int> {
* return list
* .filter { it > 5 }
* }
*
* fun bar(list: List<Int>): List<Int> {
* return list
* .filter { it > 5 }
* .onEach {
* doSomething(it)
* }
* }
*
* fun baz(set: Set<Int>): List<Int> {
* return set.toList()
* }
* </compliant>
*
*/
@RequiresTypeResolution
@Suppress("ReturnCount")
class RedundantHigherOrderMapUsage(config: Config = Config.empty) : Rule(config) {
override val issue: Issue = Issue(
javaClass.simpleName,
Severity.Style,
"Checks for redundant 'map' calls, which can be removed.",
Debt.FIVE_MINS
)
override fun visitCallExpression(expression: KtCallExpression) {
super.visitCallExpression(expression)
if (bindingContext == BindingContext.EMPTY) return
val calleeExpression = expression.calleeExpression
if (calleeExpression?.text != "map") return
val functionLiteral = expression.lambda()?.functionLiteral
val lambdaStatements = functionLiteral?.bodyExpression?.statements ?: return
val resolvedCall = expression.getResolvedCall(bindingContext) ?: return
if (resolvedCall.resultingDescriptor.fqNameOrNull() !in mapFqNames) return
val receiverType = resolvedCall.extensionReceiver?.type ?: return
val receiverIsList = receiverType.isInheritorOf(listFqName)
val receiverIsSet = receiverType.isInheritorOf(setFqName)
val receiverIsSequence = receiverType.isInheritorOf(sequenceFqName)
if (!receiverIsList && !receiverIsSet && !receiverIsSequence) return
if (!functionLiteral.isRedundant(lambdaStatements)) return
val message = when {
lambdaStatements.size != 1 -> "This 'map' call can be replaced with 'onEach' or 'forEach'."
receiverIsSet -> "This 'map' call can be replaced with 'toList'."
else -> "This 'map' call can be removed."
}
report(CodeSmell(issue, Entity.from(calleeExpression), message))
}
private fun KtCallExpression.lambda(): KtLambdaExpression? {
val argument = lambdaArguments.singleOrNull() ?: valueArguments.singleOrNull()
val lambda = argument?.getArgumentExpression()?.unpackFunctionLiteral() ?: return null
if (lambda.valueParameters.firstOrNull()?.destructuringDeclaration != null) return null
return lambda
}
private fun KotlinType.isInheritorOf(fqName: FqName): Boolean =
fqNameOrNull() == fqName || immediateSupertypes().any { it.fqNameOrNull() == fqName }
private fun KtFunctionLiteral.isRedundant(lambdaStatements: List<KtExpression>): Boolean {
val lambdaDescriptor = bindingContext[BindingContext.FUNCTION, this] ?: return false
val lambdaParameter = lambdaDescriptor.valueParameters.singleOrNull() ?: return false
val lastStatement = lambdaStatements.lastOrNull() ?: return false
if (!lastStatement.isReferenceTo(lambdaParameter)) return false
val returnExpressions = collectDescendantsOfType<KtReturnExpression> {
it != lastStatement && it.getTargetFunctionDescriptor(bindingContext) == lambdaDescriptor
}
return returnExpressions.all { it.isReferenceTo(lambdaParameter) }
}
private fun KtExpression.isReferenceTo(descriptor: ValueParameterDescriptor): Boolean {
val nameReference = if (this is KtReturnExpression) {
this.returnedExpression
} else {
this
} as? KtNameReferenceExpression
return nameReference?.getResolvedCall(bindingContext)?.resultingDescriptor == descriptor
}
companion object {
private val mapFqNames = listOf(FqName("kotlin.collections.map"), FqName("kotlin.sequences.map"))
private val listFqName = FqName("kotlin.collections.List")
private val setFqName = FqName("kotlin.collections.Set")
private val sequenceFqName = FqName("kotlin.sequences.Sequence")
}
}