From 44f1c0906368d5593af4be42903c950a9acb762a Mon Sep 17 00:00:00 2001 From: Satish Srinivasan <0xe@users.noreply.github.com> Date: Wed, 15 Jan 2025 07:34:54 +0530 Subject: [PATCH] FEAT: Implement Symbol.hasInstance for Function.prototype Adds support for Function.prototype[Symbol.hasInstance]. --- .../org/mozilla/javascript/BaseFunction.java | 41 ++++- .../javascript/IdScriptableObject.java | 16 +- .../java/org/mozilla/javascript/Node.java | 2 +- .../mozilla/javascript/ScriptableObject.java | 6 + ...unctionPrototypeSymbolHasInstanceTest.java | 152 ++++++++++++++++++ tests/testsrc/test262.properties | 11 +- 6 files changed, 214 insertions(+), 14 deletions(-) create mode 100644 rhino/src/test/java/org/mozilla/javascript/FunctionPrototypeSymbolHasInstanceTest.java diff --git a/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java b/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java index 72c025d872..c35d9281c1 100644 --- a/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java +++ b/rhino/src/main/java/org/mozilla/javascript/BaseFunction.java @@ -80,7 +80,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 +156,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 +267,17 @@ protected void fillConstructorProperties(IdFunctionObject ctor) { @Override protected void initPrototypeId(int id) { + if (id == SymbolId_hasInstance) { + initPrototypeMethod( + FUNCTION_TAG, + id, + SymbolKey.HAS_INSTANCE, + String.valueOf(SymbolKey.HAS_INSTANCE), + 1, + CONST | DONTENUM); + return; + } + String s; int arity; switch (id) { @@ -368,6 +381,23 @@ public Object execIdCall( boundArgs = ScriptRuntime.emptyArgs; } return new BoundFunction(cx, scope, targetFunction, boundThis, boundArgs); + + case SymbolId_hasInstance: + 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. } throw new IllegalArgumentException(String.valueOf(id)); } @@ -632,6 +662,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; @@ -675,7 +711,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..a42b5cc131 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); } @@ -764,6 +770,14 @@ public final IdFunctionObject initPrototypeMethod( return function; } + public final IdFunctionObject initPrototypeMethod( + Object tag, int id, Symbol key, String functionName, int arity, int attributes) { + Scriptable scope = ScriptableObject.getTopLevelScope(this); + IdFunctionObject function = newIdFunction(tag, id, functionName, arity, scope); + prototypeValues.initValue(id, key, function, attributes); + return function; + } + public final void initPrototypeConstructor(IdFunctionObject f) { int id = prototypeValues.constructorId; if (id == 0) throw new IllegalStateException(); 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/ScriptableObject.java b/rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java index 1dacfbf683..8d953ea28f 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java +++ b/rhino/src/main/java/org/mozilla/javascript/ScriptableObject.java @@ -864,6 +864,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 ScriptRuntime.toBoolean( + ((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..a4414ec4b0 --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/FunctionPrototypeSymbolHasInstanceTest.java @@ -0,0 +1,152 @@ +package org.mozilla.javascript; + +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.assertWithAllModes("true:false", script); + } + + @Test + public void testSymbolHasInstanceCanBeCalledLikeAnotherMethod() { + String script = + "" + + "var f = {\n" + + " [Symbol.hasInstance](value) { " + + " return 42;" + + " }" + + "};\n" + + "f[Symbol.hasInstance]() == 42"; + Utils.assertWithAllModes(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.assertWithAllModes("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.assertWithAllModes("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.assertWithAllModes("1:false:true:false", script); + Utils.assertWithAllModes("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.assertWithAllModes("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.assertWithAllModes("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.assertWithAllModes(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.assertWithAllModes("false:false:false:false", script); + } + + @Test + public void testFunctionPrototypeSymbolHasInstanceReturnsFalseOnUndefinedOrProtoypeNotFound() { + String script = + "Function.prototype[Symbol.hasInstance].call() + ':' +" + + "Function.prototype[Symbol.hasInstance].call({});"; + Utils.assertWithAllModes("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.assertWithAllModes(true, script); + } + + @Test + public void testThrowTypeErrorOnNonObjectIncludingSymbol() { + String script = + "" + + "var f = function() {}; \n" + + "f.prototype = Symbol(); \n" + + "f[Symbol.hasInstance]({})"; + Utils.assertEcmaErrorES6( + "TypeError: 'prototype' property of is not an object. (test#3)", script); + } +} diff --git a/tests/testsrc/test262.properties b/tests/testsrc/test262.properties index 09e4fa0d85..bed9cb1490 100644 --- a/tests/testsrc/test262.properties +++ b/tests/testsrc/test262.properties @@ -660,7 +660,7 @@ built-ins/Error 5/41 (12.2%) ~built-ins/FinalizationRegistry -built-ins/Function 184/508 (36.22%) +built-ins/Function 175/508 (34.45%) internals/Call 2/2 (100.0%) internals/Construct 6/6 (100.0%) length/S15.3.5.1_A1_T3.js strict @@ -719,16 +719,7 @@ built-ins/Function 184/508 (36.22%) prototype/call/S15.3.4.4_A6_T2.js compiled prototype/call/S15.3.4.4_A6_T5.js compiled prototype/call/S15.3.4.4_A6_T7.js compiled - 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/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]}