-
-
Notifications
You must be signed in to change notification settings - Fork 755
/
OptionalUnit.kt
145 lines (134 loc) · 5.79 KB
/
OptionalUnit.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
package io.gitlab.arturbosch.detekt.rules.style.optional
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.rules.isOverride
import org.jetbrains.kotlin.cfg.WhenChecker
import org.jetbrains.kotlin.js.translate.callTranslator.getReturnType
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtIfExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtTypeReference
import org.jetbrains.kotlin.psi.KtWhenExpression
import org.jetbrains.kotlin.psi.psiUtil.siblings
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.util.getType
import org.jetbrains.kotlin.types.typeUtil.isNothing
import org.jetbrains.kotlin.types.typeUtil.isTypeParameter
import org.jetbrains.kotlin.types.typeUtil.isUnit
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
/**
* It is not necessary to define a return type of `Unit` on functions or to specify a lone Unit statement.
* This rule detects and reports instances where the `Unit` return type is specified on functions and the occurrences
* of a lone Unit statement.
*
* <noncompliant>
* fun foo(): Unit {
* return Unit
* }
* fun foo() = Unit
*
* fun doesNothing() {
* Unit
* }
* </noncompliant>
*
* <compliant>
* fun foo() { }
*
* // overridden no-op functions are allowed
* override fun foo() = Unit
* </compliant>
*/
@Suppress("ViolatesTypeResolutionRequirements")
class OptionalUnit(config: Config = Config.empty) : Rule(config) {
override val issue = Issue(
javaClass.simpleName,
Severity.Style,
"Return type of `Unit` is unnecessary and can be safely removed.",
Debt.FIVE_MINS
)
override fun visitNamedFunction(function: KtNamedFunction) {
val typeReference = function.typeReference
if (typeReference != null) {
checkFunctionWithExplicitReturnType(function, typeReference)
} else if (!function.isOverride()) {
checkFunctionWithInferredReturnType(function)
}
super.visitNamedFunction(function)
}
override fun visitBlockExpression(expression: KtBlockExpression) {
val statements = expression.statements
val lastStatement = statements.lastOrNull() ?: return
statements
.filter {
when {
it !is KtNameReferenceExpression || it.text != UNIT -> false
it != lastStatement || bindingContext == BindingContext.EMPTY -> true
!it.isUsedAsExpression(bindingContext) -> true
else -> {
val prev =
it.siblings(forward = false, withItself = false).firstIsInstanceOrNull<KtExpression>()
prev?.getType(bindingContext)?.isUnit() == true && prev.canBeUsedAsValue()
}
}
}
.onEach {
report(
CodeSmell(
issue,
Entity.from(expression),
"A single Unit expression is unnecessary and can safely be removed."
)
)
}
super.visitBlockExpression(expression)
}
private fun KtExpression.canBeUsedAsValue(): Boolean {
return when (this) {
is KtIfExpression -> {
val elseExpression = `else`
if (elseExpression is KtIfExpression) elseExpression.canBeUsedAsValue() else elseExpression != null
}
is KtWhenExpression ->
entries.lastOrNull()?.elseKeyword != null || WhenChecker.getMissingCases(this, bindingContext).isEmpty()
else ->
true
}
}
private fun checkFunctionWithExplicitReturnType(function: KtNamedFunction, typeReference: KtTypeReference) {
val typeElementText = typeReference.typeElement?.text
if (typeElementText == UNIT) {
val initializer = function.initializer
if (initializer?.isGenericOrNothingType() == true) return
report(CodeSmell(issue, Entity.from(typeReference), createMessage(function)))
}
}
private fun checkFunctionWithInferredReturnType(function: KtNamedFunction) {
val referenceExpression = function.bodyExpression as? KtNameReferenceExpression
if (referenceExpression != null && referenceExpression.text == UNIT) {
report(CodeSmell(issue, Entity.from(referenceExpression), createMessage(function)))
}
}
private fun createMessage(function: KtNamedFunction) = "The function ${function.name} " +
"defines a return type of Unit. This is unnecessary and can safely be removed."
private fun KtExpression.isGenericOrNothingType(): Boolean {
if (bindingContext == BindingContext.EMPTY) return false
val isGenericType = getResolvedCall(bindingContext)?.getReturnType()?.isTypeParameter() == true
val isNothingType = getType(bindingContext)?.isNothing() == true
// Either the function initializer returns Nothing or it is a generic function
// into which Unit is passed, but not both.
return (isGenericType && !isNothingType) || (isNothingType && !isGenericType)
}
companion object {
private const val UNIT = "Unit"
}
}