diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java index cff63396431..fb2c7275087 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/Errors.java @@ -983,6 +983,8 @@ public interface Errors { DiagnosticFactory1 ILLEGAL_SUSPEND_FUNCTION_CALL = DiagnosticFactory1.create(ERROR); DiagnosticFactory1 ILLEGAL_SUSPEND_PROPERTY_ACCESS = DiagnosticFactory1.create(ERROR); DiagnosticFactory0 ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL = DiagnosticFactory0.create(ERROR); + DiagnosticFactory0 NON_MODIFIER_FORM_FOR_BUILT_IN_SUSPEND = DiagnosticFactory0.create(ERROR); + DiagnosticFactory0 MODIFIER_FORM_FOR_NON_BUILT_IN_SUSPEND = DiagnosticFactory0.create(WARNING); DiagnosticFactory1> PLUGIN_ERROR = DiagnosticFactory1.create(ERROR); DiagnosticFactory1> PLUGIN_WARNING = DiagnosticFactory1.create(WARNING); diff --git a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java index 1fe3fc37010..145c47a3ffc 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java +++ b/compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java @@ -873,6 +873,8 @@ public class DefaultErrorMessages { MAP.put(ILLEGAL_SUSPEND_FUNCTION_CALL, "Suspend function ''{0}'' should be called only from a coroutine or another suspend function", NAME); MAP.put(ILLEGAL_SUSPEND_PROPERTY_ACCESS, "Suspend property ''{0}'' should be accessed only from a coroutine or suspend function", NAME); MAP.put(ILLEGAL_RESTRICTED_SUSPENDING_FUNCTION_CALL, "Restricted suspending functions can only invoke member or extension suspending functions on their restricted coroutine scope"); + MAP.put(NON_MODIFIER_FORM_FOR_BUILT_IN_SUSPEND, "''suspend'' function can only be called in a form of modifier of a lambda: suspend { ... }"); + MAP.put(MODIFIER_FORM_FOR_NON_BUILT_IN_SUSPEND, "Calls having a form of ''suspend {}'' are deprecated because ''suspend'' in the context will have a meaning of a modifier. Add empty argument list to the call: ''suspend() { ... }''"); MAP.put(PLUGIN_ERROR, "{0}", (d, c) -> d.getText()); MAP.put(PLUGIN_WARNING, "{0}", (d, c) -> d.getText()); diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt index 514bc1dd583..17f15357021 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/TargetPlatform.kt @@ -91,7 +91,7 @@ private val DEFAULT_CALL_CHECKERS = listOf( CoroutineSuspendCallChecker, BuilderFunctionsCallChecker, DslScopeViolationCallChecker, MissingDependencyClassChecker, CallableReferenceCompatibilityChecker(), LateinitIntrinsicApplicabilityChecker, UnderscoreUsageChecker, AssigningNamedArgumentToVarargChecker(), - PrimitiveNumericComparisonCallChecker + PrimitiveNumericComparisonCallChecker, LambdaWithSuspendModifierCallChecker ) private val DEFAULT_TYPE_CHECKERS = emptyList() private val DEFAULT_CLASSIFIER_USAGE_CHECKERS = listOf( diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/LambdaWithSuspendModifierCallChecker.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/LambdaWithSuspendModifierCallChecker.kt new file mode 100644 index 00000000000..b4fe39cdd15 --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/checkers/LambdaWithSuspendModifierCallChecker.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2000-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.resolve.calls.checkers + +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.Call +import org.jetbrains.kotlin.psi.KtBinaryExpression +import org.jetbrains.kotlin.psi.KtLambdaExpression +import org.jetbrains.kotlin.psi.KtSimpleNameExpression +import org.jetbrains.kotlin.resolve.calls.callResolverUtil.isInfixCall +import org.jetbrains.kotlin.resolve.calls.callUtil.isCallableReference +import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall +import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +object LambdaWithSuspendModifierCallChecker : CallChecker { + private val KOTLIN_SUSPEND_BUILT_IN_FUNCTION_FQ_NAME = FqName("kotlin.suspend") + + override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) { + val descriptor = resolvedCall.candidateDescriptor + val call = resolvedCall.call + val callName = call.referencedName() + + if (callName != "suspend" && descriptor.name.asString() != "suspend") return + + when (descriptor.fqNameOrNull()) { + KOTLIN_SUSPEND_BUILT_IN_FUNCTION_FQ_NAME -> { + if (!call.hasFormOfSuspendModifierForLambda() || call.explicitReceiver != null) { + context.trace.report(Errors.NON_MODIFIER_FORM_FOR_BUILT_IN_SUSPEND.on(reportOn)) + } + } + else -> { + if (call.hasFormOfSuspendModifierForLambda()) { + context.trace.report(Errors.MODIFIER_FORM_FOR_NON_BUILT_IN_SUSPEND.on(reportOn)) + } + } + } + } + + private fun Call.hasFormOfSuspendModifierForLambda() = + referencedName() == "suspend" + && !isCallableReference() + && typeArguments.isEmpty() + && (hasNoArgumentListButDanglingLambdas() || isInfixWithRightLambda()) + + private fun Call.referencedName() = + calleeExpression?.safeAs()?.getReferencedName() + + private fun Call.hasNoArgumentListButDanglingLambdas() = + valueArgumentList?.leftParenthesis == null && functionLiteralArguments.isNotEmpty() + + private fun Call.isInfixWithRightLambda() = + isInfixCall(this) + && callElement.safeAs()?.right is KtLambdaExpression +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspend.kt b/compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspend.kt new file mode 100644 index 00000000000..951ceb0a606 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspend.kt @@ -0,0 +1,74 @@ +// SKIP_TXT + +fun suspend(block: suspend () -> R): suspend () -> R = block + +class A { + infix fun suspend(block: suspend () -> R): suspend () -> R = block +} + +@Target(AnnotationTarget.EXPRESSION) +annotation class Ann + +fun bar() { + suspend { + println() + } + + @Ann suspend { + println() + } + + suspend @Ann { + println() + } + + suspend() { + println() + } + + suspend({ println() }) + + suspend { + println() + } + + val w: (suspend () -> Int) -> Any? = ::suspend + + A().suspend { + println() + } + + A().suspend() { + println() + } + + A().suspend({ println() }) + + A().suspend { + println() + } + + with(A()) { + suspend { + println() + } + + suspend() { + println() + } + + suspend({ println() }) + + suspend { + println() + } + } + + A() suspend { + println() + } + + A() suspend ({ + println() + }) +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspendWithAnyParameter.kt b/compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspendWithAnyParameter.kt new file mode 100644 index 00000000000..a6a0c62167d --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspendWithAnyParameter.kt @@ -0,0 +1,82 @@ +// SKIP_TXT + +fun suspend(block: R) = block + +class A { + infix fun suspend(block: R) = block +} + +@Target(AnnotationTarget.EXPRESSION) +annotation class Ann + +fun bar() { + suspend { + println() + } + + @Ann suspend { + println() + } + + suspend @Ann { + println() + } + + suspend() { + println() + } + + suspend({ println() }) + + suspend Unit> { + println() + } + + suspend(null) + + val w: (Any?) -> Any? = ::suspend + + A().suspend { + println() + } + + A().suspend() { + println() + } + + A().suspend({ println() }) + + A().suspend Unit> { + println() + } + + A().suspend(null) + + with(A()) { + suspend { + println() + } + + suspend() { + println() + } + + suspend({ println() }) + + suspend Unit> { + println() + } + + suspend(null) + } + + A() suspend { + println() + } + + A() suspend ({ + println() + }) + + A() suspend "" +} diff --git a/compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltIn.kt b/compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltIn.kt new file mode 100644 index 00000000000..29c971fd882 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltIn.kt @@ -0,0 +1,33 @@ +// SKIP_TXT +fun bar() { + suspend { + println() + } + + @Ann suspend { + println() + } + + suspend @Ann { + println() + } + + kotlin.suspend { + + } + + suspend() { + println() + } + + suspend({ println() }) + + suspend { + println() + } + + val w: (suspend () -> Int) -> Any? = ::suspend +} + +@Target(AnnotationTarget.EXPRESSION) +annotation class Ann diff --git a/compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltInRenameOnImport.kt b/compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltInRenameOnImport.kt new file mode 100644 index 00000000000..93d92a35662 --- /dev/null +++ b/compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltInRenameOnImport.kt @@ -0,0 +1,28 @@ +// SKIP_TXT +import kotlin.suspend as suspendLambda + +fun bar() { + suspend { + println() + } + + kotlin.suspend { + + } + + suspendLambda { + println() + } + + suspendLambda() { + println() + } + + suspendLambda({ println() }) + + suspendLambda { + println() + } + + val w: (suspend () -> Int) -> Any? = ::suspendLambda +} diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java index 07ed01870c8..de27fce9e47 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestWithStdLibGenerated.java @@ -1332,6 +1332,18 @@ public class DiagnosticsTestWithStdLibGenerated extends AbstractDiagnosticsTestW doTest(fileName); } + @TestMetadata("modifierFormForNonBuiltInSuspend.kt") + public void testModifierFormForNonBuiltInSuspend() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspend.kt"); + doTest(fileName); + } + + @TestMetadata("modifierFormForNonBuiltInSuspendWithAnyParameter.kt") + public void testModifierFormForNonBuiltInSuspendWithAnyParameter() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspendWithAnyParameter.kt"); + doTest(fileName); + } + @TestMetadata("noDefaultCoroutineImports.kt") public void testNoDefaultCoroutineImports() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/noDefaultCoroutineImports.kt"); @@ -1344,6 +1356,18 @@ public class DiagnosticsTestWithStdLibGenerated extends AbstractDiagnosticsTestW doTest(fileName); } + @TestMetadata("nonModifierFormForBuiltIn.kt") + public void testNonModifierFormForBuiltIn() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltIn.kt"); + doTest(fileName); + } + + @TestMetadata("nonModifierFormForBuiltInRenameOnImport.kt") + public void testNonModifierFormForBuiltInRenameOnImport() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltInRenameOnImport.kt"); + doTest(fileName); + } + @TestMetadata("operators.kt") public void testOperators() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/operators.kt"); diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java index 19d0e7e583e..e65650545c5 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/javac/DiagnosticsTestWithStdLibUsingJavacGenerated.java @@ -1332,6 +1332,18 @@ public class DiagnosticsTestWithStdLibUsingJavacGenerated extends AbstractDiagno doTest(fileName); } + @TestMetadata("modifierFormForNonBuiltInSuspend.kt") + public void testModifierFormForNonBuiltInSuspend() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspend.kt"); + doTest(fileName); + } + + @TestMetadata("modifierFormForNonBuiltInSuspendWithAnyParameter.kt") + public void testModifierFormForNonBuiltInSuspendWithAnyParameter() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/modifierFormForNonBuiltInSuspendWithAnyParameter.kt"); + doTest(fileName); + } + @TestMetadata("noDefaultCoroutineImports.kt") public void testNoDefaultCoroutineImports() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/noDefaultCoroutineImports.kt"); @@ -1344,6 +1356,18 @@ public class DiagnosticsTestWithStdLibUsingJavacGenerated extends AbstractDiagno doTest(fileName); } + @TestMetadata("nonModifierFormForBuiltIn.kt") + public void testNonModifierFormForBuiltIn() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltIn.kt"); + doTest(fileName); + } + + @TestMetadata("nonModifierFormForBuiltInRenameOnImport.kt") + public void testNonModifierFormForBuiltInRenameOnImport() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/nonModifierFormForBuiltInRenameOnImport.kt"); + doTest(fileName); + } + @TestMetadata("operators.kt") public void testOperators() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/testsWithStdLib/coroutines/operators.kt");