[FIR] Utilize equality compatibility logic for cast checks

This makes it more consistent and fixes some
overlooked corner cases. Also it was decided
on the last equality applicability DM
(KT-62646) that we'd like
`is`/`!is`/`as`/`as?` to work similarly
to `===`/`!==`.

Also note that it now gives a clearer
explaination of why some corner cases work
the way they do. For example,
`FirPsiDiagnosticTestGenerated.testLambdaInLhsOfTypeOperatorCall`
yields `UNCHECKED_CAST` instead of
`CAST_NEVER_SUCCEEDS`, because
`toTypeInfo()` replaces all type arguments
with star projections, even when the argument
is not a type parameter. This is because
it has been desided to work this way in
KT-57779.

In
`FirPsiOldFrontendDiagnosticsTestGenerated..NeverSucceeds#testNoGenericsRelated`
the diagnostic is introduced, because
`t2 as FC1` and `FC1` is a final class with
no `T5` supertype.

`UNCHECKED_CAST` in
`FirPsiOldFrontendDiagnosticsTestGenerated.testSmartCast`
disappeared, because previously we didn't
take smartcasts into account.

Note that
`FirPsiOldFrontendDiagnosticsTestGenerated.testMappedSubtypes`
is a false positive. It appears because `isSubtypeOf()` doesn't
take into account platform types in supertypes of the given types
(doesn't map them).
This commit is contained in:
Nikolay Lunyak
2024-02-28 14:17:35 +02:00
committed by Space Team
parent a5e43c9e3f
commit fab6cec93a
25 changed files with 253 additions and 193 deletions
@@ -5,139 +5,18 @@
package org.jetbrains.kotlin.fir.analysis.checkers
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.declarations.utils.isInterface
import org.jetbrains.kotlin.fir.declarations.utils.isLocal
import org.jetbrains.kotlin.fir.resolve.defaultType
import org.jetbrains.kotlin.fir.resolve.getClassAndItsOuterClassesWhenLocal
import org.jetbrains.kotlin.fir.resolve.substitution.ConeSubstitutorByMap
import org.jetbrains.kotlin.fir.scopes.platformClassMapper
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirTypeParameterSymbol
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.types.AbstractTypeChecker
import org.jetbrains.kotlin.types.AbstractTypeChecker.findCorrespondingSupertypes
import org.jetbrains.kotlin.types.TypeCheckerState
import org.jetbrains.kotlin.types.model.typeConstructor
enum class CastingType {
Possible,
Impossible,
Always
}
fun checkCasting(
lhsType: ConeKotlinType,
rhsType: ConeKotlinType,
isSafeCase: Boolean,
context: CheckerContext
): CastingType {
val lhsLowerType = lhsType.lowerBoundIfFlexible().originalIfDefinitelyNotNullable()
val rhsLowerType = rhsType.lowerBoundIfFlexible().originalIfDefinitelyNotNullable()
if (lhsLowerType is ConeErrorType || rhsLowerType is ConeErrorType) return CastingType.Possible
val session = context.session
if (lhsLowerType is ConeIntersectionType) {
var result = false
for (intersectedType in lhsLowerType.intersectedTypes) {
val isIntersectedCastPossible = checkCasting(intersectedType, rhsLowerType, isSafeCase, context)
val intersectedTypeSymbol = intersectedType.toRegularClassSymbol(session)
if (intersectedTypeSymbol?.isInterface == false && isIntersectedCastPossible == CastingType.Impossible) {
return CastingType.Impossible // Any class type in intersection type should be subtype of RHS
}
result = result or (isIntersectedCastPossible != CastingType.Impossible)
}
return if (result) CastingType.Possible else CastingType.Impossible
}
val lhsNullable = lhsLowerType.canBeNull(session)
val rhsNullable = rhsLowerType.canBeNull(session)
if (lhsLowerType.isNothing) return CastingType.Possible
if (lhsLowerType.isNullableNothing && !rhsNullable) {
return if (isSafeCase) CastingType.Always else CastingType.Impossible
}
if (rhsLowerType.isNothing) return CastingType.Impossible
if (rhsLowerType.isNullableNothing) {
return if (lhsNullable) CastingType.Possible else CastingType.Impossible
}
if (lhsNullable && rhsNullable) return CastingType.Possible
// This is an oversimplification (which does not render the method incomplete):
// we consider any type parameter capable of taking any value, which may be made more precise if we considered bounds
if (lhsLowerType is ConeTypeParameterType || rhsLowerType is ConeTypeParameterType) return CastingType.Possible
val lhsClassSymbol = lhsLowerType.toRegularClassSymbol(session)
val rhsClassSymbol = rhsLowerType.toRegularClassSymbol(session)
val lhsNormalizedType = getCorrespondingKotlinClass(lhsClassSymbol?.defaultType() ?: lhsLowerType, session)
val rhsNormalizedType = getCorrespondingKotlinClass(rhsClassSymbol?.defaultType() ?: rhsLowerType, session)
val state = session.typeContext.newTypeCheckerState(errorTypesEqualToAnything = false, stubTypesEqualToAnything = false)
// It's an optimization, the code below with `isRoughSubtypeOf` also checks subtyping, but it's slower
if (AbstractTypeChecker.isSubtypeOf(state, lhsNormalizedType, rhsNormalizedType) ||
AbstractTypeChecker.isSubtypeOf(state, rhsNormalizedType, lhsNormalizedType)
) {
return CastingType.Possible
}
if (isRoughSubtypeOf(lhsNormalizedType, rhsNormalizedType, state, session) ||
isRoughSubtypeOf(rhsNormalizedType, lhsNormalizedType, state, session)
) {
return CastingType.Possible
}
if (isFinal(lhsNormalizedType, session) || isFinal(rhsNormalizedType, session)) return CastingType.Impossible
val lhsNormalizedTypeSymbol = lhsNormalizedType.toSymbol(session) as? FirClassSymbol<*>
val rhsNormalizedTypeSymbol = rhsNormalizedType.toSymbol(session) as? FirClassSymbol<*>
if (lhsNormalizedTypeSymbol?.isInterface == true || rhsNormalizedTypeSymbol?.isInterface == true) return CastingType.Possible
return CastingType.Impossible
}
/**
* One type is roughly subtype of another superType when one of type's supertype constructor equals another superType constructor.
*
* Note that some types have platform-specific counterparts, i.e. kotlin.String is mapped to java.lang.String,
* such types (and all their sub- and supertypes) are related too.
*
* Due to limitations in PlatformToKotlinClassMap, we only consider mapping of platform classes to Kotlin classed
* (i.e. java.lang.String -> kotlin.String) and ignore mappings that go the other way.
*/
private fun isRoughSubtypeOf(
type: ConeSimpleKotlinType,
superType: ConeSimpleKotlinType,
state: TypeCheckerState,
session: FirSession
): Boolean {
var result = false
val superTypeConstructor = superType.typeConstructor(state.typeSystemContext)
state.anySupertype(type, { typeMarker ->
val correspondingKotlinClass = getCorrespondingKotlinClass(typeMarker as ConeSimpleKotlinType, session)
if (correspondingKotlinClass.typeConstructor(state.typeSystemContext) == superTypeConstructor) {
result = true
true
} else {
false
}
}, { TypeCheckerState.SupertypesPolicy.LowerIfFlexible })
return result
}
private fun getCorrespondingKotlinClass(type: ConeSimpleKotlinType, session: FirSession): ConeSimpleKotlinType {
return session.platformClassMapper.getCorrespondingKotlinClass(type.classId)?.defaultType(emptyList()) ?: type
}
private fun isFinal(type: ConeSimpleKotlinType, session: FirSession): Boolean {
return !type.canHaveSubtypesAccordingToK1(session)
}
fun isCastErased(supertype: ConeKotlinType, subtype: ConeKotlinType, context: CheckerContext): Boolean {
val typeContext = context.session.typeContext
@@ -32,11 +32,13 @@ internal fun FirExpression.unwrapToMoreUsefulExpression() = when (this) {
internal class TypeInfo(
val type: ConeKotlinType,
val notNullType: ConeKotlinType,
val directType: ConeKotlinType,
val isEnumClass: Boolean,
val isPrimitive: Boolean,
val isBuiltin: Boolean,
val isValueClass: Boolean,
val isFinal: Boolean,
val isClass: Boolean,
val canHaveSubtypesAccordingToK1: Boolean,
) {
override fun toString() = "$type"
@@ -64,20 +66,22 @@ internal fun ConeKotlinType.toTypeInfo(session: FirSession): TypeInfo {
val type = bounds.ifNotEmpty { ConeTypeIntersector.intersectTypes(session.typeContext, this) }
?: session.builtinTypes.nullableAnyType.type
val notNullType = type.withNullability(ConeNullability.NOT_NULL, session.typeContext)
val boundsSymbols = bounds.mapNotNull { it.toClassSymbol(session) }
return TypeInfo(
type, notNullType,
isEnumClass = bounds.any { it.isEnum(session) },
type, notNullType, directType = this,
isEnumClass = boundsSymbols.any { it.isEnumClass },
isPrimitive = bounds.any { it.isPrimitiveOrNullablePrimitive },
isBuiltin = bounds.any { it.toClassSymbol(session)?.isBuiltin == true },
isValueClass = bounds.any { it.toClassSymbol(session)?.isInline == true },
isFinal = bounds.any { it.toClassSymbol(session)?.isFinalClass == true },
isBuiltin = boundsSymbols.any { it.isBuiltin },
isValueClass = boundsSymbols.any { it.isInline },
isFinal = boundsSymbols.any { it.isFinalClass },
isClass = boundsSymbols.any { it.isClass },
// In K1's intersector, `canHaveSubtypes()` is called for `nullabilityStripped`.
withNullability(ConeNullability.NOT_NULL, session.typeContext).canHaveSubtypesAccordingToK1(session),
)
}
private fun ConeClassLikeType.toKotlinTypeIfPlatform(session: FirSession): ConeClassLikeType {
internal fun ConeClassLikeType.toKotlinTypeIfPlatform(session: FirSession): ConeClassLikeType {
val kotlinClassId = session.platformClassMapper.getCorrespondingKotlinClass(lookupTag.classId)
return kotlinClassId?.constructClassLikeType(typeArguments, isNullable, attributes) ?: this
}
@@ -8,54 +8,133 @@ package org.jetbrains.kotlin.fir.analysis.checkers.expression
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.analysis.checkers.CastingType
import org.jetbrains.kotlin.fir.analysis.checkers.MppCheckerKind
import org.jetbrains.kotlin.fir.analysis.checkers.checkCasting
import org.jetbrains.kotlin.fir.analysis.checkers.*
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.fir.analysis.checkers.finalApproximationOrSelf
import org.jetbrains.kotlin.fir.analysis.checkers.isCastErased
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors
import org.jetbrains.kotlin.fir.expressions.FirOperation
import org.jetbrains.kotlin.fir.expressions.FirTypeOperatorCall
import org.jetbrains.kotlin.fir.expressions.unwrapSmartcastExpression
import org.jetbrains.kotlin.fir.expressions.*
import org.jetbrains.kotlin.fir.firPlatformSpecificCastChecker
import org.jetbrains.kotlin.fir.resolve.fullyExpandedType
import org.jetbrains.kotlin.fir.types.ConeDynamicType
import org.jetbrains.kotlin.fir.types.coneType
import org.jetbrains.kotlin.fir.types.resolvedType
import org.jetbrains.kotlin.fir.types.*
object FirCastOperatorsChecker : FirTypeOperatorCallChecker(MppCheckerKind.Common) {
override fun check(expression: FirTypeOperatorCall, context: CheckerContext, reporter: DiagnosticReporter) {
val session = context.session
val firstArgument = expression.argumentList.arguments[0]
val actualType = firstArgument.unwrapSmartcastExpression().resolvedType.fullyExpandedType(session).finalApproximationOrSelf(context)
val conversionTypeRef = expression.conversionTypeRef
val targetType = conversionTypeRef.coneType.fullyExpandedType(session).finalApproximationOrSelf(context)
val arguments = expression.argumentList.arguments
require(arguments.size == 1) { "Type operator call with non-1 arguments" }
if (expression.operation in FirOperation.TYPES && targetType is ConeDynamicType) {
reporter.reportOn(conversionTypeRef.source, FirErrors.DYNAMIC_NOT_ALLOWED, context)
val l = arguments[0].toArgumentInfo(context)
val r = expression.conversionTypeRef.coneType
.fullyExpandedType(context.session)
.finalApproximationOrSelf(context)
.toTypeInfo(context.session)
if (expression.operation in FirOperation.TYPES && r.directType is ConeDynamicType) {
reporter.reportOn(expression.conversionTypeRef.source, FirErrors.DYNAMIC_NOT_ALLOWED, context)
}
val isSafeAs = expression.operation == FirOperation.SAFE_AS
if (expression.operation == FirOperation.AS || isSafeAs) {
val castType = checkCasting(actualType, targetType, isSafeAs, context)
if (castType == CastingType.Impossible) {
if (context.languageVersionSettings.supportsFeature(LanguageFeature.EnableDfaWarningsInK2)) {
if (!session.firPlatformSpecificCastChecker.shouldSuppressImpossibleCast(session, actualType, targetType)) {
reporter.reportOn(expression.source, FirErrors.CAST_NEVER_SUCCEEDS, context)
}
}
} else if (castType == CastingType.Always) {
if (context.languageVersionSettings.supportsFeature(LanguageFeature.EnableDfaWarningsInK2)) {
reporter.reportOn(expression.source, FirErrors.USELESS_CAST, context)
}
} else if (isCastErased(actualType, targetType, context)) {
reporter.reportOn(expression.source, FirErrors.UNCHECKED_CAST, actualType, targetType, context)
}
} else if (expression.operation == FirOperation.IS) {
if (isCastErased(actualType, targetType, context)) {
reporter.reportOn(conversionTypeRef.source, FirErrors.CANNOT_CHECK_FOR_ERASED, targetType, context)
}
val checkApplicability = when (expression.operation) {
FirOperation.IS, FirOperation.NOT_IS -> ::checkIsApplicability
FirOperation.AS, FirOperation.SAFE_AS -> ::checkAsApplicability
else -> error("Invalid operator of FirTypeOperatorCall")
}
val rUserType = expression.conversionTypeRef.coneType.finalApproximationOrSelf(context)
// No need to check original types separately from smartcast types, because we only report warnings
checkApplicability(l.smartCastTypeInfo, r, expression, context).ifInapplicable {
return reporter.reportInapplicabilityDiagnostic(expression, it, l.originalTypeInfo, r.type, l.userType, rUserType, context)
}
}
// IDE doesn't care this function is referenced as :: and then used polymorphicly
@Suppress("UNUSED_PARAMETER")
private fun checkIsApplicability(l: TypeInfo, r: TypeInfo, expression: FirTypeOperatorCall, context: CheckerContext): Applicability {
return when {
isCastErased(l.directType, r.directType, context) -> Applicability.CAST_ERASED
else -> Applicability.APPLICABLE
}
}
private fun checkAsApplicability(l: TypeInfo, r: TypeInfo, expression: FirTypeOperatorCall, context: CheckerContext): Applicability {
val oneIsFinal = l.isFinal || r.isFinal
val oneIsNotNull = !l.type.isNullable || !r.type.isNullable
val isNullableNothingWithNotNull = !l.type.isNullable && r.type.isNullableNothing
|| l.type.isNullableNothing && !r.type.isNullable
return when {
l.type.isNothing && r.type.isNothingOrNullableNothing -> Applicability.APPLICABLE
r.type.isNothing -> Applicability.IMPOSSIBLE
isNullableNothingWithNotNull -> when (expression.operation) {
// (null as? WhatEver) == null
FirOperation.SAFE_AS -> Applicability.USELESS
else -> Applicability.IMPOSSIBLE
}
oneIsNotNull && oneIsFinal && areUnrelated(l, r, context) -> Applicability.IMPOSSIBLE
isCastErased(l.directType, r.directType, context) -> Applicability.CAST_ERASED
else -> Applicability.APPLICABLE
}
}
private fun areUnrelated(a: TypeInfo, b: TypeInfo, context: CheckerContext) =
!a.isSubtypeOf(b, context) && !b.isSubtypeOf(a, context)
private fun TypeInfo.isSubtypeOf(other: TypeInfo, context: CheckerContext) =
notNullType.isSubtypeOf(other.notNullType, context.session)
/**
* K1 reports different diagnostics for different
* cases, and this enum helps to replicate the K1's
* choice of diagnostics.
*
* Should the K2's diagnostic severity differ,
* the proper version will be picked later
* when reporting the diagnostic.
*/
private enum class Applicability {
APPLICABLE,
IMPOSSIBLE,
USELESS,
CAST_ERASED,
}
private inline fun Applicability.ifInapplicable(block: (Applicability) -> Unit) = when (this) {
Applicability.APPLICABLE -> {}
else -> block(this)
}
private fun DiagnosticReporter.reportInapplicabilityDiagnostic(
expression: FirTypeOperatorCall,
applicability: Applicability,
l: TypeInfo,
r: ConeKotlinType,
lUserType: ConeKotlinType,
rUserType: ConeKotlinType,
context: CheckerContext,
) {
when (applicability) {
Applicability.IMPOSSIBLE -> getImpossibilityDiagnostic(l, r, context)?.let {
reportOn(expression.source, it, context)
}
Applicability.USELESS -> getUselessnessDiagnostic(context)?.let {
reportOn(expression.source, it, context)
}
Applicability.CAST_ERASED -> when {
expression.operation == FirOperation.AS || expression.operation == FirOperation.SAFE_AS -> {
reportOn(expression.source, FirErrors.UNCHECKED_CAST, lUserType, rUserType, context)
}
else -> reportOn(expression.conversionTypeRef.source, FirErrors.CANNOT_CHECK_FOR_ERASED, rUserType, context)
}
else -> error("Shouldn't be here")
}
}
private fun getImpossibilityDiagnostic(l: TypeInfo, rType: ConeKotlinType, context: CheckerContext) = when {
!context.languageVersionSettings.supportsFeature(LanguageFeature.EnableDfaWarningsInK2) -> null
context.session.firPlatformSpecificCastChecker.shouldSuppressImpossibleCast(context.session, l.type, rType) -> null
else -> FirErrors.CAST_NEVER_SUCCEEDS
}
private fun getUselessnessDiagnostic(context: CheckerContext) = when {
!context.languageVersionSettings.supportsFeature(LanguageFeature.EnableDfaWarningsInK2) -> null
else -> FirErrors.USELESS_CAST
}
}