-
-
Notifications
You must be signed in to change notification settings - Fork 774
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New rule: SuspendFunWithFlowReturnType #3098
Changes from 14 commits
ec0fa79
edbdda3
ed306e7
a02869d
62a99b9
6bed5c8
b445068
d04cc26
dbb35d9
27dea52
8cfc906
1ca8248
30baa22
697fcc3
ad3d7bf
2b969b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ dependencies { | |
api("com.pinterest.ktlint:ktlint-core:${version.ktlint}") | ||
api("com.pinterest.ktlint:ktlint-ruleset-experimental:${version.ktlint}") | ||
api("org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2") | ||
api("org.assertj:assertj-core:3.16.1") | ||
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, when using Coroutines
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting this should be the same incompatibility we see in the intellij plugin with Kotlin 1.4 vs 1.3 |
||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
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 org.jetbrains.kotlin.js.descriptorUtils.getJetTypeFqName | ||
import org.jetbrains.kotlin.lexer.KtTokens | ||
import org.jetbrains.kotlin.psi.KtNamedFunction | ||
import org.jetbrains.kotlin.resolve.BindingContext | ||
import org.jetbrains.kotlin.types.KotlinType | ||
import org.jetbrains.kotlin.types.typeUtil.supertypes | ||
|
||
/** | ||
* Functions that return `Flow` from `kotlinx.coroutines.flow` should not be marked as `suspend`. | ||
* `Flows` are intended to be cold observable streams. The act of simply invoking a function that | ||
* returns a `Flow`, should not have any side effects. Only once collection begins against the | ||
* returned `Flow`, should work actually be done. | ||
* | ||
* See https://kotlinlang.org/docs/reference/coroutines/flow.html#flows-are-cold | ||
* | ||
* <noncompliant> | ||
* suspend fun observeSignals(): Flow<Unit> { | ||
* val pollingInterval = getPollingInterval() // Done outside of the flow builder block. | ||
* return flow { | ||
* while (true) { | ||
* delay(pollingInterval) | ||
* emit(Unit) | ||
* } | ||
* } | ||
* } | ||
* | ||
* private suspend fun getPollingInterval(): Long { | ||
* // Return the polling interval from some repository | ||
* // in a suspending manner. | ||
* } | ||
* </noncompliant> | ||
* | ||
* <compliant> | ||
* fun observeSignals(): Flow<Unit> { | ||
* return flow { | ||
* val pollingInterval = getPollingInterval() // Moved into the flow builder block. | ||
* while (true) { | ||
* delay(pollingInterval) | ||
* emit(Unit) | ||
* } | ||
* } | ||
* } | ||
* | ||
* private suspend fun getPollingInterval(): Long { | ||
* // Return the polling interval from some repository | ||
* // in a suspending manner. | ||
* } | ||
* </compliant> | ||
* | ||
* @requiresTypeResolution | ||
*/ | ||
class SuspendFunWithFlowReturnType(config: Config) : Rule(config) { | ||
|
||
override val issue = Issue( | ||
id = "SuspendFunWithFlowReturnType", | ||
severity = Severity.Minor, | ||
description = "`suspend` modifier should not be used for functions returning Coroutines " + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TOL: "Coroutines Flow" or "Coroutine Flow"? 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coroutines Flow interface? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm… the rule will also detect subtypes of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I decided to reword it a bit in 2b969b6. Let me know what you think. 🙂 |
||
"Flow. Flows are cold streams and invoking a function that returns one should not " + | ||
"produce any side effects.", | ||
debt = Debt.TEN_MINS | ||
) | ||
|
||
override fun visitNamedFunction(function: KtNamedFunction) { | ||
if (bindingContext == BindingContext.EMPTY) return | ||
val suspendModifier = function.modifierList?.getModifier(KtTokens.SUSPEND_KEYWORD) ?: return | ||
bindingContext[BindingContext.FUNCTION, function] | ||
?.returnType | ||
?.takeIf { it.isCoroutineFlow() } | ||
?.also { | ||
report( | ||
CodeSmell( | ||
issue = issue, | ||
entity = Entity.from(suspendModifier), | ||
message = "`suspend` function returns Coroutines Flow." | ||
) | ||
) | ||
} | ||
} | ||
|
||
private fun KotlinType.isCoroutineFlow(): Boolean { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TOL: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would choose There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. For consistency with the issue description, I will make that change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return sequence { | ||
yield(this@isCoroutineFlow) | ||
yieldAll(this@isCoroutineFlow.supertypes()) | ||
} | ||
.map { it.getJetTypeFqName(printTypeArguments = false) } | ||
.contains("kotlinx.coroutines.flow.Flow") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, AssertJ already declared on line 12.