diff --git a/idea/src/META-INF/plugin.xml.183 b/idea/src/META-INF/plugin.xml.183 index 3b67ec5a0d6..7b33983f439 100644 --- a/idea/src/META-INF/plugin.xml.183 +++ b/idea/src/META-INF/plugin.xml.183 @@ -3140,6 +3140,10 @@ The Kotlin plugin provides language support in IntelliJ IDEA and Android Studio. + + + + diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/blockingCallsDetection/CoroutineNonBlockingContextChecker.kt.183 b/idea/src/org/jetbrains/kotlin/idea/inspections/blockingCallsDetection/CoroutineNonBlockingContextChecker.kt.183 new file mode 100644 index 00000000000..4fb01f7e247 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/blockingCallsDetection/CoroutineNonBlockingContextChecker.kt.183 @@ -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" + } +} \ No newline at end of file diff --git a/idea/testData/inspections/blockingCallsDetection/ContextCheck.kt.183 b/idea/testData/inspections/blockingCallsDetection/ContextCheck.kt.183 new file mode 100644 index 00000000000..7a5656eced5 --- /dev/null +++ b/idea/testData/inspections/blockingCallsDetection/ContextCheck.kt.183 @@ -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.sleep(3); + } +} + +@BlockingContext +fun getContext(): CoroutineContext = TODO() + +fun getNonBlockingContext(): CoroutineContext = TODO() + +suspend fun withContext( + context: CoroutineContext, + f: suspend () -> T +) { + TODO() +} \ No newline at end of file diff --git a/idea/testData/inspections/blockingCallsDetection/InsideCoroutine.kt.183 b/idea/testData/inspections/blockingCallsDetection/InsideCoroutine.kt.183 new file mode 100644 index 00000000000..a6affbaabde --- /dev/null +++ b/idea/testData/inspections/blockingCallsDetection/InsideCoroutine.kt.183 @@ -0,0 +1,10 @@ +@file:Suppress("UNUSED_PARAMETER") + +import kotlin.coroutines.* +import java.lang.Thread.sleep + +class InsideCoroutine { + suspend fun example1() { + Thread.sleep(1); + } +} \ No newline at end of file diff --git a/idea/testData/inspections/blockingCallsDetection/LambdaReceiverTypeCheck.kt.183 b/idea/testData/inspections/blockingCallsDetection/LambdaReceiverTypeCheck.kt.183 new file mode 100644 index 00000000000..1ef40df0f63 --- /dev/null +++ b/idea/testData/inspections/blockingCallsDetection/LambdaReceiverTypeCheck.kt.183 @@ -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.sleep(2)}) +} + +fun withRestrictedReceiver(firstParam: suspend Test1.() -> Unit, secondParam: () -> Unit) {} + +fun withSimpleReceiver(firstParam: suspend Test2.() -> Unit) {} + +@RestrictsSuspension +class Test1 + +class Test2 \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/inspections/CoroutineNonBlockingontextDetectionTest.kt.183 b/idea/tests/org/jetbrains/kotlin/idea/inspections/CoroutineNonBlockingontextDetectionTest.kt.183 new file mode 100644 index 00000000000..04ac6cac445 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/inspections/CoroutineNonBlockingontextDetectionTest.kt.183 @@ -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") + } +} \ No newline at end of file