diff --git a/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java b/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java index fbaae75043..68d5b756d7 100644 --- a/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java +++ b/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java @@ -31,7 +31,10 @@ static void init(Context cx, Scriptable scope, boolean sealed) { if (cx.getLanguageVersion() >= Context.VERSION_ES6) { obj.setStandardPropertyAttributes(READONLY | DONTENUM); } - obj.exportAsJSClass(MAX_PROTOTYPE_ID, scope, sealed); + IdFunctionObject constructor = obj.exportAsJSClass(MAX_PROTOTYPE_ID, scope, sealed); + if (cx.getLanguageVersion() >= Context.VERSION_ES6) { + ScriptRuntimeES6.addSymbolHasInstance(cx, scope, constructor); + } } /** @@ -80,7 +83,7 @@ protected boolean hasDefaultParameters() { /** * Gets the value returned by calling the typeof operator on this object. * - * @see org.mozilla.javascript.ScriptableObject#getTypeOf() + * @see ScriptableObject#getTypeOf() * @return "function" or "undefined" if {@link #avoidObjectDetection()} returns true * */ @@ -156,6 +159,8 @@ protected int findInstanceIdInfo(String s) { @Override protected String getInstanceIdName(int id) { switch (id) { + case SymbolId_hasInstance: + return "SymbolId_hasInstance"; case Id_length: return "length"; case Id_arity: @@ -265,6 +270,11 @@ protected void fillConstructorProperties(IdFunctionObject ctor) { @Override protected void initPrototypeId(int id) { + if (id == SymbolId_hasInstance) { + initPrototypeValue(id, SymbolKey.HAS_INSTANCE, makeHasInstance(), CONST | DONTENUM); + return; + } + String s; int arity; switch (id) { @@ -313,6 +323,52 @@ static boolean isApplyOrCall(IdFunctionObject f) { return false; } + private Object makeHasInstance() { + Context cx = Context.getCurrentContext(); + ScriptableObject obj = null; + + if (cx != null) { + Scriptable scope = this.getParentScope(); + obj = + new LambdaFunction( + scope, + 0, + new Callable() { + @Override + public Object call( + Context cx, + Scriptable scope, + Scriptable thisObj, + Object[] args) { + if (thisObj != null + && args.length == 1 + && args[0] instanceof Scriptable) { + Scriptable obj = (Scriptable) args[0]; + Object protoProp = null; + if (thisObj instanceof BoundFunction) + protoProp = + ((NativeFunction) + ((BoundFunction) thisObj) + .getTargetFunction()) + .getPrototypeProperty(); + else + protoProp = + ScriptableObject.getProperty( + thisObj, "prototype"); + if (protoProp instanceof IdScriptableObject) { + return ScriptRuntime.jsDelegatesTo( + obj, (Scriptable) protoProp); + } + throw ScriptRuntime.typeErrorById( + "msg.instanceof.bad.prototype", getFunctionName()); + } + return false; // NOT_FOUND, null etc. + } + }); + } + return obj; + } + @Override public Object execIdCall( IdFunctionObject f, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { @@ -627,6 +683,12 @@ private Object jsConstructor(Context cx, Scriptable scope, Object[] args) { return cx.compileFunction(global, source, evaluator, reporter, sourceURI, 1, null); } + @Override + protected int findPrototypeId(Symbol k) { + if (SymbolKey.HAS_INSTANCE.equals(k)) return SymbolId_hasInstance; + return 0; + } + @Override protected int findPrototypeId(String s) { int id; @@ -670,7 +732,8 @@ public Scriptable getHomeObject() { Id_apply = 4, Id_call = 5, Id_bind = 6, - MAX_PROTOTYPE_ID = Id_bind; + SymbolId_hasInstance = 7, + MAX_PROTOTYPE_ID = SymbolId_hasInstance; private Object prototypeProperty; private Object argumentsObj = NOT_FOUND; diff --git a/rhino/src/main/java/org/mozilla/javascript/IdScriptableObject.java b/rhino/src/main/java/org/mozilla/javascript/IdScriptableObject.java index 33e582de30..c3a531e110 100644 --- a/rhino/src/main/java/org/mozilla/javascript/IdScriptableObject.java +++ b/rhino/src/main/java/org/mozilla/javascript/IdScriptableObject.java @@ -201,7 +201,13 @@ final void delete(int id) { Context cx = Context.getContext(); if (cx.isStrictMode()) { int nameSlot = (id - 1) * SLOT_SPAN + NAME_SLOT; - String name = (String) valueArray[nameSlot]; + + String name = null; + if (valueArray[nameSlot] instanceof String) + name = (String) valueArray[nameSlot]; + else if (valueArray[nameSlot] instanceof Symbol) { + name = valueArray[nameSlot].toString(); + } throw ScriptRuntime.typeErrorById( "msg.delete.property.with.configurable.false", name); } diff --git a/rhino/src/main/java/org/mozilla/javascript/Node.java b/rhino/src/main/java/org/mozilla/javascript/Node.java index 0b3f7ca4fc..e52e43dfbf 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Node.java +++ b/rhino/src/main/java/org/mozilla/javascript/Node.java @@ -1181,7 +1181,7 @@ private void toString(Map printIds, StringBuilder sb) { Object[] a = (Object[]) x.objectValue; sb.append("["); for (int i = 0; i < a.length; i++) { - sb.append(a[i].toString()); + if (a[i] != null) sb.append(a[i].toString()); if (i + 1 < a.length) sb.append(", "); } sb.append("]"); diff --git a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntimeES6.java b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntimeES6.java index 204e2422b0..ec3136c7e8 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntimeES6.java +++ b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntimeES6.java @@ -46,4 +46,15 @@ public static void addSymbolUnscopables( ScriptableObject.putProperty(unScopablesDescriptor, "writable", false); constructor.defineOwnProperty(cx, SymbolKey.UNSCOPABLES, unScopablesDescriptor, false); } + + /** Registers the symbol [Symbol.hasInstance] on the given constructor function. */ + public static void addSymbolHasInstance( + Context cx, Scriptable scope, IdScriptableObject constructor) { + ScriptableObject hasInstanceDescriptor = (ScriptableObject) cx.newObject(scope); + ScriptableObject.putProperty(hasInstanceDescriptor, "value", ScriptableObject.EMPTY); + ScriptableObject.putProperty(hasInstanceDescriptor, "enumerable", false); + ScriptableObject.putProperty(hasInstanceDescriptor, "configurable", false); + ScriptableObject.putProperty(hasInstanceDescriptor, "writable", false); + constructor.defineOwnProperty(cx, SymbolKey.HAS_INSTANCE, hasInstanceDescriptor, false); + } } diff --git a/rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java b/rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java index f8e1f80fa2..293561e32e 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java +++ b/rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java @@ -831,6 +831,12 @@ public boolean hasInstance(Scriptable instance) { // chasing. This will be overridden in NativeFunction and non-JS // objects. + Context cx = Context.getCurrentContext(); + Object hasInstance = ScriptRuntime.getObjectElem(this, SymbolKey.HAS_INSTANCE, cx); + if (hasInstance instanceof Callable) { + return (boolean) + ((Callable) hasInstance).call(cx, getParentScope(), this, new Object[] {this}); + } return ScriptRuntime.jsDelegatesTo(instance, this); } diff --git a/rhino/src/test/java/org/mozilla/javascript/FunctionPrototypeSymbolHasInstanceTest.java b/rhino/src/test/java/org/mozilla/javascript/FunctionPrototypeSymbolHasInstanceTest.java new file mode 100644 index 0000000000..a3e2e980e2 --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/FunctionPrototypeSymbolHasInstanceTest.java @@ -0,0 +1,172 @@ +package org.mozilla.javascript; + +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Test; +import org.mozilla.javascript.tests.Utils; + +public class FunctionPrototypeSymbolHasInstanceTest { + @Test + public void testSymbolHasInstanceIsPresent() { + String script = + "" + + "var f = {\n" + + " [Symbol.hasInstance](value) { " + + " }" + + "};\n" + + "var g = {};\n" + + "`${f.hasOwnProperty(Symbol.hasInstance)}:${g.hasOwnProperty(Symbol.hasInstance)}`"; + Utils.assertWithAllOptimizationLevelsES6("true:false", script); + } + + @Test + public void testSymbolHasInstanceCanBeCalledLikeAnotherMethod() { + String script = + "" + + "var f = {\n" + + " [Symbol.hasInstance](value) { " + + " return 42;" + + " }" + + "};\n" + + "f[Symbol.hasInstance]() == 42"; + Utils.assertWithAllOptimizationLevelsES6(true, script); + } + + // See: https://tc39.es/ecma262/#sec-function.prototype-%symbol.hasinstance% + @Test + public void testFunctionPrototypeSymbolHasInstanceHasAttributes() { + String script = + "var a = Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance);\n" + + "a.writable + ':' + a.configurable + ':' + a.enumerable"; + Utils.assertWithAllOptimizationLevelsES6("false:false:false", script); + } + + // See: https://tc39.es/ecma262/#sec-function.prototype-%symbol.hasinstance% + @Test + public void testFunctionPrototypeSymbolHasInstanceHasAttributesStrictMode() { + String script = + "'use strict';\n" + + "var t = typeof Function.prototype[Symbol.hasInstance];\n" + + "var a = Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance);\n" + + "var typeErrorThrown = false;\n" + + "try { \n" + + " delete Function.prototype[Symbol.hasInstance] \n" + + "} catch (e) { \n" + + " typeErrorThrown = true \n" + + "}\n" + + "Object.prototype.hasOwnProperty.call(Function.prototype, Symbol.hasInstance) + ':' + typeErrorThrown + ':' + t + ':' + a.writable + ':' + a.configurable + ':' + a.enumerable; \n"; + Utils.assertWithAllOptimizationLevelsES6("true:true:function:false:false:false", script); + } + + @Test + @Ignore("name-length-params-prototype-set-incorrectly") + public void testFunctionPrototypeSymbolHasInstanceHasProperties() { + String script = + "var a = Object.getOwnPropertyDescriptor(Function.prototype[Symbol.hasInstance], 'length');\n" + + "a.value + ':' + a.writable + ':' + a.configurable + ':' + a.enumerable"; + + String script2 = + "var a = Object.getOwnPropertyDescriptor(Function.prototype[Symbol.hasInstance], 'name');\n" + + "a.value + ':' + a.writable + ':' + a.configurable + ':' + a.enumerable"; + Utils.assertWithAllOptimizationLevelsES6("1:false:true:false", script); + Utils.assertWithAllOptimizationLevelsES6( + "Symbol(Symbol.hasInstance):false:true:false", script2); + } + + @Test + public void testFunctionPrototypeSymbolHasInstance() { + String script = + "(Function.prototype[Symbol.hasInstance] instanceof Function) + ':' + " + + "Function.prototype[Symbol.hasInstance].call(Function, Object)\n"; + Utils.assertWithAllOptimizationLevelsES6("true:true", script); + } + + @Test + public void testFunctionPrototypeSymbolHasInstanceOnObjectReturnsTrue() { + String script = + "var f = function() {};\n" + + "var o = new f();\n" + + "var o2 = Object.create(o);\n" + + "(f[Symbol.hasInstance](o)) + ':' + " + + "(f[Symbol.hasInstance](o2));\n"; + Utils.assertWithAllOptimizationLevelsES6("true:true", script); + } + + @Test + public void testFunctionPrototypeSymbolHasInstanceOnBoundTargetReturnsTrue() { + String script = + "var BC = function() {};\n" + + "var bc = new BC();\n" + + "var bound = BC.bind();\n" + + "bound[Symbol.hasInstance](bc);\n"; + Utils.assertWithAllOptimizationLevelsES6(true, script); + } + + @Test + public void testFunctionInstanceNullVoidEtc() { + String script = + "var f = function() {};\n" + + "var x;\n" + + "a = (undefined instanceof f) + ':' +\n" + + "(x instanceof f) + ':' +\n" + + "(null instanceof f) + ':' +\n" + + "(void 0 instanceof f)\n" + + "a"; + Utils.assertWithAllOptimizationLevelsES6("false:false:false:false", script); + } + + @Test + public void testFunctionPrototypeSymbolHasInstanceReturnsFalseOnUndefinedOrProtoypeNotFound() { + String script = + "Function.prototype[Symbol.hasInstance].call() + ':' +" + + "Function.prototype[Symbol.hasInstance].call({});"; + Utils.assertWithAllOptimizationLevelsES6("false:false", script); + } + + @Test + public void testSymbolHasInstanceIsInvokedInInstanceOf() { + String script = + "" + + "var globalSet = 0;" + + "var f = {\n" + + " [Symbol.hasInstance](value) { " + + " globalSet = 1;" + + " return true;" + + " }" + + "}\n" + + "var g = {}\n" + + "Object.setPrototypeOf(g, f);\n" + + "g instanceof f;" + + "globalSet == 1"; + Utils.assertWithAllOptimizationLevelsES6(true, script); + } + + @Test + public void testThrowTypeErrorOnNonObjectIncludingSymbol() { + String script = + "" + + "var f = function() {}; \n" + + "f.prototype = Symbol(); \n" + + "f[Symbol.hasInstance]({})"; + + Utils.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + var error = + Assert.assertThrows( + EcmaError.class, + () -> + cx.evaluateString( + scope, + script, + "testSymbolHasInstance", + 0, + null)); + Assert.assertTrue( + error.toString() + .contains("'prototype' property of is not an object.")); + return null; + }); + } +} diff --git a/tests/testsrc/test262.properties b/tests/testsrc/test262.properties index e340fae792..ba2dde05ed 100644 --- a/tests/testsrc/test262.properties +++ b/tests/testsrc/test262.properties @@ -721,14 +721,7 @@ built-ins/Function 184/508 (36.22%) prototype/call/S15.3.4.4_A6_T7.js non-interpreted prototype/Symbol.hasInstance/length.js prototype/Symbol.hasInstance/name.js - prototype/Symbol.hasInstance/prop-desc.js - prototype/Symbol.hasInstance/this-val-bound-target.js - prototype/Symbol.hasInstance/this-val-not-callable.js - prototype/Symbol.hasInstance/this-val-poisoned-prototype.js - prototype/Symbol.hasInstance/value-get-prototype-of-err.js - prototype/Symbol.hasInstance/value-negative.js - prototype/Symbol.hasInstance/value-non-obj.js - prototype/Symbol.hasInstance/value-positive.js + prototype/Symbol.hasInstance/value-get-prototype-of-err.js {unsupported: [Proxy]} prototype/toString/async-arrow-function.js {unsupported: [async-functions]} prototype/toString/async-function-declaration.js {unsupported: [async-functions]} prototype/toString/async-function-expression.js {unsupported: [async-functions]}