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"/>
|
<idePlatformKindTooling implementation="org.jetbrains.kotlin.idea.core.platform.impl.CommonIdePlatformKindTooling"/>
|
||||||
</extensions>
|
</extensions>
|
||||||
|
|
||||||
|
<extensions defaultExtensionNs="com.intellij.codeInsight">
|
||||||
|
<nonBlockingContextChecker implementation="org.jetbrains.kotlin.idea.inspections.blockingCallsDetection.CoroutineNonBlockingContextChecker"/>
|
||||||
|
</extensions>
|
||||||
|
|
||||||
<extensions defaultExtensionNs="com.intellij.jvm">
|
<extensions defaultExtensionNs="com.intellij.jvm">
|
||||||
<declarationSearcher language="kotlin" implementationClass="org.jetbrains.kotlin.idea.jvm.KotlinDeclarationSearcher"/>
|
<declarationSearcher language="kotlin" implementationClass="org.jetbrains.kotlin.idea.jvm.KotlinDeclarationSearcher"/>
|
||||||
</extensions>
|
</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