183: CoroutineNonBlockingContextChecker for warning on blocking calls in coroutines (KT-15525)
This commit is contained in:
committed by
Nicolay Mitropolsky
parent
341f7c348a
commit
d2536f207c
@@ -3140,6 +3140,10 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio.
|
||||
<idePlatformKindTooling implementation="org.jetbrains.kotlin.idea.core.platform.impl.CommonIdePlatformKindTooling"/>
|
||||
</extensions>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij.codeInsight">
|
||||
<nonBlockingContextChecker implementation="org.jetbrains.kotlin.idea.inspections.blockingCallsDetection.CoroutineNonBlockingContextChecker"/>
|
||||
</extensions>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij.jvm">
|
||||
<declarationSearcher language="kotlin" implementationClass="org.jetbrains.kotlin.idea.jvm.KotlinDeclarationSearcher"/>
|
||||
</extensions>
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.inspections.blockingCallsDetection
|
||||
|
||||
import com.intellij.codeInspection.blockingCallsDetection.NonBlockingContextChecker
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import org.jetbrains.kotlin.builtins.getReceiverTypeFromFunctionType
|
||||
import org.jetbrains.kotlin.builtins.isBuiltinFunctionalType
|
||||
import org.jetbrains.kotlin.builtins.isSuspendFunctionType
|
||||
import org.jetbrains.kotlin.config.LanguageFeature
|
||||
import org.jetbrains.kotlin.config.LanguageVersion
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
|
||||
import org.jetbrains.kotlin.idea.project.getLanguageVersionSettings
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.parents
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getParameterForArgument
|
||||
import org.jetbrains.kotlin.resolve.calls.checkers.isRestrictsSuspensionReceiver
|
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
||||
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
|
||||
|
||||
|
||||
class CoroutineNonBlockingContextChecker : NonBlockingContextChecker {
|
||||
|
||||
override fun isApplicable(file: PsiFile): Boolean {
|
||||
val languageVersionSettings = file.project.getLanguageVersionSettings()
|
||||
return languageVersionSettings.supportsFeature(LanguageFeature.Coroutines) &&
|
||||
languageVersionSettings.languageVersion >= LanguageVersion.KOTLIN_1_3
|
||||
}
|
||||
|
||||
override fun isContextNonBlockingFor(element: PsiElement): Boolean {
|
||||
if (element !is KtCallExpression)
|
||||
return false
|
||||
|
||||
val containingLambda = element.parents
|
||||
.firstOrNull { it is KtLambdaExpression && it.analyze().get(BindingContext.LAMBDA_INVOCATIONS, it) == null }
|
||||
val containingArgument = PsiTreeUtil.getParentOfType(containingLambda, KtValueArgument::class.java)
|
||||
if (containingArgument != null) {
|
||||
val callExpression = PsiTreeUtil.getParentOfType(element, KtCallExpression::class.java) ?: return false
|
||||
val call = callExpression.resolveToCall(BodyResolveMode.FULL) ?: return false
|
||||
|
||||
val hasBlockingAnnotation = call.getFirstArgument()?.resolveToCall()
|
||||
?.resultingDescriptor?.annotations?.hasAnnotation(FqName(BLOCKING_CONTEXT_ANNOTATION))
|
||||
if (hasBlockingAnnotation == true)
|
||||
return false
|
||||
|
||||
val parameterForArgument = call.getParameterForArgument(containingArgument) ?: return false
|
||||
val type = parameterForArgument.returnType ?: return false
|
||||
|
||||
val hasRestrictSuspensionAnnotation = if (type.isBuiltinFunctionalType) {
|
||||
type.getReceiverTypeFromFunctionType()?.isRestrictsSuspensionReceiver(element.project.getLanguageVersionSettings())
|
||||
} else null
|
||||
|
||||
return hasRestrictSuspensionAnnotation != true && type.isSuspendFunctionType
|
||||
}
|
||||
|
||||
val callingMethod = PsiTreeUtil.getParentOfType(element, KtNamedFunction::class.java) ?: return false
|
||||
return callingMethod.hasModifier(KtTokens.SUSPEND_KEYWORD)
|
||||
}
|
||||
|
||||
private fun ResolvedCall<*>.getFirstArgument(): KtExpression? =
|
||||
valueArgumentsByIndex?.firstOrNull()?.arguments?.firstOrNull()?.getArgumentExpression()
|
||||
|
||||
companion object {
|
||||
private const val BLOCKING_CONTEXT_ANNOTATION = "org.jetbrains.annotations.BlockingContext"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
@file:Suppress("UNUSED_PARAMETER")
|
||||
|
||||
import kotlin.coroutines.*
|
||||
import org.jetbrains.annotations.BlockingContext
|
||||
import kotlin.coroutines.experimental.buildSequence
|
||||
import java.lang.Thread.sleep
|
||||
|
||||
suspend fun testFunction() {
|
||||
@BlockingContext
|
||||
val ctx = getContext()
|
||||
// no warnings with @BlockingContext annotation on ctx object
|
||||
withContext(ctx) {Thread.sleep (2)}
|
||||
|
||||
// no warnings with @BlockingContext annotation on getContext() method
|
||||
withContext(getContext()) {Thread.sleep(3)}
|
||||
|
||||
withContext(getNonBlockingContext()) {
|
||||
Thread.<warning descr="Inappropriate blocking method call">sleep</warning>(3);
|
||||
}
|
||||
}
|
||||
|
||||
@BlockingContext
|
||||
fun getContext(): CoroutineContext = TODO()
|
||||
|
||||
fun getNonBlockingContext(): CoroutineContext = TODO()
|
||||
|
||||
suspend fun <T> withContext(
|
||||
context: CoroutineContext,
|
||||
f: suspend () -> T
|
||||
) {
|
||||
TODO()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
@file:Suppress("UNUSED_PARAMETER")
|
||||
|
||||
import kotlin.coroutines.*
|
||||
import java.lang.Thread.sleep
|
||||
|
||||
class InsideCoroutine {
|
||||
suspend fun example1() {
|
||||
Thread.<warning descr="Inappropriate blocking method call">sleep</warning>(1);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
@file:Suppress("UNUSED_PARAMETER")
|
||||
|
||||
import java.lang.Thread.sleep
|
||||
import kotlin.coroutines.RestrictsSuspension
|
||||
|
||||
suspend fun testFunction() {
|
||||
// @RestrictsSuspension annotation allows blocking calls
|
||||
withRestrictedReceiver({Thread.sleep(0)}, {Thread.sleep(1)})
|
||||
|
||||
withSimpleReceiver({Thread.<warning descr="Inappropriate blocking method call">sleep</warning>(2)})
|
||||
}
|
||||
|
||||
fun withRestrictedReceiver(firstParam: suspend Test1.() -> Unit, secondParam: () -> Unit) {}
|
||||
|
||||
fun withSimpleReceiver(firstParam: suspend Test2.() -> Unit) {}
|
||||
|
||||
@RestrictsSuspension
|
||||
class Test1
|
||||
|
||||
class Test2
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.idea.inspections
|
||||
|
||||
import com.intellij.codeInspection.blockingCallsDetection.BlockingMethodInNonBlockingContextInspection
|
||||
import com.intellij.testFramework.LightProjectDescriptor
|
||||
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
|
||||
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor
|
||||
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
|
||||
import org.jetbrains.kotlin.idea.test.configureCompilerOptions
|
||||
|
||||
class CoroutineNonBlockingontextDetectionTest: KotlinLightCodeInsightFixtureTestCase() {
|
||||
override fun getTestDataPath(): String
|
||||
= PluginTestCaseBase.getTestDataPathBase() + "/inspections/blockingCallsDetection"
|
||||
|
||||
override fun getProjectDescriptor(): LightProjectDescriptor = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
myFixture.addClass("""package org.jetbrains.annotations; public @interface BlockingContext {}""")
|
||||
myFixture.enableInspections(BlockingMethodInNonBlockingContextInspection::class.java)
|
||||
}
|
||||
|
||||
fun testSimpleCoroutineScope() {
|
||||
myFixture.configureByFile("InsideCoroutine.kt")
|
||||
myFixture.testHighlighting(true, false, false, "InsideCoroutine.kt")
|
||||
}
|
||||
|
||||
fun testCoroutineContextCheck() {
|
||||
myFixture.configureByFiles("ContextCheck.kt")
|
||||
myFixture.testHighlighting(true, false, false, "ContextCheck.kt")
|
||||
}
|
||||
|
||||
fun testLambdaReceiverType() {
|
||||
myFixture.configureByFile("LambdaReceiverTypeCheck.kt")
|
||||
myFixture.testHighlighting(true, false, false, "LambdaReceiverTypeCheck.kt")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user