183: CoroutineNonBlockingContextChecker for warning on blocking calls in coroutines (KT-15525)

This commit is contained in:
NikitaKatkov
2018-09-12 19:05:16 +03:00
committed by Nicolay Mitropolsky
parent 341f7c348a
commit d2536f207c
6 changed files with 182 additions and 0 deletions
+4
View File
@@ -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>
@@ -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);
}
}
@@ -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
@@ -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")
}
}