Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: Implement Symbol.hasInstance for Function.prototype #1751

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions rhino/src/main/java/org/mozilla/javascript/BaseFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>true
* </code>
*/
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -627,6 +657,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;
Expand Down Expand Up @@ -670,7 +706,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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion rhino/src/main/java/org/mozilla/javascript/Node.java
Original file line number Diff line number Diff line change
Expand Up @@ -1181,7 +1181,7 @@ private void toString(Map<Node, Integer> 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("]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
17 changes: 4 additions & 13 deletions tests/testsrc/test262.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]}
Expand Down Expand Up @@ -957,7 +948,7 @@ built-ins/Number 23/335 (6.87%)
S9.3.1_A3_T1_U180E.js {unsupported: [u180e]}
S9.3.1_A3_T2_U180E.js {unsupported: [u180e]}

built-ins/Object 178/3408 (5.22%)
built-ins/Object 177/3408 (5.19%)
assign/assignment-to-readonly-property-of-target-must-throw-a-typeerror-exception.js
assign/not-a-constructor.js
assign/source-own-prop-error.js
Expand Down Expand Up @@ -1530,7 +1521,7 @@ built-ins/Promise 392/631 (62.12%)
resolve-thenable-deferred.js {unsupported: [async]}
resolve-thenable-immed.js {unsupported: [async]}

built-ins/Proxy 76/311 (24.44%)
built-ins/Proxy 73/311 (23.47%)
construct/arguments-realm.js
construct/call-parameters.js
construct/call-parameters-new-target.js
Expand Down Expand Up @@ -3146,7 +3137,7 @@ built-ins/undefined 0/8 (0.0%)

~intl402

language/arguments-object 185/263 (70.34%)
language/arguments-object 184/263 (69.96%)
mapped/mapped-arguments-nonconfigurable-3.js non-strict
mapped/mapped-arguments-nonconfigurable-delete-1.js non-strict
mapped/mapped-arguments-nonconfigurable-delete-2.js non-strict
Expand Down
Loading