Extract checks on equality expressions into a separate checker

^KT-43493 Fixed
This commit is contained in:
Victor Petukhov
2021-08-10 10:52:16 +03:00
committed by TeamCityServer
parent 598f09d7a5
commit 060af85a93
7 changed files with 160 additions and 64 deletions
@@ -67,6 +67,7 @@ private val DEFAULT_CALL_CHECKERS = listOf(
ReferencingToUnderscoreNamedParameterOfCatchBlockChecker, VarargWrongExecutionOrderChecker, SelfCallInNestedObjectConstructorChecker,
NewSchemeOfIntegerOperatorResolutionChecker, EnumEntryVsCompanionPriorityCallChecker, CompanionInParenthesesLHSCallChecker,
ResolutionToPrivateConstructorOfSealedClassChecker,
EqualityCallChecker,
)
private val DEFAULT_TYPE_CHECKERS = emptyList<AdditionalTypeChecker>()
private val DEFAULT_CLASSIFIER_USAGE_CHECKERS = listOf(
@@ -0,0 +1,100 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.resolve.calls.checkers
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.psi.KtBinaryExpression
import org.jetbrains.kotlin.resolve.calls.context.ResolutionContext
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.isInlineClassType
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeIntersector
import org.jetbrains.kotlin.types.checkEnumsForCompatibility
import org.jetbrains.kotlin.types.checker.KotlinTypeChecker
import org.jetbrains.kotlin.types.expressions.OperatorConventions
import org.jetbrains.kotlin.types.expressions.SenselessComparisonChecker.checkSenselessComparisonWithNull
import org.jetbrains.kotlin.types.typeUtil.builtIns
import org.jetbrains.kotlin.util.OperatorNameConventions
object EqualityCallChecker : CallChecker {
override fun check(resolvedCall: ResolvedCall<*>, reportOn: PsiElement, context: CallCheckerContext) {
val callExpression = resolvedCall.call.callElement as? KtBinaryExpression ?: return
val operationType = callExpression.operationReference.getReferencedNameElementType()
if (operationType in OperatorConventions.EQUALS_OPERATIONS) {
checkEquality(resolvedCall, callExpression, context.resolutionContext)
}
}
private fun checkEquality(resolvedCall: ResolvedCall<*>, expression: KtBinaryExpression, context: ResolutionContext<*>) {
val returnType = resolvedCall.resultingDescriptor.returnType ?: return
val builtIns = returnType.builtIns
if (!builtIns.isBooleanOrSubtype(returnType)) {
context.trace.report(
Errors.RESULT_TYPE_MISMATCH.on(
expression.operationReference,
"'${OperatorNameConventions.EQUALS}'",
builtIns.booleanType,
returnType
)
)
return
}
ensureNonemptyIntersectionOfOperandTypes(expression, context)
}
// We check identity equality separately because we don't have a call for it
fun checkIdentityEquality(expression: KtBinaryExpression, context: ResolutionContext<*>) {
ensureNonemptyIntersectionOfOperandTypes(expression, context)
checkIdentityOnPrimitiveOrInlineClassTypes(expression, context)
}
private fun checkIdentityOnPrimitiveOrInlineClassTypes(expression: KtBinaryExpression, context: ResolutionContext<*>) {
val left = expression.left ?: return
val right = expression.right ?: return
val leftType = context.trace.getType(left) ?: return
val rightType = context.trace.getType(right) ?: return
if (KotlinTypeChecker.DEFAULT.equalTypes(leftType, rightType)) {
if (KotlinBuiltIns.isPrimitiveType(leftType)) {
context.trace.report(Errors.DEPRECATED_IDENTITY_EQUALS.on(expression, leftType, rightType))
}
} else if (isIdentityComparedWithImplicitBoxing(leftType, rightType) || isIdentityComparedWithImplicitBoxing(rightType, leftType)) {
context.trace.report(Errors.IMPLICIT_BOXING_IN_IDENTITY_EQUALS.on(expression, leftType, rightType))
}
if (leftType.isInlineClassType() || rightType.isInlineClassType()) {
context.trace.report(Errors.FORBIDDEN_IDENTITY_EQUALS.on(expression, leftType, rightType))
}
}
private fun isIdentityComparedWithImplicitBoxing(leftType: KotlinType, rightType: KotlinType) =
KotlinBuiltIns.isPrimitiveType(leftType)
&& !KotlinBuiltIns.isPrimitiveType(rightType)
&& KotlinTypeChecker.DEFAULT.isSubtypeOf(leftType, rightType)
private fun ensureNonemptyIntersectionOfOperandTypes(expression: KtBinaryExpression, context: ResolutionContext<*>) {
val left = expression.left ?: return
val right = expression.right ?: return
val leftType = context.trace.getType(left) ?: return
val rightType = context.trace.getType(right) ?: return
if (TypeIntersector.isIntersectionEmpty(leftType, rightType)) {
context.trace.report(Errors.EQUALITY_NOT_APPLICABLE.on(expression, expression.operationReference, leftType, rightType))
} else {
checkEnumsForCompatibility(context, expression, leftType, rightType)
}
checkSenselessComparisonWithNull(
expression, left, right, context, context.trace::getType, context.dataFlowInfo::getStableNullability
)
}
}
@@ -11,10 +11,10 @@ import org.jetbrains.kotlin.descriptors.TypeParameterDescriptor
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.types.expressions.ExpressionTypingContext
import org.jetbrains.kotlin.resolve.calls.context.ResolutionContext
import org.jetbrains.kotlin.types.typeUtil.*
fun checkEnumsForCompatibility(context: ExpressionTypingContext, reportOn: KtElement, typeA: KotlinType, typeB: KotlinType) {
fun checkEnumsForCompatibility(context: ResolutionContext<*>, reportOn: KtElement, typeA: KotlinType, typeB: KotlinType) {
if (isIncompatibleEnums(typeA, typeB)) {
val diagnostic = if (context.languageVersionSettings.supportsFeature(LanguageFeature.ProhibitComparisonOfIncompatibleEnums)) {
Errors.INCOMPATIBLE_ENUM_COMPARISON_ERROR
@@ -1096,10 +1096,7 @@ public class BasicExpressionTypingVisitor extends ExpressionTypingVisitor {
result = visitEquality(expression, context, operationSign, left, right);
}
else if (OperatorConventions.IDENTITY_EQUALS_OPERATIONS.contains(operationType)) {
ensureNonemptyIntersectionOfOperandTypes(expression, context);
// TODO : Check comparison pointlessness
result = TypeInfoFactoryKt.createTypeInfo(components.builtIns.getBooleanType(), context);
checkIdentityOnPrimitiveOrInlineClassTypes(expression, context);
result = visitIdentityEquality(expression, context);
}
else if (OperatorConventions.IN_OPERATIONS.contains(operationType)) {
ValueArgument leftArgument = CallMaker.makeValueArgument(left, left != null ? left : operationSign);
@@ -1121,33 +1118,24 @@ public class BasicExpressionTypingVisitor extends ExpressionTypingVisitor {
return components.dataFlowAnalyzer.checkType(result, expression, contextWithExpectedType);
}
private static void checkIdentityOnPrimitiveOrInlineClassTypes(
private KotlinTypeInfo visitIdentityEquality(
@NotNull KtBinaryExpression expression,
@NotNull ExpressionTypingContext context
) {
if (expression.getLeft() == null || expression.getRight() == null) return;
KotlinTypeInfo typeInfo = TypeInfoFactoryKt.createTypeInfo(components.builtIns.getBooleanType(), context);
KotlinType leftType = context.trace.getType(expression.getLeft());
KotlinType rightType = context.trace.getType(expression.getRight());
if (leftType == null || rightType == null) return;
KtExpression left = expression.getLeft();
KtExpression right = expression.getRight();
if (KotlinTypeChecker.DEFAULT.equalTypes(leftType, rightType)) {
if (KotlinBuiltIns.isPrimitiveType(leftType)) {
context.trace.report(DEPRECATED_IDENTITY_EQUALS.on(expression, leftType, rightType));
}
}
else if (isIdentityComparedWithImplicitBoxing(leftType, rightType) || isIdentityComparedWithImplicitBoxing(rightType, leftType)) {
context.trace.report(IMPLICIT_BOXING_IN_IDENTITY_EQUALS.on(expression, leftType, rightType));
}
if (InlineClassesUtilsKt.isInlineClassType(leftType) || InlineClassesUtilsKt.isInlineClassType(rightType)) {
context.trace.report(FORBIDDEN_IDENTITY_EQUALS.on(expression, leftType, rightType));
}
}
if (left == null || right == null) return typeInfo;
private static boolean isIdentityComparedWithImplicitBoxing(KotlinType leftType, KotlinType rightType) {
return KotlinBuiltIns.isPrimitiveType(leftType) &&
!KotlinBuiltIns.isPrimitiveType(rightType) &&
KotlinTypeChecker.DEFAULT.isSubtypeOf(leftType, rightType);
// compute type of LHS and RHS
facade.getTypeInfo(left, context);
facade.getTypeInfo(right, context);
EqualityCallChecker.INSTANCE.checkIdentityEquality(expression, context);
return typeInfo;
}
private KotlinTypeInfo visitEquality(
@@ -1185,17 +1173,10 @@ public class BasicExpressionTypingVisitor extends ExpressionTypingVisitor {
OverloadResolutionResults<FunctionDescriptor> resolutionResults =
components.callResolver.resolveEqualsCallWithGivenDescriptors(contextWithDataFlow, operationSign, receiver, call, equalsFunctions);
if (resolutionResults.isSuccess()) {
FunctionDescriptor equals = resolutionResults.getResultingCall().getResultingDescriptor();
if (ensureBooleanResult(operationSign, OperatorNameConventions.EQUALS, equals.getReturnType(), contextWithDataFlow)) {
ensureNonemptyIntersectionOfOperandTypes(expression, contextWithDataFlow);
}
}
else {
if (!resolutionResults.isSuccess()) {
if (resolutionResults.isAmbiguity()) {
context.trace.report(OVERLOAD_RESOLUTION_AMBIGUITY.on(operationSign, resolutionResults.getResultingCalls()));
}
else {
} else {
context.trace.report(EQUALS_MISSING.on(operationSign));
}
}
@@ -1463,34 +1444,6 @@ public class BasicExpressionTypingVisitor extends ExpressionTypingVisitor {
return true;
}
private void ensureNonemptyIntersectionOfOperandTypes(KtBinaryExpression expression, ExpressionTypingContext context) {
KtExpression left = expression.getLeft();
if (left == null) return;
KtExpression right = expression.getRight();
// TODO : duplicated effort for == and !=
KotlinType leftType = facade.getTypeInfo(left, context).getType();
if (leftType != null && right != null) {
KotlinType rightType = facade.getTypeInfo(right, context).getType();
if (rightType != null) {
if (TypeIntersector.isIntersectionEmpty(leftType, rightType)) {
context.trace.report(EQUALITY_NOT_APPLICABLE.on(expression, expression.getOperationReference(), leftType, rightType));
} else {
EnumCompatibilityCheckerKt.checkEnumsForCompatibility(context, expression, leftType, rightType);
}
SenselessComparisonChecker.checkSenselessComparisonWithNull(
expression, left, right, context,
expr -> facade.getTypeInfo(expr, context).getType(),
context.dataFlowInfo::getStableNullability
);
}
}
}
@NotNull
private KotlinTypeInfo visitAssignmentOperation(KtBinaryExpression expression, ExpressionTypingContext context) {
return assignmentIsNotAnExpressionError(expression, context);
@@ -0,0 +1,19 @@
// WITH_RUNTIME
fun main(x: Long, y: Int) {
sequence {
1L == 3
x == 3
3 == 1L
3 == x
y == x
1L === 3
x === 3
3 === 1L
3 === x
y === x
yield("")
}
}
@@ -0,0 +1,19 @@
// WITH_RUNTIME
fun main(x: Long, y: Int) {
sequence {
<!EQUALITY_NOT_APPLICABLE("==; Long; Int")!>1L == 3<!>
<!EQUALITY_NOT_APPLICABLE("==; Long; Int")!>x == 3<!>
<!EQUALITY_NOT_APPLICABLE("==; Int; Long")!>3 == 1L<!>
<!EQUALITY_NOT_APPLICABLE("==; Int; Long")!>3 == x<!>
<!EQUALITY_NOT_APPLICABLE("==; Int; Long")!>y == x<!>
<!EQUALITY_NOT_APPLICABLE("===; Long; Int")!>1L === 3<!>
<!EQUALITY_NOT_APPLICABLE("===; Long; Int")!>x === 3<!>
<!EQUALITY_NOT_APPLICABLE("===; Int; Long")!>3 === 1L<!>
<!EQUALITY_NOT_APPLICABLE("===; Int; Long")!>3 === x<!>
<!EQUALITY_NOT_APPLICABLE("===; Int; Long")!>y === x<!>
yield("")
}
}
@@ -0,0 +1,4 @@
package
public fun main(/*0*/ x: kotlin.Long, /*1*/ y: kotlin.Int): kotlin.Unit