New rule: SuspendFunWithFlowReturnType#3098
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3098 +/- ##
============================================
+ Coverage 79.28% 79.43% +0.15%
- Complexity 2577 2591 +14
============================================
Files 435 437 +2
Lines 7766 7813 +47
Branches 1482 1484 +2
============================================
+ Hits 6157 6206 +49
Misses 819 819
+ Partials 790 788 -2
Continue to review full report at Codecov.
|
| 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") |
There was a problem hiding this comment.
FYI, AssertJ already declared on line 12.
| 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.
FYI, when using Coroutines 1.3.9 which is currently the latest version, :detekt-gradle-plugin:test seems to fail with the following error:
org.gradle.api.internal.tasks.testing.TestSuiteExecutionException: Could not complete execution for Gradle Test Executor 5.
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:63)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at com.sun.proxy.$Proxy2.stop(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.stop(TestWorker.java:133)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
at java.lang.Thread.run(Thread.java:748)
Caused by: org.junit.platform.commons.JUnitException: TestEngine with ID 'spek2' failed to discover tests
at org.junit.platform.launcher.core.DefaultLauncher.discoverEngineRoot(DefaultLauncher.java:189)
at org.junit.platform.launcher.core.DefaultLauncher.discoverRoot(DefaultLauncher.java:168)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:132)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:61)
... 25 more
Caused by: java.lang.NoSuchMethodError: kotlin.jvm.internal.FunctionReferenceImpl.<init>(ILjava/lang/Class;Ljava/lang/String;Ljava/lang/String;I)V
at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.<init>(SafeCollector.kt)
at kotlinx.coroutines.flow.internal.SafeCollectorKt.<clinit>(SafeCollector.kt:15)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:73)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:55)
at org.spekframework.spek2.runtime.SpekRuntime$filterScopes$2.invokeSuspend(SpekRuntime.kt:39)
at ???(Coroutine boundary.?(?)
at org.spekframework.spek2.runtime.SpekRuntime$discover$time$1$1.invokeSuspend(SpekRuntime.kt:49)
at org.spekframework.spek2.runtime.ExecutorsKt$doRunBlocking$1.invokeSuspend(executors.kt:8)
Caused by: java.lang.NoSuchMethodError: kotlin.jvm.internal.FunctionReferenceImpl.<init>(ILjava/lang/Class;Ljava/lang/String;Ljava/lang/String;I)V
at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.<init>(SafeCollector.kt)
at kotlinx.coroutines.flow.internal.SafeCollectorKt.<clinit>(SafeCollector.kt:15)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:73)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:55)
at org.spekframework.spek2.runtime.SpekRuntime$filterScopes$2.invokeSuspend(SpekRuntime.kt:39)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
There was a problem hiding this comment.
Interesting this should be the same incompatibility we see in the intellij plugin with Kotlin 1.4 vs 1.3
| * 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 |
There was a problem hiding this comment.
Can we link to a specific section in this document?
There was a problem hiding this comment.
How about this? 27dea52
There is no specific section in the document discouraging suspend functions that return Flow. However when it comes to other Reactive Stream libraries, for example RxJava, it is typically discouraged to "do work" outside of the subscription.
| override val issue = Issue( | ||
| id = "SuspendFunWithFlowReturnType", | ||
| severity = Severity.Minor, | ||
| description = "`suspend` modifier should not be used for functions returning Coroutines Flow", |
There was a problem hiding this comment.
Can you extend the description by providing the why for the user?
schalkms
left a comment
There was a problem hiding this comment.
👍 awesome first contribution
…uspend-fun-with-flow-return-type
| 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.
TOL: "Coroutines Flow" or "Coroutine Flow"? 🤔
There was a problem hiding this comment.
Hmm… the rule will also detect subtypes of Flow, e.g., StateFlow, MutableStateFlow, and eventually SharedFlow. I am not so sure that putting an emphasis on "interface" adds much value to the description. For example, RxJava's Observable is actually an abstract class. Flow could have been designed as an abstract class and yet the rule description, as it currently is, would still apply. However, if you still feel strongly about it, I can make that change. 🙂
There was a problem hiding this comment.
Okay, I decided to reword it a bit in 2b969b6. Let me know what you think. 🙂
| } | ||
| } | ||
|
|
||
| private fun KotlinType.isCoroutineFlow(): Boolean { |
There was a problem hiding this comment.
TOL: isCoroutineFlow() or isCoroutinesFlow()? This is somewhat inconsistent with "Coroutines Flow" in the issue description. It is not always clear when to prefer plural over singular. 🤔
There was a problem hiding this comment.
I would choose isCoroutinesFlow() :)
There was a problem hiding this comment.
Sounds good. For consistency with the issue description, I will make that change.
A Coroutines rule that reports any functions marked as
suspendthat also return a Flow.Flowsare intended to be cold observable streams. The act of simply invoking a function that returns aFlow, should not have any side effects. Only once collection begins against the returnedFlow, should work actually be done.Addresses #3086