JS backend: fix equality check for nullable types
* Generate simple check when compare with `null` literal. * Use Kotlin.equals if type of receiver or argument is nullable. * Don't call equals method if any of receiver or argument is null (at runtime, not only literal). For more information see KT-4356. #KT-7530 fixed #KT-7916 fixed
This commit is contained in:
@@ -53,4 +53,16 @@ public final class EqualsTest extends AbstractExpressionTest {
|
|||||||
public void testCompareNullableListWithNull() throws Exception {
|
public void testCompareNullableListWithNull() throws Exception {
|
||||||
checkFooBoxIsOk();
|
checkFooBoxIsOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void testCompareToNullWithCustomEquals() throws Exception {
|
||||||
|
checkFooBoxIsOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCompareNullablesWithCustomEquals() throws Exception {
|
||||||
|
checkFooBoxIsOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testEqualsBehaviorOnNull() throws Exception {
|
||||||
|
checkFooBoxIsOk();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-1
@@ -26,6 +26,7 @@ import org.jetbrains.kotlin.js.translate.context.TemporaryVariable;
|
|||||||
import org.jetbrains.kotlin.js.translate.context.TranslationContext;
|
import org.jetbrains.kotlin.js.translate.context.TranslationContext;
|
||||||
import org.jetbrains.kotlin.js.translate.general.AbstractTranslator;
|
import org.jetbrains.kotlin.js.translate.general.AbstractTranslator;
|
||||||
import org.jetbrains.kotlin.js.translate.general.Translation;
|
import org.jetbrains.kotlin.js.translate.general.Translation;
|
||||||
|
import org.jetbrains.kotlin.js.translate.intrinsic.functions.factories.TopLevelFIF;
|
||||||
import org.jetbrains.kotlin.js.translate.intrinsic.operation.BinaryOperationIntrinsic;
|
import org.jetbrains.kotlin.js.translate.intrinsic.operation.BinaryOperationIntrinsic;
|
||||||
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils;
|
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils;
|
||||||
import org.jetbrains.kotlin.js.translate.utils.TranslationUtils;
|
import org.jetbrains.kotlin.js.translate.utils.TranslationUtils;
|
||||||
@@ -35,8 +36,12 @@ import org.jetbrains.kotlin.psi.JetBinaryExpression;
|
|||||||
import org.jetbrains.kotlin.psi.JetExpression;
|
import org.jetbrains.kotlin.psi.JetExpression;
|
||||||
import org.jetbrains.kotlin.resolve.bindingContextUtil.BindingContextUtilPackage;
|
import org.jetbrains.kotlin.resolve.bindingContextUtil.BindingContextUtilPackage;
|
||||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall;
|
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall;
|
||||||
|
import org.jetbrains.kotlin.types.JetType;
|
||||||
|
import org.jetbrains.kotlin.types.TypeUtils;
|
||||||
import org.jetbrains.kotlin.types.expressions.OperatorConventions;
|
import org.jetbrains.kotlin.types.expressions.OperatorConventions;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
import static org.jetbrains.kotlin.js.translate.operation.AssignmentTranslator.isAssignmentOperator;
|
import static org.jetbrains.kotlin.js.translate.operation.AssignmentTranslator.isAssignmentOperator;
|
||||||
import static org.jetbrains.kotlin.js.translate.operation.CompareToTranslator.isCompareToCall;
|
import static org.jetbrains.kotlin.js.translate.operation.CompareToTranslator.isCompareToCall;
|
||||||
import static org.jetbrains.kotlin.js.translate.utils.BindingUtils.getCallableDescriptorForOperationExpression;
|
import static org.jetbrains.kotlin.js.translate.utils.BindingUtils.getCallableDescriptorForOperationExpression;
|
||||||
@@ -108,6 +113,9 @@ public final class BinaryOperationTranslator extends AbstractTranslator {
|
|||||||
if (isCompareToCall(operationToken, operationDescriptor)) {
|
if (isCompareToCall(operationToken, operationDescriptor)) {
|
||||||
return CompareToTranslator.translate(expression, context());
|
return CompareToTranslator.translate(expression, context());
|
||||||
}
|
}
|
||||||
|
if (isEquals()) {
|
||||||
|
return translateEquals();
|
||||||
|
}
|
||||||
assert operationDescriptor != null :
|
assert operationDescriptor != null :
|
||||||
"Overloadable operations must have not null descriptor";
|
"Overloadable operations must have not null descriptor";
|
||||||
return translateAsOverloadedBinaryOperation();
|
return translateAsOverloadedBinaryOperation();
|
||||||
@@ -246,11 +254,33 @@ public final class BinaryOperationTranslator extends AbstractTranslator {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isEquals() {
|
||||||
|
return operationToken == JetTokens.EQEQ || operationToken == JetTokens.EXCLEQ;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsExpression translateEquals() {
|
||||||
|
JsExpression left = Translation.translateAsExpression(leftJetExpression, context());
|
||||||
|
JsExpression right = Translation.translateAsExpression(rightJetExpression, context());
|
||||||
|
|
||||||
|
if (left == JsLiteral.NULL || right == JsLiteral.NULL) {
|
||||||
|
JsBinaryOperator operator = operationToken == JetTokens.EXCLEQ ? JsBinaryOperator.NEQ : JsBinaryOperator.EQ;
|
||||||
|
return new JsBinaryOperation(operator, left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
JetType leftType = context().bindingContext().getType(leftJetExpression);
|
||||||
|
JetType rightType = context().bindingContext().getType(rightJetExpression);
|
||||||
|
|
||||||
|
if (leftType != null && TypeUtils.isNullableType(leftType) || rightType != null && TypeUtils.isNullableType(rightType)) {
|
||||||
|
return mayBeWrapWithNegation(TopLevelFIF.KOTLIN_EQUALS.apply(left, Collections.singletonList(right), context()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return translateAsOverloadedBinaryOperation();
|
||||||
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private JsExpression translateAsOverloadedBinaryOperation() {
|
private JsExpression translateAsOverloadedBinaryOperation() {
|
||||||
ResolvedCall<? extends FunctionDescriptor> resolvedCall = getFunctionResolvedCallWithAssert(expression, bindingContext());
|
ResolvedCall<? extends FunctionDescriptor> resolvedCall = getFunctionResolvedCallWithAssert(expression, bindingContext());
|
||||||
JsExpression result = CallTranslator.INSTANCE$.translate(context(), resolvedCall, getReceiver());
|
JsExpression result = CallTranslator.translate(context(), resolvedCall, getReceiver());
|
||||||
return mayBeWrapWithNegation(result);
|
return mayBeWrapWithNegation(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package foo
|
||||||
|
|
||||||
|
class A {
|
||||||
|
override fun equals(other: Any?) = this === other
|
||||||
|
}
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
val a: A? = null
|
||||||
|
val b: A? = null
|
||||||
|
val c: A? = A()
|
||||||
|
val d: A? = A()
|
||||||
|
val e: A = A()
|
||||||
|
|
||||||
|
// compare nullable vals with null
|
||||||
|
testTrue { a == b }
|
||||||
|
testTrue { a == a }
|
||||||
|
testFalse { a != b }
|
||||||
|
testFalse { a != a }
|
||||||
|
|
||||||
|
// compare null and non-null inside nullable vals
|
||||||
|
testFalse { a == c }
|
||||||
|
testTrue { a != c }
|
||||||
|
testFalse { c == a }
|
||||||
|
testTrue { c != a }
|
||||||
|
|
||||||
|
// compare nullables vals with non-null
|
||||||
|
testFalse { c == d }
|
||||||
|
testTrue { c == c }
|
||||||
|
testTrue { c != d }
|
||||||
|
testFalse { d == c }
|
||||||
|
testTrue { d != c }
|
||||||
|
testFalse { d != d }
|
||||||
|
|
||||||
|
// compare nullable val with null with non-nullable
|
||||||
|
testFalse { a == e }
|
||||||
|
testTrue { a != e }
|
||||||
|
testFalse { e == a }
|
||||||
|
testTrue { e != a }
|
||||||
|
|
||||||
|
// compare nullable val with non-null with non-nullable
|
||||||
|
testFalse { c == e }
|
||||||
|
testTrue { c != e }
|
||||||
|
testFalse { e == c }
|
||||||
|
testTrue { e != c }
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package foo
|
||||||
|
|
||||||
|
class A {
|
||||||
|
override fun equals(other: Any?) = super.equals(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
val a: A? = null
|
||||||
|
|
||||||
|
testTrue { a == null }
|
||||||
|
testFalse { a != null }
|
||||||
|
testTrue { null == a }
|
||||||
|
testFalse { null != a }
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package foo
|
||||||
|
|
||||||
|
var log = ""
|
||||||
|
|
||||||
|
class A {
|
||||||
|
override fun equals(o: Any?): Boolean {
|
||||||
|
log += "$o;"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
val a = A()
|
||||||
|
|
||||||
|
assertTrue(a.equals("aaa"))
|
||||||
|
assertEquals("aaa;", log)
|
||||||
|
assertFalse(a == null)
|
||||||
|
assertEquals("aaa;", log)
|
||||||
|
assertTrue(a.equals(null))
|
||||||
|
assertEquals("aaa;null;", log)
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+4
@@ -47,6 +47,10 @@
|
|||||||
return obj2 == null;
|
return obj2 == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (obj2 == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(obj1)) {
|
if (Array.isArray(obj1)) {
|
||||||
return Kotlin.arrayEquals(obj1, obj2);
|
return Kotlin.arrayEquals(obj1, obj2);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user