-
-
Notifications
You must be signed in to change notification settings - Fork 755
/
RedundantSuspendModifier.kt
132 lines (122 loc) · 5.91 KB
/
RedundantSuspendModifier.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
package io.gitlab.arturbosch.detekt.rules.coroutines
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.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.Modality
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.VariableDescriptorWithAccessors
import org.jetbrains.kotlin.descriptors.accessors
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtOperationReferenceExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.BindingContext.DECLARATION_TO_DESCRIPTOR
import org.jetbrains.kotlin.resolve.BindingContext.DELEGATED_PROPERTY_RESOLVED_CALL
import org.jetbrains.kotlin.resolve.BindingContext.LOOP_RANGE_HAS_NEXT_RESOLVED_CALL
import org.jetbrains.kotlin.resolve.BindingContext.LOOP_RANGE_ITERATOR_RESOLVED_CALL
import org.jetbrains.kotlin.resolve.BindingContext.LOOP_RANGE_NEXT_RESOLVED_CALL
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
/*
* Based on code from Kotlin project:
* https://github.com/JetBrains/kotlin/blob/v1.3.61/idea/src/org/jetbrains/kotlin/idea/inspections/RedundantSuspendModifierInspection.kt
*/
/**
* `suspend` modifier should only be used where needed, otherwise the function can only be used from other suspending
* functions. This needlessly restricts use of the function and should be avoided by removing the `suspend` modifier
* where it's not needed.
*
* <noncompliant>
* suspend fun normalFunction() {
* println("string")
* }
* </noncompliant>
*
* <compliant>
* fun normalFunction() {
* println("string")
* }
* </compliant>
*
*/
@RequiresTypeResolution
@ActiveByDefault(since = "1.21.0")
class RedundantSuspendModifier(config: Config) : Rule(config) {
override val issue = Issue(
"RedundantSuspendModifier",
Severity.Minor,
"The `suspend` modifier is only needed for functions that contain suspending calls.",
Debt.FIVE_MINS
)
override fun visitCondition(root: KtFile) = bindingContext != BindingContext.EMPTY && super.visitCondition(root)
override fun visitNamedFunction(function: KtNamedFunction) {
val suspendModifier = function.modifierList?.getModifier(KtTokens.SUSPEND_KEYWORD) ?: return
if (!function.hasBody()) return
if (function.hasModifier(KtTokens.OVERRIDE_KEYWORD)) return
val descriptor = bindingContext[BindingContext.FUNCTION, function] ?: return
if (descriptor.modality == Modality.OPEN) return
if (!function.anyDescendantOfType<KtExpression> { it.hasSuspendCalls() }) {
report(CodeSmell(issue, Entity.from(suspendModifier), "Function has redundant `suspend` modifier."))
}
}
private fun KtExpression.isValidCandidateExpression(): Boolean {
return when (this) {
is KtOperationReferenceExpression, is KtForExpression, is KtProperty, is KtNameReferenceExpression -> true
else -> {
val parent = parent
if (parent is KtCallExpression && parent.calleeExpression == this) true
else this is KtCallExpression && this.calleeExpression is KtCallExpression
}
}
}
private fun KtExpression.hasSuspendCalls(): Boolean {
if (!isValidCandidateExpression()) return false
return when (this) {
is KtForExpression -> {
val iteratorResolvedCall = bindingContext[LOOP_RANGE_ITERATOR_RESOLVED_CALL, loopRange]
val loopRangeHasNextResolvedCall = bindingContext[LOOP_RANGE_HAS_NEXT_RESOLVED_CALL, loopRange]
val loopRangeNextResolvedCall = bindingContext[LOOP_RANGE_NEXT_RESOLVED_CALL, loopRange]
listOf(iteratorResolvedCall, loopRangeHasNextResolvedCall, loopRangeNextResolvedCall).any {
it?.resultingDescriptor?.isSuspend == true
}
}
is KtProperty -> {
if (hasDelegateExpression()) {
val variableDescriptor =
bindingContext[DECLARATION_TO_DESCRIPTOR, this] as? VariableDescriptorWithAccessors
val accessors = variableDescriptor?.accessors.orEmpty()
accessors.any { accessor ->
val delegatedFunctionDescriptor =
bindingContext[DELEGATED_PROPERTY_RESOLVED_CALL, accessor]?.resultingDescriptor
delegatedFunctionDescriptor?.isSuspend == true
}
} else {
false
}
}
else -> {
val resolvedCall = getResolvedCall(bindingContext)
if ((resolvedCall?.resultingDescriptor as? FunctionDescriptor)?.isSuspend == true) true
else {
val propertyDescriptor = resolvedCall?.resultingDescriptor as? PropertyDescriptor
val s = propertyDescriptor?.fqNameSafe?.asString()
s?.startsWith("kotlin.coroutines.") == true && s.endsWith(".coroutineContext")
}
}
}
}
}