-
-
Notifications
You must be signed in to change notification settings - Fork 755
/
SuspendFunInsideRunCatching.kt
140 lines (132 loc) · 5.57 KB
/
SuspendFunInsideRunCatching.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
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.RequiresTypeResolution
import org.jetbrains.kotlin.backend.common.descriptors.isSuspend
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypesAndPredicate
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
/**
* Suspend functions should not be called inside `runCatching` as `runCatching` catches
* all the exception while for Coroutine cooperative cancellation to work, we have to
* never catch the `CancellationException` exception or rethrowing it again if caught
*
* See https://kotlinlang.org/docs/cancellation-and-timeouts.html#cancellation-is-cooperative
*
* <noncompliant>
* suspend fun bar(delay: Long) {
* check(delay <= 1_000L)
* delay(delay)
* }
*
* suspend fun foo() {
* runCatching {
* bar(1_000L)
* }
* }
* </noncompliant>
*
* <compliant>
* suspend fun bar(delay: Long) {
* check(delay <= 1_000L)
* delay(delay)
* }
*
* suspend fun foo() {
* try {
* bar(1_000L)
* } catch (e: IllegalStateException) {
* // handle error
* }
* }
*
* // Alternate
* suspend fun foo() {
* bar(1_000L)
* }
* </compliant>
*
*/
@RequiresTypeResolution
class SuspendFunInsideRunCatching(config: Config) : Rule(config) {
override val issue = Issue(
id = "SuspendFunInsideRunCatching",
severity = Severity.Minor,
description = "The `suspend` functions should be called inside `runCatching` block as it also swallows " +
"`CancellationException` which is important for cooperative cancellation." +
"You should either use specific `try-catch` only catching exception that you are expecting" +
" or rethrow the `CancellationException` if already caught",
debt = Debt.TEN_MINS
)
override fun visitCallExpression(expression: KtCallExpression) {
super.visitCallExpression(expression)
val resultingDescriptor = expression.getResolvedCall(bindingContext)?.resultingDescriptor
resultingDescriptor ?: return
if (resultingDescriptor.fqNameSafe != RUN_CATCHING_FQ) return
expression.forEachDescendantOfType<KtCallExpression> { descendant ->
if (descendant.getResolvedCall(bindingContext)?.resultingDescriptor?.isSuspend == true && shouldReport(
resultingDescriptor,
descendant,
bindingContext,
)
) {
val message =
"The suspend function call ${descendant.text} is inside `runCatching`. You should either " +
"use specific `try-catch` only catching exception that you are expecting or rethrow the " +
"`CancellationException` if already caught"
report(
CodeSmell(
issue,
Entity.from(expression),
message
)
)
}
}
}
private fun shouldReport(
runCatchingCallableDescriptor: CallableDescriptor,
callExpression: KtCallExpression,
bindingContext: BindingContext,
): Boolean {
val firstNonInlineOrRunCatchingParent =
callExpression.getParentOfTypesAndPredicate(true, KtCallExpression::class.java) { parentCallExp ->
val parentCallFunctionDescriptor =
parentCallExp.getResolvedCall(bindingContext)?.resultingDescriptor as? FunctionDescriptor
parentCallFunctionDescriptor ?: return@getParentOfTypesAndPredicate false
val isParentRunCatching = parentCallFunctionDescriptor.fqNameSafe == RUN_CATCHING_FQ
val isInline = parentCallFunctionDescriptor.isInline
val noInlineAndCrossInlineValueParametersIndex =
parentCallFunctionDescriptor.valueParameters.filter { valueParameterDescriptor ->
valueParameterDescriptor.isCrossinline || valueParameterDescriptor.isNoinline
}.map {
it.index
}
val callExpressionIndexInParentCall = parentCallExp.valueArguments.indexOfFirst { valueArgument ->
valueArgument?.findDescendantOfType<KtCallExpression> {
it == callExpression
} != null
}
isParentRunCatching ||
isInline.not() ||
noInlineAndCrossInlineValueParametersIndex.contains(callExpressionIndexInParentCall)
}
return firstNonInlineOrRunCatchingParent.getResolvedCall(bindingContext)?.resultingDescriptor ==
runCatchingCallableDescriptor
}
companion object {
private val RUN_CATCHING_FQ = FqName("kotlin.runCatching")
}
}