diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory.kt index fd85c3c0def..fd6fc543578 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DataFlowValueFactory.kt @@ -190,6 +190,19 @@ object DataFlowValueFactory { IdentifierInfo.qualified(receiverInfo, bindingContext.getType(receiverExpression), selectorInfo, expression.operationSign === KtTokens.SAFE_ACCESS) } + is KtBinaryExpressionWithTypeRHS -> { + val subjectExpression = expression.left + val targetTypeReference = expression.right + val operationToken = expression.operationReference.getReferencedNameElementType() + if (operationToken == KtTokens.IS_KEYWORD || operationToken == KtTokens.AS_KEYWORD) { + IdentifierInfo.NO + } + else { + IdentifierInfo.SafeCast(getIdForStableIdentifier(subjectExpression, bindingContext, containingDeclarationOrModule), + bindingContext.getType(subjectExpression), + bindingContext[BindingContext.TYPE, targetTypeReference]) + } + } is KtSimpleNameExpression -> getIdForSimpleNameExpression(expression, bindingContext, containingDeclarationOrModule) is KtThisExpression -> { diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DelegatingDataFlowInfo.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DelegatingDataFlowInfo.kt index eb8624860d7..be4e324dd94 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DelegatingDataFlowInfo.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/DelegatingDataFlowInfo.kt @@ -90,11 +90,14 @@ internal class DelegatingDataFlowInfo private constructor( nullabilityInfo[key] ?: parent?.getCollectedNullability(key) ?: key.immanentNullability } - private fun putNullability(map: MutableMap, - value: DataFlowValue, - nullability: Nullability, - languageVersionSettings: LanguageVersionSettings, - affectReceiver: Boolean = true): Boolean { + private fun putNullabilityAndTypeInfo( + map: MutableMap, + value: DataFlowValue, + nullability: Nullability, + languageVersionSettings: LanguageVersionSettings, + typeInfo: SetMultimap? = null, + affectReceiver: Boolean = true + ): Boolean { map.put(value, nullability) val identifierInfo = value.identifierInfo @@ -104,11 +107,23 @@ internal class DelegatingDataFlowInfo private constructor( is IdentifierInfo.Qualified -> { val receiverType = identifierInfo.receiverType if (identifierInfo.safe && receiverType != null) { - putNullability(map, DataFlowValue(identifierInfo.receiverInfo, receiverType), nullability, languageVersionSettings) + val receiverValue = DataFlowValue(identifierInfo.receiverInfo, receiverType) + putNullabilityAndTypeInfo(map, receiverValue, nullability, languageVersionSettings, typeInfo) + } + } + is IdentifierInfo.SafeCast -> { + val targetType = identifierInfo.targetType + val subjectType = identifierInfo.subjectType + if (targetType != null && subjectType != null && + languageVersionSettings.supportsFeature(LanguageFeature.SafeCastCheckBoundSmartCasts)) { + + val subjectValue = DataFlowValue(identifierInfo.subjectInfo, subjectType) + putNullabilityAndTypeInfo(map, subjectValue, nullability, languageVersionSettings, typeInfo) + typeInfo?.put(subjectValue, targetType) } } is IdentifierInfo.Variable -> identifierInfo.bound?.let { - putNullability(map, it, nullability, languageVersionSettings) + putNullabilityAndTypeInfo(map, it, nullability, languageVersionSettings, typeInfo) } } } @@ -149,14 +164,14 @@ internal class DelegatingDataFlowInfo private constructor( */ override fun clearValueInfo(value: DataFlowValue, languageVersionSettings: LanguageVersionSettings): DataFlowInfo { val resultNullabilityInfo = hashMapOf() - putNullability(resultNullabilityInfo, value, value.immanentNullability, languageVersionSettings) + putNullabilityAndTypeInfo(resultNullabilityInfo, value, value.immanentNullability, languageVersionSettings) return create(this, resultNullabilityInfo, EMPTY_TYPE_INFO, value) } override fun assign(a: DataFlowValue, b: DataFlowValue, languageVersionSettings: LanguageVersionSettings): DataFlowInfo { val nullability = hashMapOf() val nullabilityOfB = getStableNullability(b) - putNullability(nullability, a, nullabilityOfB, languageVersionSettings, affectReceiver = false) + putNullabilityAndTypeInfo(nullability, a, nullabilityOfB, languageVersionSettings, affectReceiver = false) val newTypeInfo = newTypeInfo() var typesForB = getStableTypes(b) @@ -179,11 +194,12 @@ internal class DelegatingDataFlowInfo private constructor( val nullabilityOfA = getStableNullability(a) val nullabilityOfB = getStableNullability(b) - var changed = putNullability(resultNullabilityInfo, a, nullabilityOfA.refine(nullabilityOfB), languageVersionSettings) or - putNullability(resultNullabilityInfo, b, nullabilityOfB.refine(nullabilityOfA), languageVersionSettings) + val newTypeInfo = newTypeInfo() + var changed = + putNullabilityAndTypeInfo(resultNullabilityInfo, a, nullabilityOfA.refine(nullabilityOfB), languageVersionSettings, newTypeInfo) or + putNullabilityAndTypeInfo(resultNullabilityInfo, b, nullabilityOfB.refine(nullabilityOfA), languageVersionSettings, newTypeInfo) // NB: == has no guarantees of type equality, see KT-11280 for the example - val newTypeInfo = newTypeInfo() if (identityEquals || !nullabilityOfA.canBeNonNull() || !nullabilityOfB.canBeNonNull()) { newTypeInfo.putAll(a, getStableTypes(b, false)) newTypeInfo.putAll(b, getStableTypes(a, false)) @@ -199,12 +215,7 @@ internal class DelegatingDataFlowInfo private constructor( changed = changed or !newTypeInfo.isEmpty } - return if (!changed) { - this - } - else { - create(this, resultNullabilityInfo, if (newTypeInfo.isEmpty) EMPTY_TYPE_INFO else newTypeInfo) - } + return if (changed) create(this, resultNullabilityInfo, if (newTypeInfo.isEmpty) EMPTY_TYPE_INFO else newTypeInfo) else this } private fun collectTypesFromMeAndParents(value: DataFlowValue): Set { @@ -232,9 +243,13 @@ internal class DelegatingDataFlowInfo private constructor( val nullabilityOfA = getStableNullability(a) val nullabilityOfB = getStableNullability(b) - val changed = putNullability(resultNullabilityInfo, a, nullabilityOfA.refine(nullabilityOfB.invert()), languageVersionSettings) or - putNullability(resultNullabilityInfo, b, nullabilityOfB.refine(nullabilityOfA.invert()), languageVersionSettings) - return if (changed) create(this, resultNullabilityInfo, EMPTY_TYPE_INFO) else this + val newTypeInfo = newTypeInfo() + val changed = + putNullabilityAndTypeInfo(resultNullabilityInfo, a, nullabilityOfA.refine(nullabilityOfB.invert()), languageVersionSettings, newTypeInfo) or + putNullabilityAndTypeInfo(resultNullabilityInfo, b, nullabilityOfB.refine(nullabilityOfA.invert()), languageVersionSettings, newTypeInfo) + + return if (changed) create(this, resultNullabilityInfo, if (newTypeInfo.isEmpty) EMPTY_TYPE_INFO else newTypeInfo) else this + } override fun establishSubtyping( @@ -247,7 +262,7 @@ internal class DelegatingDataFlowInfo private constructor( newTypeInfo.put(value, type) val nullabilityInfo = hashMapOf() if (!type.isMarkedNullable) { - putNullability(nullabilityInfo, value, NOT_NULL, languageVersionSettings) + putNullabilityAndTypeInfo(nullabilityInfo, value, NOT_NULL, languageVersionSettings) } return create(this, if (type.isMarkedNullable) emptyMap() else nullabilityInfo, newTypeInfo) } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/IdentifierInfo.kt b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/IdentifierInfo.kt index 142cac8a583..b892a3a0397 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/IdentifierInfo.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/smartcasts/IdentifierInfo.kt @@ -89,6 +89,18 @@ interface IdentifierInfo { override fun toString() = "$receiverInfo${if (safe) "?." else "."}$selectorInfo" } + data class SafeCast( + val subjectInfo: IdentifierInfo, + val subjectType: KotlinType?, + val targetType: KotlinType? + ) : IdentifierInfo { + override val kind get() = OTHER + + override val canBeBound get() = subjectInfo.canBeBound + + override fun toString() = "$subjectInfo as? ${targetType ?: "???"}" + } + companion object { fun qualified( diff --git a/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOff.kt b/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOff.kt new file mode 100644 index 00000000000..22fe9c57353 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOff.kt @@ -0,0 +1,36 @@ +// !LANGUAGE: -SafeCastCheckBoundSmartCasts + +interface SomeClass { + val data: Any? +} + +interface SomeSubClass : SomeClass { + val foo: Any? +} + +fun g(a: SomeClass?) { + if (a as? SomeSubClass != null) { + // 'a' can be cast to SomeSubClass + a.hashCode() + a.foo + (a as? SomeSubClass).foo + (a as SomeSubClass).foo + } + val b = (a as? SomeSubClass)?.foo + if (b != null) { + // 'a' can be cast to SomeSubClass + a.hashCode() + a.foo + (a as? SomeSubClass).foo + (a as SomeSubClass).foo + } + val c = a as? SomeSubClass + if (c != null) { + // 'a' and 'c' can be cast to SomeSubClass + a.hashCode() + a.foo + (a as? SomeSubClass).foo + c.hashCode() + c.foo + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOff.txt b/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOff.txt new file mode 100644 index 00000000000..583c561c362 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOff.txt @@ -0,0 +1,18 @@ +package + +public fun g(/*0*/ a: SomeClass?): kotlin.Unit + +public interface SomeClass { + public abstract val data: kotlin.Any? + 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 +} + +public interface SomeSubClass : SomeClass { + public abstract override /*1*/ /*fake_override*/ val data: kotlin.Any? + public abstract val foo: kotlin.Any? + 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/testData/diagnostics/tests/smartCasts/castchecks/basicOn.kt b/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOn.kt new file mode 100644 index 00000000000..168b0fb5f0c --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOn.kt @@ -0,0 +1,36 @@ +// !LANGUAGE: +SafeCastCheckBoundSmartCasts + +interface SomeClass { + val data: Any? +} + +interface SomeSubClass : SomeClass { + val foo: Any? +} + +fun g(a: SomeClass?) { + if (a as? SomeSubClass != null) { + // 'a' can be cast to SomeSubClass + a.hashCode() + a.foo + (a as? SomeSubClass).foo + (a as SomeSubClass).foo + } + val b = (a as? SomeSubClass)?.foo + if (b != null) { + // 'a' can be cast to SomeSubClass + a.hashCode() + a.foo + (a as? SomeSubClass).foo + (a as SomeSubClass).foo + } + val c = a as? SomeSubClass + if (c != null) { + // 'a' and 'c' can be cast to SomeSubClass + a.hashCode() + a.foo + (a as? SomeSubClass).foo + c.hashCode() + c.foo + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOn.txt b/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOn.txt new file mode 100644 index 00000000000..583c561c362 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOn.txt @@ -0,0 +1,18 @@ +package + +public fun g(/*0*/ a: SomeClass?): kotlin.Unit + +public interface SomeClass { + public abstract val data: kotlin.Any? + 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 +} + +public interface SomeSubClass : SomeClass { + public abstract override /*1*/ /*fake_override*/ val data: kotlin.Any? + public abstract val foo: kotlin.Any? + 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/testData/diagnostics/tests/smartCasts/castchecks/insideCall.kt b/compiler/testData/diagnostics/tests/smartCasts/castchecks/insideCall.kt new file mode 100644 index 00000000000..37da0adf003 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/castchecks/insideCall.kt @@ -0,0 +1,13 @@ +// !LANGUAGE: +SafeCastCheckBoundSmartCasts +// See KT-19007 + +// Stub +fun String.indexOf(arg: String) = this.length - arg.length + +// Stub +fun String.toLowerCase() = this + +fun foo(a: Any) { + // Should compile in 1.2 + (a as? String)?.indexOf(a.toLowerCase()) +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/smartCasts/castchecks/insideCall.txt b/compiler/testData/diagnostics/tests/smartCasts/castchecks/insideCall.txt new file mode 100644 index 00000000000..ff063335312 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/castchecks/insideCall.txt @@ -0,0 +1,5 @@ +package + +public fun foo(/*0*/ a: kotlin.Any): kotlin.Unit +public fun kotlin.String.indexOf(/*0*/ arg: kotlin.String): kotlin.Int +public fun kotlin.String.toLowerCase(): kotlin.String diff --git a/compiler/testData/diagnostics/tests/smartCasts/castchecks/variables.kt b/compiler/testData/diagnostics/tests/smartCasts/castchecks/variables.kt new file mode 100644 index 00000000000..e32e20e58e0 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/castchecks/variables.kt @@ -0,0 +1,67 @@ +// !LANGUAGE: +SafeCastCheckBoundSmartCasts +interface SomeClass { + val data: Any? +} + +interface SomeSubClass : SomeClass { + val foo: Any? +} + +object Impl : SomeSubClass { + override val data = "" + override val foo = 42 +} + +fun g(a: SomeClass?) { + var b = (a as? SomeSubClass)?.foo + b = "Hello" + if (b != null) { + // 'a' cannot be cast to SomeSubClass! + a.hashCode() + a.foo + (a as? SomeSubClass).foo + (a as SomeSubClass).foo + } + var c = a as? SomeSubClass + c = Impl + if (c != null) { + // 'a' cannot be cast to SomeSubClass + a.hashCode() + a.foo + (a as? SomeSubClass).foo + c.hashCode() + c.foo + } +} + +fun f(a: SomeClass?) { + var aa = a + + if (aa as? SomeSubClass != null) { + aa = null + // 'aa' cannot be cast to SomeSubClass + aa.hashCode() + aa.foo + (aa as? SomeSubClass).foo + (aa as SomeSubClass).foo + } + val b = (aa as? SomeSubClass)?.foo + aa = null + if (b != null) { + // 'aa' cannot be cast to SomeSubClass + aa.hashCode() + aa.foo + (aa as? SomeSubClass).foo + (aa as SomeSubClass).foo + } + aa = a + val c = aa as? SomeSubClass + if (c != null) { + // 'c' can be cast to SomeSubClass + aa.hashCode() + aa.foo + (aa as? SomeSubClass).foo + c.hashCode() + c.foo + } +} \ No newline at end of file diff --git a/compiler/testData/diagnostics/tests/smartCasts/castchecks/variables.txt b/compiler/testData/diagnostics/tests/smartCasts/castchecks/variables.txt new file mode 100644 index 00000000000..a02309f6503 --- /dev/null +++ b/compiler/testData/diagnostics/tests/smartCasts/castchecks/variables.txt @@ -0,0 +1,28 @@ +package + +public fun f(/*0*/ a: SomeClass?): kotlin.Unit +public fun g(/*0*/ a: SomeClass?): kotlin.Unit + +public object Impl : SomeSubClass { + private constructor Impl() + public open override /*1*/ val data: kotlin.String = "" + public open override /*1*/ val foo: kotlin.Int = 42 + 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 +} + +public interface SomeClass { + public abstract val data: kotlin.Any? + 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 +} + +public interface SomeSubClass : SomeClass { + public abstract override /*1*/ /*fake_override*/ val data: kotlin.Any? + public abstract val foo: kotlin.Any? + 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/testData/diagnostics/tests/smartCasts/safecalls/safeAccessReceiverNotNull.kt b/compiler/testData/diagnostics/tests/smartCasts/safecalls/safeAccessReceiverNotNull.kt index d6b45586d18..4cc8e6813a7 100644 --- a/compiler/testData/diagnostics/tests/smartCasts/safecalls/safeAccessReceiverNotNull.kt +++ b/compiler/testData/diagnostics/tests/smartCasts/safecalls/safeAccessReceiverNotNull.kt @@ -1,3 +1,4 @@ +// !LANGUAGE: -SafeCastCheckBoundSmartCasts // A set of examples for // "If the result of a safe call is not null, understand that its receiver is not null" // and some other improvements for nullability detection diff --git a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java index 17497caf643..afa20412936 100644 --- a/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java @@ -20266,6 +20266,39 @@ public class DiagnosticsTestGenerated extends AbstractDiagnosticsTest { doTest(fileName); } + @TestMetadata("compiler/testData/diagnostics/tests/smartCasts/castchecks") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class Castchecks extends AbstractDiagnosticsTest { + public void testAllFilesPresentInCastchecks() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/diagnostics/tests/smartCasts/castchecks"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("basicOff.kt") + public void testBasicOff() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOff.kt"); + doTest(fileName); + } + + @TestMetadata("basicOn.kt") + public void testBasicOn() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/smartCasts/castchecks/basicOn.kt"); + doTest(fileName); + } + + @TestMetadata("insideCall.kt") + public void testInsideCall() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/smartCasts/castchecks/insideCall.kt"); + doTest(fileName); + } + + @TestMetadata("variables.kt") + public void testVariables() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/smartCasts/castchecks/variables.kt"); + doTest(fileName); + } + } + @TestMetadata("compiler/testData/diagnostics/tests/smartCasts/inference") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt b/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt index 6d72eb479fa..c98ea941f03 100644 --- a/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt +++ b/compiler/util/src/org/jetbrains/kotlin/config/LanguageVersionSettings.kt @@ -54,6 +54,7 @@ enum class LanguageFeature( InlineDefaultFunctionalParameters(KOTLIN_1_2), SoundSmartCastsAfterTry(KOTLIN_1_2), DeprecatedFieldForInvisibleCompanionObject(KOTLIN_1_2), + SafeCastCheckBoundSmartCasts(KOTLIN_1_2), // Experimental features