diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/CandidateResolver.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/CandidateResolver.kt index b1f7cceae0b..4c425e3f377 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/CandidateResolver.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/CandidateResolver.kt @@ -34,6 +34,7 @@ import org.jetbrains.kotlin.resolve.calls.callResolverUtil.getEffectiveExpectedT import org.jetbrains.kotlin.resolve.calls.callResolverUtil.getErasedReceiverType import org.jetbrains.kotlin.resolve.calls.callResolverUtil.isInvokeCallOnExpressionWithBothReceivers import org.jetbrains.kotlin.resolve.calls.callUtil.isExplicitSafeCall +import org.jetbrains.kotlin.resolve.calls.callUtil.isSafeCall import org.jetbrains.kotlin.resolve.calls.checkers.AdditionalTypeChecker import org.jetbrains.kotlin.resolve.calls.context.* import org.jetbrains.kotlin.resolve.calls.inference.SubstitutionFilteringInternalResolveAnnotations @@ -454,13 +455,25 @@ class CandidateResolver( // Here we know that receiver is OK ignoring nullability and check that nullability is OK too // Doing it simply as full subtyping check (receiverValueType <: receiverParameterType) - val safeAccess = isExplicitReceiver && !implicitInvokeCheck && candidateCall.call.isExplicitSafeCall() + val call = candidateCall.call + val safeAccess = isExplicitReceiver && !implicitInvokeCheck && call.isExplicitSafeCall() val expectedReceiverParameterType = if (safeAccess) TypeUtils.makeNullable(receiverParameter.type) else receiverParameter.type val smartCastNeeded = !ArgumentTypeResolver.isSubtypeOfForArgumentType(receiverArgument.type, expectedReceiverParameterType) var reportUnsafeCall = false val dataFlowValue = DataFlowValueFactory.createDataFlowValue(receiverArgument, this) val nullability = dataFlowInfo.getPredictableNullability(dataFlowValue) + var nullableImplicitInvokeReceiver = false + if (implicitInvokeCheck && call is CallForImplicitInvoke && call.isSafeCall()) { + val outerCallReceiver = call.outerCall.explicitReceiver + if (outerCallReceiver != call.explicitReceiver && outerCallReceiver is ReceiverValue) { + val outerReceiverDataFlowValue = DataFlowValueFactory.createDataFlowValue(outerCallReceiver, this) + val outerReceiverNullability = dataFlowInfo.getPredictableNullability(outerReceiverDataFlowValue) + if (outerReceiverNullability.canBeNull() && !TypeUtils.isNullableType(expectedReceiverParameterType)) { + nullableImplicitInvokeReceiver = true + } + } + } val expression = (receiverArgument as? ExpressionReceiver)?.expression if (nullability.canBeNull() && !nullability.canBeNonNull()) { if (!TypeUtils.isNullableType(expectedReceiverParameterType)) { @@ -470,7 +483,7 @@ class CandidateResolver( expression?.let { trace.record(BindingContext.SMARTCAST_NULL, it) } } } - else if (smartCastNeeded) { + else if (!nullableImplicitInvokeReceiver && smartCastNeeded) { // Look if smart cast has some useful nullability info val smartCastResult = SmartCastManager.checkAndRecordPossibleCast( @@ -488,7 +501,7 @@ class CandidateResolver( val receiverArgumentType = receiverArgument.type - if (reportUnsafeCall) { + if (reportUnsafeCall || nullableImplicitInvokeReceiver) { tracing.unsafeCall(trace, receiverArgumentType, implicitInvokeCheck) return UNSAFE_CALL_ERROR } diff --git a/compiler/testData/diagnostics/tests/ExtensionCallInvoke.kt b/compiler/testData/diagnostics/tests/ExtensionCallInvoke.kt new file mode 100644 index 00000000000..857cc14579d --- /dev/null +++ b/compiler/testData/diagnostics/tests/ExtensionCallInvoke.kt @@ -0,0 +1,7 @@ +fun bar(doIt: Int.() -> Int) { + 1.doIt() + 1?.doIt() + val i: Int? = 1 + i.doIt() + i?.doIt() +} diff --git a/compiler/testData/diagnostics/tests/ExtensionCallInvoke.txt b/compiler/testData/diagnostics/tests/ExtensionCallInvoke.txt new file mode 100644 index 00000000000..39e1f03291f --- /dev/null +++ b/compiler/testData/diagnostics/tests/ExtensionCallInvoke.txt @@ -0,0 +1,3 @@ +package + +public fun bar(/*0*/ doIt: kotlin.Int.() -> kotlin.Int): kotlin.Unit diff --git a/compiler/testData/diagnostics/tests/SafeCallInvoke.kt b/compiler/testData/diagnostics/tests/SafeCallInvoke.kt new file mode 100644 index 00000000000..0ec22a660a8 --- /dev/null +++ b/compiler/testData/diagnostics/tests/SafeCallInvoke.kt @@ -0,0 +1,23 @@ +class Rule(val apply:() -> Unit) + +fun bar() {} + +fun foo() { + val rule: Rule? = Rule { bar() } + + // this compiles and works + val apply = rule?.apply + if (apply != null) apply() + + // this compiles and works + rule?.apply?.invoke() + + // this should be an error + rule?.apply() + + // these both also ok (with smart cast / unnecessary safe call) + if (rule != null) { + rule.apply() + rule?.apply() + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/SafeCallInvoke.txt b/compiler/testData/diagnostics/tests/SafeCallInvoke.txt new file mode 100644 index 00000000000..a6b5a79bbae --- /dev/null +++ b/compiler/testData/diagnostics/tests/SafeCallInvoke.txt @@ -0,0 +1,12 @@ +package + +public fun bar(): kotlin.Unit +public fun foo(): kotlin.Unit + +public final class Rule { + public constructor Rule(/*0*/ apply: () -> kotlin.Unit) + public final val apply: () -> kotlin.Unit + public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean + public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int + public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String +} diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java index 6f8222fb961..d53b6f52a0a 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java @@ -241,6 +241,12 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTest { doTest(fileName); } + @TestMetadata("ExtensionCallInvoke.kt") + public void testExtensionCallInvoke() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/ExtensionCallInvoke.kt"); + doTest(fileName); + } + @TestMetadata("fileDependencyRecursion.kt") public void testFileDependencyRecursion() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/fileDependencyRecursion.kt"); @@ -577,6 +583,12 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTest { doTest(fileName); } + @TestMetadata("SafeCallInvoke.kt") + public void testSafeCallInvoke() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/SafeCallInvoke.kt"); + doTest(fileName); + } + @TestMetadata("SafeCallNonNullReceiver.kt") public void testSafeCallNonNullReceiver() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/SafeCallNonNullReceiver.kt"); diff --git a/js/js.translator/testData/safeCall/cases/safeCallAndSideEffect.kt b/js/js.translator/testData/safeCall/cases/safeCallAndSideEffect.kt index 62d3ecc8550..966b992854b 100644 --- a/js/js.translator/testData/safeCall/cases/safeCallAndSideEffect.kt +++ b/js/js.translator/testData/safeCall/cases/safeCallAndSideEffect.kt @@ -50,7 +50,7 @@ fun box(): String { return "Bad call getNullA()?.someFun(). result: $n1, counters: ${toStr()}" } - val n2 = getNullA()?.b() + val n2 = getNullA()?.b?.invoke() if (n2 != null || toStr() != "20000") { return "Bad call getNullA()?.b(). result: $n2, counters: ${toStr()}" } @@ -65,7 +65,7 @@ fun box(): String { return "Bad call getA()?.someFun(). result: $i1, counters: ${toStr()}" } - val i2 = getA()?.b() + val i2 = getA()?.b?.invoke() if (i2 != 2 || toStr() != "51101") { return "Bad call getA()?.b(). result: $i2, counters: ${toStr()}" }