[FIR] Fix K2 behavior according to RULES1

The compiler should only report diagnostics for
comparisons over builtins and identity-less types,
other incompatibilities should be reported
via inspections.

It's ok that in `equalityChecksOnIntegerTypes`
instead of `EQUALITY_NOT_APPLICABLE_WARNING` we get
`EQUALITY_NOT_APPLICABLE`, because
`ProperEqualityChecksInBuilderInferenceCalls`
is already active by default.

This change also replaces the notion of a representative superclass
with the least upper bound.
This makes complex types like
intersection/flexible transparent to
RULES1-based compatibility checks.
One way to look at it is to think
that this is an automatic way of handling
type parameters: automatic picking of
"interesting" bounds, and checking them against one another.

Note that `TypeIntersector.intersectTypes`
for `Int` and `T` where `T` is a type parameter
may return both `{Int & T}` or `null`
depending on `T`-s bounds. At the same time,
for type parameters `T` and `K` it will
always return `{T & K}`.

`ConeTypeIntersector.intersectTypes`, on the
other hand, will always return `{Int & T}`
irrespectively of the bounds. Meaning, the two
intersectors differ in corner cases.

`lowerBoundIfFlexible` call in `isLiterallyTypeParameter` is backed by
the `equalityOfFlexibleTypeParameters` test.

^KT-35134 #fixed-in-k2
^KT-22499 #fixed-in-k2
^KT-46383 #fixed-in-k2
This commit is contained in:
Nikolay Lunyak
2023-02-01 18:21:53 +02:00
committed by Space Team
parent 06e687addd
commit f0720c1d12
86 changed files with 1982 additions and 594 deletions
@@ -33,7 +33,7 @@ fun case1(javaClass: JavaClass?) {
}
class Case1(val javaClass: JavaClass?) {
val x = if (javaClass != null) { it -> <!EQUALITY_NOT_APPLICABLE_WARNING!>it == javaClass<!> } else BooCase2.FILTER
val x = if (javaClass != null) { it -> it == javaClass } else BooCase2.FILTER
}
class BooCase1() {
@@ -101,7 +101,7 @@ fun test_9(a: Int, b: Int?) {
}
b<!UNSAFE_CALL!>.<!>inc()
if (a === b) {
if (<!IMPLICIT_BOXING_IN_IDENTITY_EQUALS!>a === b<!>) {
b.inc()
}
b<!UNSAFE_CALL!>.<!>inc()
@@ -111,7 +111,7 @@ fun test_9(a: Int, b: Int?) {
}
b<!UNSAFE_CALL!>.<!>inc()
if (b === a) {
if (<!IMPLICIT_BOXING_IN_IDENTITY_EQUALS!>b === a<!>) {
b.inc()
}
b<!UNSAFE_CALL!>.<!>inc()
@@ -22,12 +22,12 @@ enum class Second {
val ONE = Second.THREE
fun foo(f: First) = <!NO_ELSE_IN_WHEN!>when<!> (f) {
<!INCOMPATIBLE_TYPES!>ONE<!> -> 1
<!INCOMPATIBLE_ENUM_COMPARISON_ERROR!>ONE<!> -> 1
TWO -> 2
}
fun bar(s: Second) = <!NO_ELSE_IN_WHEN!>when<!> (s) {
<!INCOMPATIBLE_TYPES!>THREE<!> -> 3
<!INCOMPATIBLE_ENUM_COMPARISON_ERROR!>THREE<!> -> 3
FOUR -> 4
}
@@ -1,4 +1,3 @@
/*
* UNEXPECTED BEHAVIOUR
* ISSUES: KT-37081
@@ -1,8 +1,9 @@
// FULL_JDK
// ISSUE: KT-48113
// STATUS: On a K2 technical meeting on Mar-27-2023 it was decided to report a warning
// WITH_EXTENDED_CHECKERS
fun collapse(path: String) {
val result = (path as <!PLATFORM_CLASS_MAPPED_TO_KOTLIN!>java.lang.String<!>).replace("123", "456")
if (result !== path) {}
if (<!EQUALITY_NOT_APPLICABLE_WARNING!>result !== path<!>) {}
}
@@ -159,6 +159,24 @@ public class FirLightTreeOldFrontendDiagnosticsTestGenerated extends AbstractFir
runTest("compiler/testData/diagnostics/tests/CompareToWithErrorType.kt");
}
@Test
@TestMetadata("comparingArbitraryClasses.kt")
public void testComparingArbitraryClasses() throws Exception {
runTest("compiler/testData/diagnostics/tests/comparingArbitraryClasses.kt");
}
@Test
@TestMetadata("comparingCallableReferencesWithInstanceOfJavaClass.kt")
public void testComparingCallableReferencesWithInstanceOfJavaClass() throws Exception {
runTest("compiler/testData/diagnostics/tests/comparingCallableReferencesWithInstanceOfJavaClass.kt");
}
@Test
@TestMetadata("comparisonOfGenericInterfaceWithGenericClass.kt")
public void testComparisonOfGenericInterfaceWithGenericClass() throws Exception {
runTest("compiler/testData/diagnostics/tests/comparisonOfGenericInterfaceWithGenericClass.kt");
}
@Test
@TestMetadata("Constants.kt")
public void testConstants() throws Exception {
@@ -243,6 +261,12 @@ public class FirLightTreeOldFrontendDiagnosticsTestGenerated extends AbstractFir
runTest("compiler/testData/diagnostics/tests/DiamondProperty.kt");
}
@Test
@TestMetadata("differentNumericTypesFromSmartCast.kt")
public void testDifferentNumericTypesFromSmartCast() throws Exception {
runTest("compiler/testData/diagnostics/tests/differentNumericTypesFromSmartCast.kt");
}
@Test
@TestMetadata("Dollar.kt")
public void testDollar() throws Exception {
@@ -255,6 +279,18 @@ public class FirLightTreeOldFrontendDiagnosticsTestGenerated extends AbstractFir
runTest("compiler/testData/diagnostics/tests/EnumEntryAsType.kt");
}
@Test
@TestMetadata("equalityComparisonToSelf.kt")
public void testEqualityComparisonToSelf() throws Exception {
runTest("compiler/testData/diagnostics/tests/equalityComparisonToSelf.kt");
}
@Test
@TestMetadata("equalityWithSmartCastInIfBlock.kt")
public void testEqualityWithSmartCastInIfBlock() throws Exception {
runTest("compiler/testData/diagnostics/tests/equalityWithSmartCastInIfBlock.kt");
}
@Test
@TestMetadata("ExtensionCallInvoke.kt")
public void testExtensionCallInvoke() throws Exception {
@@ -10392,6 +10428,18 @@ public class FirLightTreeOldFrontendDiagnosticsTestGenerated extends AbstractFir
runTest("compiler/testData/diagnostics/tests/enum/enumWithEmptyName.kt");
}
@Test
@TestMetadata("equalityOfEnumAndParameter.kt")
public void testEqualityOfEnumAndParameter() throws Exception {
runTest("compiler/testData/diagnostics/tests/enum/equalityOfEnumAndParameter.kt");
}
@Test
@TestMetadata("equalityOfFlexibleTypeParameters.kt")
public void testEqualityOfFlexibleTypeParameters() throws Exception {
runTest("compiler/testData/diagnostics/tests/enum/equalityOfFlexibleTypeParameters.kt");
}
@Test
@TestMetadata("ExplicitConstructorCall.kt")
public void testExplicitConstructorCall() throws Exception {
@@ -35359,6 +35407,30 @@ public class FirLightTreeOldFrontendDiagnosticsTestGenerated extends AbstractFir
runTest("compiler/testData/diagnostics/testsWithStdLib/commonCollections.kt");
}
@Test
@TestMetadata("comparingDifferentSubclassesCommonInterface.kt")
public void testComparingDifferentSubclassesCommonInterface() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/comparingDifferentSubclassesCommonInterface.kt");
}
@Test
@TestMetadata("comparingPlatformTypes.kt")
public void testComparingPlatformTypes() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/comparingPlatformTypes.kt");
}
@Test
@TestMetadata("comparingSmartCastValueToBoolean.kt")
public void testComparingSmartCastValueToBoolean() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/comparingSmartCastValueToBoolean.kt");
}
@Test
@TestMetadata("comparingTripleWithPair.kt")
public void testComparingTripleWithPair() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/comparingTripleWithPair.kt");
}
@Test
@TestMetadata("compileTimeUnsignedArray.kt")
public void testCompileTimeUnsignedArray() throws Exception {
@@ -35383,6 +35455,24 @@ public class FirLightTreeOldFrontendDiagnosticsTestGenerated extends AbstractFir
runTest("compiler/testData/diagnostics/testsWithStdLib/enumEntryInitialization.kt");
}
@Test
@TestMetadata("equalityCompatibilityCommonCases.kt")
public void testEqualityCompatibilityCommonCases() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/equalityCompatibilityCommonCases.kt");
}
@Test
@TestMetadata("equalityCompatibilityOldBehavior_Off.kt")
public void testEqualityCompatibilityOldBehavior_Off() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/equalityCompatibilityOldBehavior_Off.kt");
}
@Test
@TestMetadata("equalityCompatibilityOldBehavior_On.kt")
public void testEqualityCompatibilityOldBehavior_On() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/equalityCompatibilityOldBehavior_On.kt");
}
@Test
@TestMetadata("exitProcess.kt")
public void testExitProcess() throws Exception {
@@ -159,6 +159,24 @@ public class FirPsiOldFrontendDiagnosticsTestGenerated extends AbstractFirPsiDia
runTest("compiler/testData/diagnostics/tests/CompareToWithErrorType.kt");
}
@Test
@TestMetadata("comparingArbitraryClasses.kt")
public void testComparingArbitraryClasses() throws Exception {
runTest("compiler/testData/diagnostics/tests/comparingArbitraryClasses.kt");
}
@Test
@TestMetadata("comparingCallableReferencesWithInstanceOfJavaClass.kt")
public void testComparingCallableReferencesWithInstanceOfJavaClass() throws Exception {
runTest("compiler/testData/diagnostics/tests/comparingCallableReferencesWithInstanceOfJavaClass.kt");
}
@Test
@TestMetadata("comparisonOfGenericInterfaceWithGenericClass.kt")
public void testComparisonOfGenericInterfaceWithGenericClass() throws Exception {
runTest("compiler/testData/diagnostics/tests/comparisonOfGenericInterfaceWithGenericClass.kt");
}
@Test
@TestMetadata("Constants.kt")
public void testConstants() throws Exception {
@@ -243,6 +261,12 @@ public class FirPsiOldFrontendDiagnosticsTestGenerated extends AbstractFirPsiDia
runTest("compiler/testData/diagnostics/tests/DiamondProperty.kt");
}
@Test
@TestMetadata("differentNumericTypesFromSmartCast.kt")
public void testDifferentNumericTypesFromSmartCast() throws Exception {
runTest("compiler/testData/diagnostics/tests/differentNumericTypesFromSmartCast.kt");
}
@Test
@TestMetadata("Dollar.kt")
public void testDollar() throws Exception {
@@ -255,6 +279,18 @@ public class FirPsiOldFrontendDiagnosticsTestGenerated extends AbstractFirPsiDia
runTest("compiler/testData/diagnostics/tests/EnumEntryAsType.kt");
}
@Test
@TestMetadata("equalityComparisonToSelf.kt")
public void testEqualityComparisonToSelf() throws Exception {
runTest("compiler/testData/diagnostics/tests/equalityComparisonToSelf.kt");
}
@Test
@TestMetadata("equalityWithSmartCastInIfBlock.kt")
public void testEqualityWithSmartCastInIfBlock() throws Exception {
runTest("compiler/testData/diagnostics/tests/equalityWithSmartCastInIfBlock.kt");
}
@Test
@TestMetadata("ExtensionCallInvoke.kt")
public void testExtensionCallInvoke() throws Exception {
@@ -10398,6 +10434,18 @@ public class FirPsiOldFrontendDiagnosticsTestGenerated extends AbstractFirPsiDia
runTest("compiler/testData/diagnostics/tests/enum/enumWithEmptyName.kt");
}
@Test
@TestMetadata("equalityOfEnumAndParameter.kt")
public void testEqualityOfEnumAndParameter() throws Exception {
runTest("compiler/testData/diagnostics/tests/enum/equalityOfEnumAndParameter.kt");
}
@Test
@TestMetadata("equalityOfFlexibleTypeParameters.kt")
public void testEqualityOfFlexibleTypeParameters() throws Exception {
runTest("compiler/testData/diagnostics/tests/enum/equalityOfFlexibleTypeParameters.kt");
}
@Test
@TestMetadata("ExplicitConstructorCall.kt")
public void testExplicitConstructorCall() throws Exception {
@@ -35455,6 +35503,30 @@ public class FirPsiOldFrontendDiagnosticsTestGenerated extends AbstractFirPsiDia
runTest("compiler/testData/diagnostics/testsWithStdLib/commonCollections.kt");
}
@Test
@TestMetadata("comparingDifferentSubclassesCommonInterface.kt")
public void testComparingDifferentSubclassesCommonInterface() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/comparingDifferentSubclassesCommonInterface.kt");
}
@Test
@TestMetadata("comparingPlatformTypes.kt")
public void testComparingPlatformTypes() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/comparingPlatformTypes.kt");
}
@Test
@TestMetadata("comparingSmartCastValueToBoolean.kt")
public void testComparingSmartCastValueToBoolean() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/comparingSmartCastValueToBoolean.kt");
}
@Test
@TestMetadata("comparingTripleWithPair.kt")
public void testComparingTripleWithPair() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/comparingTripleWithPair.kt");
}
@Test
@TestMetadata("compileTimeUnsignedArray.kt")
public void testCompileTimeUnsignedArray() throws Exception {
@@ -35479,6 +35551,24 @@ public class FirPsiOldFrontendDiagnosticsTestGenerated extends AbstractFirPsiDia
runTest("compiler/testData/diagnostics/testsWithStdLib/enumEntryInitialization.kt");
}
@Test
@TestMetadata("equalityCompatibilityCommonCases.kt")
public void testEqualityCompatibilityCommonCases() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/equalityCompatibilityCommonCases.kt");
}
@Test
@TestMetadata("equalityCompatibilityOldBehavior_Off.kt")
public void testEqualityCompatibilityOldBehavior_Off() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/equalityCompatibilityOldBehavior_Off.kt");
}
@Test
@TestMetadata("equalityCompatibilityOldBehavior_On.kt")
public void testEqualityCompatibilityOldBehavior_On() throws Exception {
runTest("compiler/testData/diagnostics/testsWithStdLib/equalityCompatibilityOldBehavior_On.kt");
}
@Test
@TestMetadata("exitProcess.kt")
public void testExitProcess() throws Exception {
@@ -1338,6 +1338,26 @@ object DIAGNOSTICS_LIST : DiagnosticList("FirErrors") {
parameter<ConeKotlinType>("leftType")
parameter<ConeKotlinType>("rightType")
}
val INCOMPATIBLE_ENUM_COMPARISON by warning<KtElement> {
parameter<ConeKotlinType>("leftType")
parameter<ConeKotlinType>("rightType")
}
val FORBIDDEN_IDENTITY_EQUALS by error<KtElement> {
parameter<ConeKotlinType>("leftType")
parameter<ConeKotlinType>("rightType")
}
val FORBIDDEN_IDENTITY_EQUALS_WARNING by warning<KtElement> {
parameter<ConeKotlinType>("leftType")
parameter<ConeKotlinType>("rightType")
}
val DEPRECATED_IDENTITY_EQUALS by warning<KtElement> {
parameter<ConeKotlinType>("leftType")
parameter<ConeKotlinType>("rightType")
}
val IMPLICIT_BOXING_IN_IDENTITY_EQUALS by warning<KtElement> {
parameter<ConeKotlinType>("leftType")
parameter<ConeKotlinType>("rightType")
}
val INC_DEC_SHOULD_NOT_RETURN_UNIT by error<KtExpression>(PositioningStrategy.OPERATOR)
val ASSIGNMENT_OPERATOR_SHOULD_RETURN_UNIT by error<KtExpression>(PositioningStrategy.OPERATOR) {
parameter<FirNamedFunctionSymbol>("functionSymbol")
@@ -693,6 +693,11 @@ object FirErrors {
val EQUALITY_NOT_APPLICABLE by error3<KtBinaryExpression, String, ConeKotlinType, ConeKotlinType>()
val EQUALITY_NOT_APPLICABLE_WARNING by warning3<KtBinaryExpression, String, ConeKotlinType, ConeKotlinType>()
val INCOMPATIBLE_ENUM_COMPARISON_ERROR by error2<KtElement, ConeKotlinType, ConeKotlinType>()
val INCOMPATIBLE_ENUM_COMPARISON by warning2<KtElement, ConeKotlinType, ConeKotlinType>()
val FORBIDDEN_IDENTITY_EQUALS by error2<KtElement, ConeKotlinType, ConeKotlinType>()
val FORBIDDEN_IDENTITY_EQUALS_WARNING by warning2<KtElement, ConeKotlinType, ConeKotlinType>()
val DEPRECATED_IDENTITY_EQUALS by warning2<KtElement, ConeKotlinType, ConeKotlinType>()
val IMPLICIT_BOXING_IN_IDENTITY_EQUALS by warning2<KtElement, ConeKotlinType, ConeKotlinType>()
val INC_DEC_SHOULD_NOT_RETURN_UNIT by error0<KtExpression>(SourceElementPositioningStrategies.OPERATOR)
val ASSIGNMENT_OPERATOR_SHOULD_RETURN_UNIT by error2<KtExpression, FirNamedFunctionSymbol, String>(SourceElementPositioningStrategies.OPERATOR)
val PROPERTY_AS_OPERATOR by error1<PsiElement, FirPropertySymbol>(SourceElementPositioningStrategies.OPERATOR)
@@ -5,109 +5,247 @@
package org.jetbrains.kotlin.fir.analysis.checkers.expression
import org.jetbrains.kotlin.KtRealSourceElementKind
import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.fir.analysis.checkers.ConeTypeCompatibilityChecker
import org.jetbrains.kotlin.fir.analysis.checkers.ConeTypeCompatibilityChecker.isCompatible
import org.jetbrains.kotlin.KtRealSourceElementKind
import org.jetbrains.kotlin.config.LanguageFeature
import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext
import org.jetbrains.kotlin.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactory2
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors
import org.jetbrains.kotlin.diagnostics.reportOn
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.analysis.checkers.collectUpperBounds
import org.jetbrains.kotlin.fir.declarations.utils.isEnumClass
import org.jetbrains.kotlin.fir.declarations.utils.isFinal
import org.jetbrains.kotlin.fir.declarations.utils.isInline
import org.jetbrains.kotlin.fir.declarations.utils.isInterface
import org.jetbrains.kotlin.fir.expressions.FirEqualityOperatorCall
import org.jetbrains.kotlin.fir.expressions.FirExpression
import org.jetbrains.kotlin.fir.expressions.FirOperation
import org.jetbrains.kotlin.fir.render
import org.jetbrains.kotlin.fir.resolve.toFirRegularClassSymbol
import org.jetbrains.kotlin.fir.expressions.FirSmartCastExpression
import org.jetbrains.kotlin.fir.isPrimitiveType
import org.jetbrains.kotlin.fir.resolve.fullyExpandedType
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.fir.types.impl.ConeClassLikeTypeImpl
import org.jetbrains.kotlin.name.StandardClassIds
import org.jetbrains.kotlin.text
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
object FirEqualityCompatibilityChecker : FirEqualityOperatorCallChecker() {
override fun check(expression: FirEqualityOperatorCall, context: CheckerContext, reporter: DiagnosticReporter) {
val arguments = expression.argumentList.arguments
if (arguments.size != 2) return
val lType = arguments[0].typeRef.coneType
val rType = arguments[1].typeRef.coneType
checkCompatibility(lType, rType, context, expression, reporter)
checkSensibleness(lType, rType, context, expression, reporter)
}
require(arguments.size == 2) { "Equality operator call with non-2 arguments" }
private fun checkCompatibility(
lType: ConeKotlinType,
rType: ConeKotlinType,
context: CheckerContext,
expression: FirEqualityOperatorCall,
reporter: DiagnosticReporter
) {
// If one of the type is already `Nothing?`, we skip reporting further comparison. This is to allow comparing with `null`, which has
// type `Nothing?`
if (lType.isNullableNothing || rType.isNullableNothing) return
val inferenceContext = context.session.typeContext
val l = arguments[0].toArgumentInfo(context)
val r = arguments[1].toArgumentInfo(context)
val compatibility = try {
inferenceContext.isCompatible(lType, rType)
} catch (e: Throwable) {
throw IllegalStateException(
"Exception while determining type compatibility: lType: $lType, rType: $rType, " +
"equality ${expression.render()}, " +
"file ${context.containingFile?.name}",
e
checkSenselessness(l.smartCastType, r.smartCastType, context, expression, reporter)
val checkApplicability = when (expression.operation) {
FirOperation.EQ, FirOperation.NOT_EQ -> ::checkEqualityApplicability
FirOperation.IDENTITY, FirOperation.NOT_IDENTITY -> ::checkIdentityApplicability
else -> error("Invalid operator of FirEqualityOperatorCall")
}
checkApplicability(l.originalTypeInfo, r.originalTypeInfo, context).ifInapplicable {
// Ideally this should match cases when K1
// sees a non-empty intersection and none of the
// types is an enum, but intersections in K1
// work differently from intersections in K2.
val isCaseMissedByK1 = it != Applicability.INAPPLICABLE_AS_ENUMS
&& l.originalTypeInfo.isLiterallyTypeParameter
&& r.originalTypeInfo.isLiterallyTypeParameter
val replicateK1Behavior = !context.languageVersionSettings.supportsFeature(LanguageFeature.ReportErrorsForComparisonOperators)
return reporter.reportInapplicabilityDiagnostic(
expression, it, expression.operation, forceWarning = isCaseMissedByK1 && replicateK1Behavior,
l.originalTypeInfo, r.originalTypeInfo,
l.userType, r.userType, context,
)
}
if (compatibility != ConeTypeCompatibilityChecker.Compatibility.COMPATIBLE) {
when (expression.source?.kind) {
KtRealSourceElementKind -> {
// Note: FE1.0 reports INCOMPATIBLE_ENUM_COMPARISON_ERROR only when TypeIntersector.isIntersectionEmpty() thinks the
// given types are compatible. Exactly mimicking the behavior of FE1.0 is difficult and does not seem to provide any
// value. So instead, we deterministically output INCOMPATIBLE_ENUM_COMPARISON_ERROR if at least one of the value is an
// enum.
if (compatibility == ConeTypeCompatibilityChecker.Compatibility.HARD_INCOMPATIBLE &&
(lType.isEnumType(context) || rType.isEnumType(context))
) {
reporter.reportOn(
expression.source,
FirErrors.INCOMPATIBLE_ENUM_COMPARISON_ERROR,
lType,
rType,
context
)
} else {
reporter.reportOn(
expression.source,
if (compatibility == ConeTypeCompatibilityChecker.Compatibility.HARD_INCOMPATIBLE) {
FirErrors.EQUALITY_NOT_APPLICABLE
} else {
FirErrors.EQUALITY_NOT_APPLICABLE_WARNING
},
expression.operation.operator,
lType,
rType,
context
)
}
}
else -> reporter.reportOn(
expression.source,
if (compatibility == ConeTypeCompatibilityChecker.Compatibility.HARD_INCOMPATIBLE) {
FirErrors.INCOMPATIBLE_TYPES
} else {
FirErrors.INCOMPATIBLE_TYPES_WARNING
},
lType,
rType,
context
)
}
if (l.argument !is FirSmartCastExpression && r.argument !is FirSmartCastExpression) {
return
}
checkApplicability(l.smartCastTypeInfo, r.smartCastTypeInfo, context).ifInapplicable {
return reporter.reportInapplicabilityDiagnostic(
expression, it, expression.operation, forceWarning = true,
l.smartCastTypeInfo, r.smartCastTypeInfo,
l.userType, r.userType, context,
)
}
}
private fun ConeKotlinType.isEnumType(
context: CheckerContext
): Boolean {
if (isEnum) return true
val firRegularClassSymbol = (this as? ConeClassLikeType)?.lookupTag?.toFirRegularClassSymbol(context.session) ?: return false
return firRegularClassSymbol.isEnumClass
private fun checkEqualityApplicability(l: TypeInfo, r: TypeInfo, context: CheckerContext): Applicability {
val oneIsBuiltin = l.isBuiltin || r.isBuiltin
// The compiler should only check comparisons
// when builtins are involved.
// Builtins' supertypes must not be present in
// the list of special fqNames described in RULES1
return when {
oneIsBuiltin && shouldReportAsPerRules1(l, r, context) -> getInapplicabilityFor(l, r)
else -> Applicability.APPLICABLE
}
}
private fun checkSensibleness(
private fun checkIdentityApplicability(l: TypeInfo, r: TypeInfo, context: CheckerContext): Applicability {
// The compiler should only check comparisons
// when identity-less types or builtins are involved.
val oneIsBuiltin = l.isBuiltin || r.isBuiltin
val oneIsNotNull = !l.type.isNullable || !r.type.isNullable
return when {
l.isIdentityLess || r.isIdentityLess -> Applicability.INAPPLICABLE_AS_IDENTITY_LESS
oneIsBuiltin && oneIsNotNull && shouldReportAsPerRules1(l, r, context) -> getInapplicabilityFor(l, r)
else -> Applicability.APPLICABLE
}
}
private fun getInapplicabilityFor(l: TypeInfo, r: TypeInfo): Applicability {
val isIntersectionEmpty = l.enforcesEmptyIntersection || r.enforcesEmptyIntersection
val isOneEnum = l.isEnumClass || r.isEnumClass
return when {
!isIntersectionEmpty && isOneEnum -> Applicability.INAPPLICABLE_AS_ENUMS
else -> Applicability.GENERALLY_INAPPLICABLE
}
}
private fun shouldReportAsPerRules1(l: TypeInfo, r: TypeInfo, context: CheckerContext): Boolean {
// Builtins are always final classes, so
// we only need to check if one is related
// to the other
fun TypeInfo.isSubclassOf(other: TypeInfo) = when {
other.enforcesEmptyIntersection -> type.classId == other.type.classId
else -> notNullType.isSubtypeOf(other.notNullType, context.session)
}
return when {
l.type.isNothingOrNullableNothing || r.type.isNothingOrNullableNothing -> false
else -> !l.isSubclassOf(r) && !r.isSubclassOf(l)
}
}
private enum class Applicability {
APPLICABLE,
GENERALLY_INAPPLICABLE,
INAPPLICABLE_AS_ENUMS,
INAPPLICABLE_AS_IDENTITY_LESS,
}
private inline fun Applicability.ifInapplicable(block: (Applicability) -> Unit) = when (this) {
Applicability.APPLICABLE -> {}
else -> block(this)
}
private fun getGeneralInapplicabilityDiagnostic(forceWarning: Boolean) = when {
forceWarning -> FirErrors.EQUALITY_NOT_APPLICABLE_WARNING
else -> FirErrors.EQUALITY_NOT_APPLICABLE
}
private fun getIdentityLessInapplicabilityDiagnostic(
l: TypeInfo,
r: TypeInfo,
forceWarning: Boolean,
context: CheckerContext,
): KtDiagnosticFactory2<ConeKotlinType, ConeKotlinType> {
val areBothPrimitives = l.isPrimitive && r.isPrimitive
val areSameTypes = l.type.classId == r.type.classId
val shouldProperlyReportError = context.languageVersionSettings.supportsFeature(LanguageFeature.ReportErrorsForComparisonOperators)
// In this case K1 reports nothing
val shouldRelaxDiagnostic = (l.type.isNullableNothing || r.type.isNullableNothing) && !shouldProperlyReportError
return when {
// See: KT-28252
areSameTypes && areBothPrimitives -> FirErrors.DEPRECATED_IDENTITY_EQUALS
// The same reason as above
isIdentityComparedWithImplicitBoxing(l, r, context.session) -> FirErrors.IMPLICIT_BOXING_IN_IDENTITY_EQUALS
forceWarning || shouldRelaxDiagnostic -> FirErrors.FORBIDDEN_IDENTITY_EQUALS_WARNING
else -> FirErrors.FORBIDDEN_IDENTITY_EQUALS
}
}
private fun isIdentityComparedWithImplicitBoxing(l: TypeInfo, r: TypeInfo, session: FirSession) =
isPrimitiveWithNonPrimitiveSupertype(l, r, session) || isPrimitiveWithNonPrimitiveSupertype(r, l, session)
private fun isPrimitiveWithNonPrimitiveSupertype(l: TypeInfo, r: TypeInfo, session: FirSession) =
l.isPrimitive && !r.isPrimitive && l.type.isSubtypeOf(r.type, session)
private fun getSourceLessInapplicabilityDiagnostic(forceWarning: Boolean) = when {
forceWarning -> FirErrors.INCOMPATIBLE_TYPES_WARNING
else -> FirErrors.INCOMPATIBLE_TYPES
}
private fun getEnumInapplicabilityDiagnostic(
l: TypeInfo,
r: TypeInfo,
forceWarning: Boolean,
context: CheckerContext,
): KtDiagnosticFactory2<ConeKotlinType, ConeKotlinType> {
// Preserving the behavior on the old test data
// simplifies detecting fir-differences,
// which is crucial for this checker
val isOldTestData = !context.languageVersionSettings.supportsFeature(
LanguageFeature.ProhibitComparisonOfIncompatibleEnums,
)
// In this corner case K1 reports nothing
val bothNullableEnums = l.isNullableEnum && r.isNullableEnum
// When comparing enums, for type parameters K1
// tries to pick the "representative" superclass
// instead of the proper intersection.
// We can't guarantee that in such cases
// K2 reports the same as K1
val areIntersectionsInvolved = l.type is ConeIntersectionType || r.type is ConeIntersectionType
val shouldProperlyReportError = context.languageVersionSettings.supportsFeature(LanguageFeature.ReportErrorsForComparisonOperators)
val shouldRelaxDiagnostic = (bothNullableEnums || areIntersectionsInvolved) && !shouldProperlyReportError
return when {
forceWarning || isOldTestData || shouldRelaxDiagnostic -> FirErrors.INCOMPATIBLE_ENUM_COMPARISON
else -> FirErrors.INCOMPATIBLE_ENUM_COMPARISON_ERROR
}
}
private fun DiagnosticReporter.reportInapplicabilityDiagnostic(
expression: FirEqualityOperatorCall,
applicability: Applicability,
operation: FirOperation,
forceWarning: Boolean,
l: TypeInfo,
r: TypeInfo,
lUserType: ConeKotlinType,
rUserType: ConeKotlinType,
context: CheckerContext,
): Unit = when {
applicability == Applicability.INAPPLICABLE_AS_IDENTITY_LESS -> reportOn(
expression.source, getIdentityLessInapplicabilityDiagnostic(l, r, forceWarning, context),
lUserType, rUserType, context,
)
applicability == Applicability.INAPPLICABLE_AS_ENUMS -> reportOn(
expression.source, getEnumInapplicabilityDiagnostic(l, r, forceWarning, context),
lUserType, rUserType, context,
)
// This check ensures K2 reports the same diagnostics as K1 used to.
expression.source?.kind !is KtRealSourceElementKind -> reportOn(
expression.source, getSourceLessInapplicabilityDiagnostic(forceWarning),
lUserType, rUserType, context,
)
applicability == Applicability.GENERALLY_INAPPLICABLE -> reportOn(
expression.source, getGeneralInapplicabilityDiagnostic(forceWarning),
operation.operator, lUserType, rUserType, context,
)
else -> error("Shouldn't be here")
}
private fun checkSenselessness(
lType: ConeKotlinType,
rType: ConeKotlinType,
context: CheckerContext,
@@ -139,3 +277,83 @@ object FirEqualityCompatibilityChecker : FirEqualityOperatorCallChecker() {
}
}
}
private class TypeInfo(
val type: ConeKotlinType,
val notNullType: ConeKotlinType,
val isFinalClass: Boolean,
val isEnumClass: Boolean,
val isPrimitive: Boolean,
val isBuiltin: Boolean,
val isValueClass: Boolean,
val isLiterallyTypeParameter: Boolean,
) {
override fun toString() = "$type"
}
private val FirClassSymbol<*>.isBuiltin get() = isPrimitiveType() || classId == StandardClassIds.String || isEnumClass
// This property is used to replicate K1 behavior, and it
// tries to predict empty intersections from the K1 point-of-view.
// Enum classes are final, but enum entries are their subclasses.
private val TypeInfo.enforcesEmptyIntersection get() = isFinalClass && !isEnumClass
private val TypeInfo.isNullableEnum get() = isEnumClass && type.isNullable
private val TypeInfo.isIdentityLess get() = isPrimitive || !type.isNullable && isValueClass
private val FirClassSymbol<*>.isFinalClass get() = isClass && isFinal
// NB: This is what RULES1 means then it says "class".
private val FirClassSymbol<*>.isClass get() = !isInterface
private fun ConeKotlinType.toTypeInfo(session: FirSession): TypeInfo {
val bounds = collectUpperBounds().map { type -> toKotlinType(type).replaceArgumentsWithStarProjections() }
val type = bounds.ifNotEmpty { ConeTypeIntersector.intersectTypes(session.typeContext, this) }
?: session.builtinTypes.nullableAnyType.type
val notNullType = type.withNullability(ConeNullability.NOT_NULL, session.typeContext)
return TypeInfo(
type, notNullType,
isFinalClass = bounds.any { it.toClassSymbol(session)?.isFinalClass == true },
isEnumClass = bounds.any { it.toClassSymbol(session)?.isEnumClass == true },
isPrimitive = bounds.any { it.isPrimitive },
isBuiltin = bounds.any { it.toClassSymbol(session)?.isBuiltin == true },
isValueClass = bounds.any { it.toClassSymbol(session)?.isInline == true },
isLiterallyTypeParameter = this.lowerBoundIfFlexible() is ConeTypeParameterType,
)
}
private fun toKotlinType(type: ConeClassLikeType): ConeClassLikeType {
// Type arguments are ignored by design
return ConeClassLikeTypeImpl(type.lookupTag, type.typeArguments, type.isNullable)
}
private class ArgumentInfo(
val argument: FirExpression,
val userType: ConeKotlinType,
val originalType: ConeKotlinType,
val session: FirSession,
) {
val smartCastType: ConeKotlinType by lazy {
if (argument !is FirSmartCastExpression) originalType else userType.fullyExpandedType(session)
}
val originalTypeInfo get() = originalType.toTypeInfo(session)
val smartCastTypeInfo get() = smartCastType.toTypeInfo(session)
override fun toString() = "${argument.source?.text} :: $userType"
}
@Suppress("RecursivePropertyAccessor")
private val FirExpression.mostOriginalTypeIfSmartCast: ConeKotlinType
get() = when (this) {
is FirSmartCastExpression -> originalExpression.mostOriginalTypeIfSmartCast
else -> typeRef.coneType
}
private fun FirExpression.toArgumentInfo(context: CheckerContext) =
ArgumentInfo(
this, typeRef.coneType, mostOriginalTypeIfSmartCast.fullyExpandedType(context.session), context.session,
)
@@ -221,7 +221,10 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.FINAL_UPPER_BOUND
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.FLOAT_LITERAL_OUT_OF_RANGE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.DEPRECATED_BINARY_MOD
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.EXPOSED_PROPERTY_TYPE_IN_CONSTRUCTOR
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.DEPRECATED_IDENTITY_EQUALS
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.FORBIDDEN_BINARY_MOD
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.FORBIDDEN_IDENTITY_EQUALS
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.FORBIDDEN_IDENTITY_EQUALS_WARNING
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.FORBIDDEN_VARARG_PARAMETER_TYPE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.FUNCTION_CALL_EXPECTED
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.FUNCTION_DECLARATION_WITH_NO_NAME
@@ -249,6 +252,7 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ILLEGAL_SUSPEND_F
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ILLEGAL_SUSPEND_PROPERTY_ACCESS
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.ILLEGAL_UNDERSCORE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.IMPLEMENTATION_BY_DELEGATION_IN_EXPECT_CLASS
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.IMPLICIT_BOXING_IN_IDENTITY_EQUALS
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.IMPLICIT_NOTHING_PROPERTY_TYPE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.IMPLICIT_NOTHING_RETURN_TYPE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INAPPLICABLE_CANDIDATE
@@ -262,6 +266,7 @@ import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INAPPLICABLE_TARG
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INAPPLICABLE_TARGET_PROPERTY_HAS_NO_BACKING_FIELD
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INAPPLICABLE_TARGET_PROPERTY_HAS_NO_DELEGATE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INAPPLICABLE_TARGET_PROPERTY_IMMUTABLE
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INCOMPATIBLE_ENUM_COMPARISON
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INCOMPATIBLE_ENUM_COMPARISON_ERROR
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INCOMPATIBLE_MODIFIERS
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors.INCOMPATIBLE_TYPES
@@ -2002,6 +2007,36 @@ object FirErrorsDefaultMessages : BaseDiagnosticRendererFactory() {
RENDER_TYPE,
RENDER_TYPE
)
map.put(
INCOMPATIBLE_ENUM_COMPARISON,
"Comparison of incompatible enums ''{0}'' and ''{1}'' is always unsuccessful",
RENDER_TYPE,
RENDER_TYPE
)
map.put(
FORBIDDEN_IDENTITY_EQUALS,
"Identity equality for arguments of types {0} and {1} is forbidden",
RENDER_TYPE,
RENDER_TYPE
)
map.put(
FORBIDDEN_IDENTITY_EQUALS_WARNING,
"Identity equality for arguments of types {0} and {1} is forbidden",
RENDER_TYPE,
RENDER_TYPE
)
map.put(
DEPRECATED_IDENTITY_EQUALS,
"Identity equality for arguments of types {0} and {1} is deprecated",
RENDER_TYPE,
RENDER_TYPE
)
map.put(
IMPLICIT_BOXING_IN_IDENTITY_EQUALS,
"Identity equality for arguments of types {0} and {1} can be unstable because of implicit boxing",
RENDER_TYPE,
RENDER_TYPE
)
map.put(INC_DEC_SHOULD_NOT_RETURN_UNIT, "Functions inc(), dec() shouldn't return Unit to be used by operators ++, --")
map.put(
ASSIGNMENT_OPERATOR_SHOULD_RETURN_UNIT,
@@ -23,10 +23,7 @@ import org.jetbrains.kotlin.fir.resolve.substitution.substitutorByMap
import org.jetbrains.kotlin.fir.resolve.toSymbol
import org.jetbrains.kotlin.fir.resolvedTypeFromPrototype
import org.jetbrains.kotlin.fir.symbols.ConeTypeParameterLookupTag
import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirClassifierSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirRegularClassSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirTypeParameterSymbol
import org.jetbrains.kotlin.fir.symbols.impl.*
import org.jetbrains.kotlin.fir.symbols.lazyResolveToPhase
import org.jetbrains.kotlin.fir.types.builder.buildErrorTypeRef
import org.jetbrains.kotlin.fir.types.builder.buildResolvedTypeRef
@@ -607,6 +604,10 @@ fun ConeKotlinType.toRegularClassSymbol(session: FirSession): FirRegularClassSym
return (this as? ConeClassLikeType)?.toRegularClassSymbol(session)
}
fun ConeClassLikeType.toClassSymbol(session: FirSession): FirClassSymbol<*>? {
return fullyExpandedType(session).toSymbol(session) as? FirClassSymbol<*>
}
private fun lowerThanBound(context: ConeInferenceContext, argument: ConeKotlinType, typeParameterSymbol: FirTypeParameterSymbol): Boolean {
typeParameterSymbol.resolvedBounds.forEach { boundTypeRef ->
if (argument != boundTypeRef.coneType && argument.isSubtypeOf(context, boundTypeRef.coneType)) {
@@ -83,12 +83,18 @@ inline val FirClassSymbol<*>.isLocalClassOrAnonymousObject: Boolean
get() = classId.isLocal || this is FirAnonymousObjectSymbol
inline val FirClassSymbol<*>.isClass: Boolean
get() = classKind.isClass
inline val FirClassSymbol<*>.isInterface: Boolean
get() = classKind.isInterface
inline val FirClassSymbol<*>.isEnumClass: Boolean
get() = classKind.isEnumClass
inline val FirClassSymbol<*>.isEnumEntry: Boolean
get() = classKind.isEnumEntry
// ---------------------- specific callables ----------------------
inline val FirPropertyAccessorSymbol.allowsToHaveFakeOverride: Boolean get() = visibility.allowsToHaveFakeOverride