diff --git a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java index ea7be1f238..1c3ac25ab7 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java +++ b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java @@ -3737,7 +3737,15 @@ public static boolean eq(Object x, Object y) { if (isSymbol(y) && isObject(x)) { return eq(toPrimitive(x), y); } - if (y instanceof Scriptable) { + if (y == null || Undefined.isUndefined(y)) { + if (x instanceof ScriptableObject) { + Object test = ((ScriptableObject) x).equivalentValues(y); + if (test != Scriptable.NOT_FOUND) { + return ((Boolean) test).booleanValue(); + } + } + return false; + } else if (y instanceof Scriptable) { if (x instanceof ScriptableObject) { Object test = ((ScriptableObject) x).equivalentValues(y); if (test != Scriptable.NOT_FOUND) { diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/BodyCodegen.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/BodyCodegen.java index f4432f9c97..6bb3a000de 100644 --- a/rhino/src/main/java/org/mozilla/javascript/optimizer/BodyCodegen.java +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/BodyCodegen.java @@ -3934,34 +3934,18 @@ private void visitIfJumpEqOp(Node node, Node child, int trueGOTO, int falseGOTO) int type = node.getType(); Node rChild = child.getNext(); - // Optimize if one of operands is null - if (child.getType() == Token.NULL || rChild.getType() == Token.NULL) { + // Optimize if one of operands is null; but we can't do this + // for EQ/NEQ because a ScripableObject might overwrite equivalentValues() + if (type != Token.EQ + && type != Token.NE + && (child.getType() == Token.NULL || rChild.getType() == Token.NULL)) { // eq is symmetric in this case if (child.getType() == Token.NULL) { child = rChild; } generateExpression(child, node); - if (type == Token.SHEQ || type == Token.SHNE) { - int testCode = (type == Token.SHEQ) ? ByteCode.IFNULL : ByteCode.IFNONNULL; - cfw.add(testCode, trueGOTO); - } else { - if (type != Token.EQ) { - // swap false/true targets for != - if (type != Token.NE) throw Codegen.badTree(); - int tmp = trueGOTO; - trueGOTO = falseGOTO; - falseGOTO = tmp; - } - cfw.add(ByteCode.DUP); - int undefCheckLabel = cfw.acquireLabel(); - cfw.add(ByteCode.IFNONNULL, undefCheckLabel); - int stack = cfw.getStackTop(); - cfw.add(ByteCode.POP); - cfw.add(ByteCode.GOTO, trueGOTO); - cfw.markLabel(undefCheckLabel, stack); - Codegen.pushUndefined(cfw); - cfw.add(ByteCode.IF_ACMPEQ, trueGOTO); - } + int testCode = (type == Token.SHEQ) ? ByteCode.IFNULL : ByteCode.IFNONNULL; + cfw.add(testCode, trueGOTO); cfw.add(ByteCode.GOTO, falseGOTO); } else { int child_dcp_register = nodeIsDirectCallParameter(child); diff --git a/rhino/src/test/java/org/mozilla/javascript/tests/ScriptRuntimeEquivalentValuesTest.java b/rhino/src/test/java/org/mozilla/javascript/tests/ScriptRuntimeEquivalentValuesTest.java new file mode 100644 index 0000000000..7dedbf2dad --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/tests/ScriptRuntimeEquivalentValuesTest.java @@ -0,0 +1,86 @@ +package org.mozilla.javascript.tests; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.mozilla.javascript.*; +import org.mozilla.javascript.annotations.JSConstructor; + +/** + * Test cases for the {@link ScriptRuntime} support for ScriptableObject#equivalentValues(Object) + * method. + * + * @author Ronald Brill + */ +public class ScriptRuntimeEquivalentValuesTest { + + @Test + public void equivalentValuesUndefined() throws Exception { + Utils.runWithAllOptimizationLevels( + cx -> { + final Scriptable scope = cx.initStandardObjects(); + try { + ScriptableObject.defineClass(scope, EquivalentTesterObject.class); + } catch (Exception e) { + } + + Object result = + cx.evaluateString( + scope, + "var o = new EquivalentTesterObject();" + + "'' + (o == undefined) + ' ' + (undefined == o)", + "test", + 1, + null); + assertEquals("" + cx.getOptimizationLevel(), "true true", result); + + return null; + }); + } + + @Test + public void equivalentValuesNull() throws Exception { + Utils.runWithAllOptimizationLevels( + cx -> { + final Scriptable scope = cx.initStandardObjects(); + try { + ScriptableObject.defineClass(scope, EquivalentTesterObject.class); + } catch (Exception e) { + } + + Object result = + cx.evaluateString( + scope, + "var o = new EquivalentTesterObject();" + + "'' + (o == null) + ' ' + (null == o)", + "test", + 1, + null); + assertEquals("" + cx.getOptimizationLevel(), "true true", result); + + return null; + }); + } + + public static class EquivalentTesterObject extends ScriptableObject { + + public EquivalentTesterObject() {} + + @Override + public String getClassName() { + return "EquivalentTesterObject"; + } + + @JSConstructor + public void jsConstructorMethod() {} + + @Override + protected Object equivalentValues(final Object value) { + if (value == null || Undefined.isUndefined(value)) { + return Boolean.TRUE; + } + + return super.equivalentValues(value); + } + } +}