[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:
committed by
Space Team
parent
a5e43c9e3f
commit
fab6cec93a
-121
@@ -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
|
||||
|
||||
|
||||
+10
-6
@@ -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
|
||||
}
|
||||
|
||||
+117
-38
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user