Skip to content
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

Merged
merged 16 commits into from
Sep 26, 2020
Merged

New rule: SuspendFunWithFlowReturnType #3098

merged 16 commits into from
Sep 26, 2020

Conversation

mhernand40
Copy link
Contributor

A Coroutines rule that reports any functions marked as suspend that also return a Flow.

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.

Addresses #3086

@codecov
Copy link

codecov bot commented Sep 25, 2020

Codecov Report

Merging #3098 into master will increase coverage by 0.15%.
The diff coverage is 86.95%.

Impacted file tree graph

@@             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     
Impacted Files Coverage Δ Complexity Δ
...t/rules/coroutines/SuspendFunWithFlowReturnType.kt 85.71% <85.71%> (ø) 7.00 <7.00> (?)
...osch/detekt/rules/coroutines/CoroutinesProvider.kt 100.00% <100.00%> (ø) 3.00 <0.00> (ø)
...urbosch/detekt/rules/bugs/ImplicitDefaultLocale.kt 74.19% <0.00%> (ø) 9.00% <0.00%> (ø%)
...osch/detekt/rules/complexity/ComplexityProvider.kt 100.00% <0.00%> (ø) 3.00% <0.00%> (ø%)
...kt/rules/complexity/ReplaceSafeCallChainWithRun.kt 86.66% <0.00%> (ø) 7.00% <0.00%> (?%)
...rbosch/detekt/rules/complexity/TooManyFunctions.kt 98.43% <0.00%> (+1.66%) 25.00% <0.00%> (ø%)
...lab/arturbosch/detekt/api/internal/Suppressions.kt 76.00% <0.00%> (+2.08%) 0.00% <0.00%> (ø%)
...rturbosch/detekt/rules/style/UnusedPrivateClass.kt 71.42% <0.00%> (+5.25%) 4.00% <0.00%> (ø%)
...gitlab/arturbosch/detekt/api/AnnotationExcluder.kt 71.42% <0.00%> (+35.06%) 7.00% <0.00%> (ø%)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e52f6f2...2b969b6. Read the comment docs.

@@ -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")
Copy link
Contributor Author

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.

@@ -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")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Member

Choose a reason for hiding this comment

The 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

* 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we link to a specific section in this document?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect!

override val issue = Issue(
id = "SuspendFunWithFlowReturnType",
severity = Severity.Minor,
description = "`suspend` modifier should not be used for functions returning Coroutines Flow",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you extend the description by providing the why for the user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Does this work? 8cfc906

Copy link
Member

@schalkms schalkms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 awesome first contribution

override val issue = Issue(
id = "SuspendFunWithFlowReturnType",
severity = Severity.Minor,
description = "`suspend` modifier should not be used for functions returning Coroutines " +
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TOL: "Coroutines Flow" or "Coroutine Flow"? 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coroutines Flow interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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. 🙂

}
}

private fun KotlinType.isCoroutineFlow(): Boolean {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would choose isCoroutinesFlow() :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arturbosch arturbosch added this to the 1.14.0 milestone Sep 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants