diff --git a/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java b/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java index 13794f945b..4adf30d467 100644 --- a/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java +++ b/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java @@ -31,7 +31,8 @@ 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); + ScriptRuntimeES6.addSymbolHasInstance(cx, scope, constructor); } /** @@ -80,7 +81,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 +157,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 +268,11 @@ protected void fillConstructorProperties(IdFunctionObject ctor) { @Override protected void initPrototypeId(int id) { + if (id == SymbolId_hasInstance) { + initPrototypeValue(id, SymbolKey.HAS_INSTANCE, makeHasInstance(), 0x0F); + return; + } + String s; int arity; switch (id) { @@ -313,6 +321,53 @@ 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()); + } else { + return false; // NOT_FOUND, null etc. + } + } + }); + } + return obj; + } + @Override public Object execIdCall( IdFunctionObject f, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { @@ -509,6 +564,26 @@ protected boolean hasPrototypeProperty() { return prototypeProperty != null || this instanceof NativeFunction; } + @Override + ScriptableObject buildDataDescriptorHelper( + int instanceIdInfo, Scriptable scope, Object value, int attr) { + if (instanceIdInfo == SymbolId_hasInstance) { + return buildDataDescriptor(scope, value, attr, SymbolKey.HAS_INSTANCE.toString(), 1); + } else { + return super.buildDataDescriptorHelper(instanceIdInfo, scope, value, attr); + } + } + + @Override + ScriptableObject buildDataDescriptorHelper( + Symbol key, Scriptable scope, Object value, int attr) { + if (key == SymbolKey.HAS_INSTANCE) { + return buildDataDescriptor(scope, value, attr, key.toString(), 1); + } else { + return super.buildDataDescriptorHelper(key, scope, value, attr); + } + } + public Object getPrototypeProperty() { Object result = prototypeProperty; if (result == null) { @@ -627,6 +702,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; + else return 0; + } + @Override protected int findPrototypeId(String s) { int id; @@ -662,7 +743,8 @@ protected int findPrototypeId(String s) { 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 b21484dda3..51b3a0bfc4 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 SymbolKey) { + name = valueArray[nameSlot].toString(); + } throw ScriptRuntime.typeErrorById( "msg.delete.property.with.configurable.false", name); } @@ -936,6 +942,21 @@ protected ScriptableObject getOwnPropertyDescriptor(Context cx, Object id) { return desc; } + /** + * Overridden in the base class for different descriptors + * + * @return ScriptableObject + */ + ScriptableObject buildDataDescriptorHelper( + Symbol key, Scriptable scope, Object value, int attr) { + return buildDataDescriptor(scope, value, attr); + } + + ScriptableObject buildDataDescriptorHelper( + int instanceIdInfo, Scriptable scope, Object value, int attr) { + return buildDataDescriptor(scope, value, attr); + } + private ScriptableObject getBuiltInDescriptor(String name) { Object value = null; int attr = EMPTY; @@ -950,14 +971,14 @@ private ScriptableObject getBuiltInDescriptor(String name) { int id = (info & 0xFFFF); value = getInstanceIdValue(id); attr = (info >>> 16); - return buildDataDescriptor(scope, value, attr); + return buildDataDescriptorHelper(info, scope, value, attr); } if (prototypeValues != null) { int id = prototypeValues.findId(name); if (id != 0) { value = prototypeValues.get(id); attr = prototypeValues.getAttributes(id); - return buildDataDescriptor(scope, value, attr); + return buildDataDescriptorHelper(info, scope, value, attr); } } return null; @@ -977,7 +998,7 @@ private ScriptableObject getBuiltInDescriptor(Symbol key) { if (id != 0) { value = prototypeValues.get(id); attr = prototypeValues.getAttributes(id); - return buildDataDescriptor(scope, value, attr); + return buildDataDescriptorHelper(key, scope, value, attr); } } return null; diff --git a/rhino/src/main/java/org/mozilla/javascript/Node.java b/rhino/src/main/java/org/mozilla/javascript/Node.java index 928426fef5..1d28f1b21e 100644 --- a/rhino/src/main/java/org/mozilla/javascript/Node.java +++ b/rhino/src/main/java/org/mozilla/javascript/Node.java @@ -1178,7 +1178,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 8fc9cd5eca..48998047af 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java +++ b/rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java @@ -135,14 +135,28 @@ public abstract class ScriptableObject protected static ScriptableObject buildDataDescriptor( Scriptable scope, Object value, int attributes) { + return buildDataDescriptor(scope, value, attributes, null, -1); + } + + protected static ScriptableObject buildDataDescriptor( + Scriptable scope, Object value, int attributes, String name, int length) { ScriptableObject desc = new NativeObject(); ScriptRuntime.setBuiltinProtoAndParent(desc, scope, TopLevel.Builtins.Object); desc.defineProperty("value", value, EMPTY); - desc.setCommonDescriptorProperties(attributes, true); + desc.setCommonDescriptorProperties(attributes, true, name, length); return desc; } - protected void setCommonDescriptorProperties(int attributes, boolean defineWritable) { + protected void setCommonDescriptorProperties( + int attributes, boolean defineWritable, String name, int length) { + if (name != null) { + defineProperty("name", "[Symbol.hasInstance]", attributes); + } + + if (length != -1) { + defineProperty("length", length, attributes); + } + if (defineWritable) { defineProperty("writable", (attributes & READONLY) == 0, EMPTY); } @@ -150,6 +164,10 @@ protected void setCommonDescriptorProperties(int attributes, boolean defineWrita defineProperty("configurable", (attributes & PERMANENT) == 0, EMPTY); } + protected void setCommonDescriptorProperties(int attributes, boolean defineWritable) { + setCommonDescriptorProperties(attributes, defineWritable, null, -1); + } + static void checkValidAttributes(int attributes) { final int mask = READONLY | DONTENUM | PERMANENT | UNINITIALIZED_CONST; if ((attributes & ~mask) != 0) { @@ -831,6 +849,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..56e1644a4f --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/FunctionPrototypeSymbolHasInstanceTest.java @@ -0,0 +1,263 @@ +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.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + String result = + (String) + cx.evaluateString( + scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals("true:false", result); + return null; + }); + } + + @Test + public void testSymbolHasInstanceCanBeCalledLikeAnotherMethod() { + String script = + "" + + "var f = {\n" + + " [Symbol.hasInstance](value) { " + + " return 42;" + + " }" + + "};\n" + + "f[Symbol.hasInstance]() == 42"; + Utils.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals(true, result); + return null; + }); + } + + // 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.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals("false:false:false", result); + return null; + }); + } + + // 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.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals("true:true:function:false:false:false", result); + return null; + }); + } + + @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.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Object result2 = + cx.evaluateString(scope, script2, "testSymbolHasInstance", 0, null); + Assert.assertEquals("1:false:true:false", result); + Assert.assertEquals("Symbol(Symbol.hasInstance):false:true:false", result2); + return null; + }); + } + + @Test + public void testFunctionPrototypeSymbolHasInstance() { + String script = + "(Function.prototype[Symbol.hasInstance] instanceof Function) + ':' + " + + "Function.prototype[Symbol.hasInstance].call(Function, Object)\n"; + Utils.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals("true:true", result); + return null; + }); + } + + @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.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals("true:true", result); + return null; + }); + } + + @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.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals(true, result); + return null; + }); + } + + @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.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals("false:false:false:false", result); + return null; + }); + } + + @Test + public void testFunctionPrototypeSymbolHasInstanceReturnsFalseOnUndefinedOrProtoypeNotFound() { + String script = + "Function.prototype[Symbol.hasInstance].call() + ':' +" + + "Function.prototype[Symbol.hasInstance].call({});"; + Utils.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals("false:false", result); + return null; + }); + } + + @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.runWithAllOptimizationLevels( + (cx) -> { + cx.setLanguageVersion(Context.VERSION_ES6); + final Scriptable scope = cx.initStandardObjects(); + Object result = + cx.evaluateString(scope, script, "testSymbolHasInstance", 0, null); + Assert.assertEquals(true, result); + return null; + }); + } + + @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 a5b400211e..acf95d07ad 100644 --- a/tests/testsrc/test262.properties +++ b/tests/testsrc/test262.properties @@ -708,7 +708,7 @@ built-ins/Error 6/41 (14.63%) ~built-ins/FinalizationRegistry -built-ins/Function 186/508 (36.61%) +built-ins/Function 179/508 (35.24%) internals/Call 2/2 (100.0%) internals/Construct 6/6 (100.0%) length/S15.3.5.1_A1_T3.js strict @@ -769,14 +769,7 @@ built-ins/Function 186/508 (36.61%) 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 {unsupported: [Proxy]} - prototype/Symbol.hasInstance/value-negative.js - prototype/Symbol.hasInstance/value-non-obj.js - prototype/Symbol.hasInstance/value-positive.js 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]}