From cd310ef19fdcc5e480c623f4240231dadb04a20f Mon Sep 17 00:00:00 2001 From: Gregory Brail Date: Wed, 14 Apr 2021 11:00:23 -0700 Subject: [PATCH 1/3] Add support for lambda-based native functions This introduces LambdaFunction and LambdaConstructor, which can be used to represent Java lambda functions as native JavaScript functions, and also can be used to construct an entire class out of lambdas. This pattern is a more direct representation of the JavaScript object model to Java than the existing reflection mechanism, and it is usable with far less code than the IdScriptableObject class. It also outperforms both in microbenchmarks. --- .../benchmarks/BuiltinBenchmark.java | 378 ++++++++++++++++++ build.gradle | 4 +- src/org/mozilla/javascript/BaseFunction.java | 6 +- src/org/mozilla/javascript/Constructable.java | 21 + src/org/mozilla/javascript/Function.java | 9 +- .../mozilla/javascript/LambdaConstructor.java | 137 +++++++ .../mozilla/javascript/LambdaFunction.java | 76 ++++ .../javascript/resources/Messages.properties | 5 + .../javascript/tests/LambdaFunctionTest.java | 222 ++++++++++ 9 files changed, 851 insertions(+), 7 deletions(-) create mode 100644 benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java create mode 100644 src/org/mozilla/javascript/Constructable.java create mode 100644 src/org/mozilla/javascript/LambdaConstructor.java create mode 100644 src/org/mozilla/javascript/LambdaFunction.java create mode 100644 testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java diff --git a/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java b/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java new file mode 100644 index 0000000000..1e0e151332 --- /dev/null +++ b/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java @@ -0,0 +1,378 @@ +package org.mozilla.javascript.benchmarks; + +import java.lang.reflect.InvocationTargetException; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.IdFunctionObject; +import org.mozilla.javascript.IdScriptableObject; +import org.mozilla.javascript.LambdaConstructor; +import org.mozilla.javascript.Script; +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Undefined; +import org.mozilla.javascript.annotations.*; +import org.openjdk.jmh.annotations.*; + +public class BuiltinBenchmark { + + @State(Scope.Thread) + public static class AbstractClassState { + + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + cx = Context.enter(); + cx.setOptimizationLevel(9); + cx.setLanguageVersion(Context.VERSION_ES6); + + scope = cx.initStandardObjects(); + ScriptableObject.defineClass(scope, AnnotatedClass.class); + IdClass.init(scope); + DumbLambdaClass.init(scope); + } + + void compileScript(String testClassName) { + testScript = cx.compileString( + "o = new " + testClassName + "();\n" + + "o.setValue(99);\n" + + "for (var i = 0; i < 1000; i++) {\n" + + "if (o.getValue() !== 99) { throw 'Not working!'; }\n" + + "}\n" + + "o.getValue();", + "test.js", 1, null); + } + + @TearDown(Level.Trial) + public void close() { + Context.exit(); + } + + Context cx; + Scriptable scope; + Script testScript; + } + + @State(Scope.Thread) + public static class AnnotatedClassState extends AbstractClassState { + + @Setup(Level.Trial) + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + super.init(); + compileScript("AnnotatedClass"); + } + } + + @Benchmark + public Object annotatedClassMethods(AnnotatedClassState state) { + return state.testScript.exec(state.cx, state.scope); + } + + public static class AnnotatedClass + extends ScriptableObject { + + @Override + public String getClassName() { + return "AnnotatedClass"; + } + + @JSFunction + public void one() { + } + + @JSFunction + public void two() { + } + + @JSFunction + public void three() { + } + + @JSFunction + public void four() { + } + + @JSFunction + public void five() { + } + + @JSFunction + public void six() { + } + + @JSFunction + public void seven() { + } + + @JSFunction + public void eight() { + } + + @JSFunction + public void nine() { + } + + @JSFunction + public void setValue(int value) { + this.value = value; + } + + @JSFunction + public int getValue() { + return value; + } + + private int value; + } + + @State(Scope.Thread) + public static class IdClassState extends AbstractClassState { + + @Setup(Level.Trial) + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + super.init(); + compileScript("IdClass"); + } + } + + @Benchmark + public Object idClassMethods(IdClassState state) { + return state.testScript.exec(state.cx, state.scope); + } + + public static class IdClass + extends IdScriptableObject { + + private static final String TAG = "IdClass"; + + public static void init(Scriptable scope) { + IdClass idc = new IdClass(); + idc.exportAsJSClass(MAX_ID, scope, false); + } + + @Override + public String getClassName() { + return "IdClass"; + } + + @Override + protected void initPrototypeId(int id) { + String s, fnName = null; + int arity; + switch (id) { + case Id_one: + arity = 0; + s = "one"; + break; + case Id_two: + arity = 0; + s = "two"; + break; + case Id_three: + arity = 0; + s = "three"; + break; + case Id_four: + arity = 0; + s = "four"; + break; + case Id_five: + arity = 0; + s = "five"; + break; + case Id_six: + arity = 0; + s = "six"; + break; + case Id_seven: + arity = 0; + s = "seven"; + break; + case Id_eight: + arity = 0; + s = "eight"; + break; + case Id_nine: + arity = 0; + s = "nine"; + break; + case Id_setValue: + arity = 1; + s = "setValue"; + break; + case Id_getValue: + arity = 0; + s = "getValue"; + break; + case Id_constructor: + arity = 0; + s = "constructor"; + break; + default: + throw new IllegalArgumentException(String.valueOf(id)); + } + + initPrototypeMethod(TAG, id, s, fnName, arity); + } + + @Override + public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (!f.hasTag(TAG)) { + return super.execIdCall(f, cx, scope, thisObj, args); + } + int id = f.methodId(); + IdClass self; + switch (id) { + case Id_constructor: + return new IdClass(); + case Id_setValue: + self = (IdClass) thisObj; + if (args.length < 1) { + throw ScriptRuntime.throwError(cx, scope, "not enough args"); + } + self.value = ScriptRuntime.toInt32(args[0]); + break; + case Id_getValue: + self = (IdClass) thisObj; + return self.value; + default: + throw new IllegalArgumentException( + "Array.prototype has no method: " + f.getFunctionName()); + } + return Undefined.instance; + } + + // #string_id_map# + + @Override + protected int findPrototypeId(String s) { + int id; + // #generated# Last update: 2021-04-13 16:17:26 PDT + switch (s) { + case "one": + id = Id_one; + break; + case "two": + id = Id_two; + break; + case "three": + id = Id_three; + break; + case "four": + id = Id_four; + break; + case "five": + id = Id_five; + break; + case "six": + id = Id_six; + break; + case "seven": + id = Id_seven; + break; + case "eight": + id = Id_eight; + break; + case "nine": + id = Id_nine; + break; + case "getValue": + id = Id_getValue; + break; + case "setValue": + id = Id_setValue; + break; + case "constructor": + id = Id_constructor; + break; + default: + id = 0; + break; + } + // #/generated# + return id; + } + + private static final int + Id_one = 1, + Id_two = 2, + Id_three = 3, + Id_four = 4, + Id_five = 5, + Id_six = 6, + Id_seven = 7, + Id_eight = 8, + Id_nine = 9, + Id_getValue = 10, + Id_setValue = 11, + Id_constructor = 12, + MAX_ID = Id_constructor; + + // #/string_id_map# + + private int value; + } + + @State(Scope.Thread) + public static class DumbLambdaState extends AbstractClassState { + + @Setup(Level.Trial) + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + super.init(); + compileScript("DumbLambdaClass"); + } + } + + @Benchmark + public Object dumbLambdaClassMethods(DumbLambdaState state) { + return state.testScript.exec(state.cx, state.scope); + } + + private static class DumbLambdaClass extends ScriptableObject { + + private static Object noop(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return Undefined.instance; + } + + private static Object setValue(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + if (args.length < 1) { + throw ScriptRuntime.throwError(cx, scope, "Not enough args"); + } + DumbLambdaClass self = LambdaConstructor.convertThisObject(thisObj, DumbLambdaClass.class); + self.value = ScriptRuntime.toInt32(args[0]); + return Undefined.instance; + } + + private static Object getValue(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + DumbLambdaClass self = LambdaConstructor.convertThisObject(thisObj, DumbLambdaClass.class); + return self.value; + } + + public static void init(Scriptable scope) { + LambdaConstructor cons = + new LambdaConstructor(scope, "DumbLambdaClass", 0, + (Context cx, Scriptable s, Object[] args) -> new DumbLambdaClass()); + cons.definePrototypeMethod(scope, "one", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "two", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "three", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "four", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "five", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "six", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "seven", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "eight", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "nine", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "setValue", 1, DumbLambdaClass::setValue); + cons.definePrototypeMethod(scope, "getValue", 1, DumbLambdaClass::getValue); + ScriptableObject.putProperty(scope, "DumbLambdaClass", cons); + } + + @Override + public String getClassName() { + return "DumbLambdaClass"; + } + + private int value; + } +} diff --git a/build.gradle b/build.gradle index c2c787401e..d2a3dc52ea 100644 --- a/build.gradle +++ b/build.gradle @@ -108,10 +108,10 @@ task testBenchmark() {} testBenchmark.dependsOn sunSpiderBenchmark testBenchmark.dependsOn v8Benchmark -task microBenchmarks(type: JavaExec, description: 'JMH benchmarks') { +task builtinBenchmarks(type: JavaExec, description: 'JMH benchmarks') { classpath = sourceSets.jmh.runtimeClasspath main = 'org.openjdk.jmh.Main' - args '-f', '1', '-bm', 'avgt', '-tu', 'ns', 'ObjectCall' + args '-f', '1', '-bm', 'avgt', '-tu', 'ns', 'Builtin' } task jmhHelp(type: JavaExec, description: 'JMH benchmarks') { diff --git a/src/org/mozilla/javascript/BaseFunction.java b/src/org/mozilla/javascript/BaseFunction.java index e29a97234d..bc4d834bcc 100644 --- a/src/org/mozilla/javascript/BaseFunction.java +++ b/src/org/mozilla/javascript/BaseFunction.java @@ -484,7 +484,11 @@ protected Object getPrototypeProperty() { return result; } - private synchronized Object setupDefaultPrototype() { + protected void setPrototypeProperty(Object prototype) { + this.prototypeProperty = prototype; + } + + protected synchronized Object setupDefaultPrototype() { if (prototypeProperty != null) { return prototypeProperty; } diff --git a/src/org/mozilla/javascript/Constructable.java b/src/org/mozilla/javascript/Constructable.java new file mode 100644 index 0000000000..a15a10ba78 --- /dev/null +++ b/src/org/mozilla/javascript/Constructable.java @@ -0,0 +1,21 @@ +package org.mozilla.javascript; + +/** + * An interface that can be used to implement a constructor function as a lambda. + */ +public interface Constructable { + + /** + * Call the function as a constructor. + * + * This method is invoked by the runtime in order to satisfy a use of the JavaScript + * new operator. This method is expected to create a new object and return it. + * + * @param cx the current Context for this thread + * @param scope an enclosing scope of the caller except when the function is called from a + * closure. + * @param args the array of arguments + * @return the allocated object + */ + Scriptable construct(Context cx, Scriptable scope, Object[] args); +} diff --git a/src/org/mozilla/javascript/Function.java b/src/org/mozilla/javascript/Function.java index 89c6d4b473..0992317019 100644 --- a/src/org/mozilla/javascript/Function.java +++ b/src/org/mozilla/javascript/Function.java @@ -16,7 +16,7 @@ * @author Norris Boyd */ -public interface Function extends Scriptable, Callable +public interface Function extends Scriptable, Callable, Constructable { /** * Call the function. @@ -33,8 +33,8 @@ public interface Function extends Scriptable, Callable * @return the result of the call */ @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, - Object[] args); + Object call(Context cx, Scriptable scope, Scriptable thisObj, + Object[] args); /** * Call the function as a constructor. @@ -49,5 +49,6 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, * @param args the array of arguments * @return the allocated object */ - public Scriptable construct(Context cx, Scriptable scope, Object[] args); + @Override + Scriptable construct(Context cx, Scriptable scope, Object[] args); } diff --git a/src/org/mozilla/javascript/LambdaConstructor.java b/src/org/mozilla/javascript/LambdaConstructor.java new file mode 100644 index 0000000000..8f5d7bbf47 --- /dev/null +++ b/src/org/mozilla/javascript/LambdaConstructor.java @@ -0,0 +1,137 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript; + +/** + * This class implements a JavaScript function that may be used as a constructor by delegating to an + * interface that can be easily implemented as a lambda. The LambdaFunction class may be used to add + * functions to the prototype that are also implemented as lambdas. + * + * In micro benchmarks (as of 2021) using this class to implement a built-in class is about + * 15% more efficient than using IdScriptableObject, and about 25% faster than using reflection + * via the ScriptableObject.defineClass() family of methods. Furthermore, it results in + * code that more directly maps to JavaScript idioms than either methods, it is much easier + * to implement than IdScriptableObject, and the lambda pattern makes it easier to maintain + * state in various ways that don't always map directly to the existing concepts. + */ +public class LambdaConstructor + extends LambdaFunction { + + private static final long serialVersionUID = 2691205302914111400L; + + /** If this flag is set, the constructor may be invoked as an ordinary function */ + public static final int CONSTRUCTOR_FUNCTION = 1; + /** If this flag is set, the constructor may be invoked using "new" */ + public static final int CONSTRUCTOR_NEW = 1 << 1; + /** By default, the constructor may be invoked either way */ + public static final int CONSTRUCTOR_DEFAULT = CONSTRUCTOR_FUNCTION | CONSTRUCTOR_NEW; + + // Lambdas should not be serialized. + private transient final Constructable targetConstructor; + private final int flags; + + /** + * Create a new function that may be used as a constructor. The new object will have the + * Function prototype and no parent. The caller is responsible for binding this object + * to the appropriate scope. + * + * @param scope scope of the calling context + * @param name name of the function + * @param length the arity of the function + * @param target an object that implements the function in Java. Since Constructable is a + * single-function interface this will typically be implemented as a lambda. + */ + public LambdaConstructor(Scriptable scope, String name, int length, Constructable target) { + super(scope, name, length, null); + this.targetConstructor = target; + this.flags = CONSTRUCTOR_DEFAULT; + } + + /** + * Create a new function and control whether it may be invoked using new, as a function, + * or both. + */ + public LambdaConstructor(Scriptable scope, String name, int length, int flags, + Constructable target) { + super(scope, name, length, null); + this.targetConstructor = target; + this.flags = flags; + } + + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + if ((flags & CONSTRUCTOR_FUNCTION) == 0) { + throw ScriptRuntime.typeErrorById("msg.constructor.no.function", getFunctionName()); + } + return targetConstructor.construct(cx, scope, args); + } + + @Override + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { + if ((flags & CONSTRUCTOR_NEW) == 0) { + throw ScriptRuntime.typeErrorById("msg.no.new", getFunctionName()); + } + Scriptable obj = targetConstructor.construct(cx, scope, args); + obj.setPrototype(getClassPrototype()); + obj.setParentScope(scope); + return obj; + } + + /** + * Define a function property on the prototype of the constructor using a LambdaFunction under the + * covers. + */ + public void definePrototypeMethod(Scriptable scope, String name, int length, Callable target) { + LambdaFunction f = new LambdaFunction(scope, name, length, target); + ScriptableObject proto = getPrototypeScriptable(); + proto.defineProperty(name, f, 0); + } + + /** + * Define a property that may be of any type on the prototype of this constructor. + */ + public void definePrototypeProperty(String name, Object value, int attributes) { + ScriptableObject proto = getPrototypeScriptable(); + proto.defineProperty(name, value, 0); + } + + public void definePrototypeProperty(Symbol key, Object value, int attributes) { + ScriptableObject proto = getPrototypeScriptable(); + proto.defineProperty(key, value, 0); + } + + /** + * Define a function property directly on the constructor that is implemented under the + * covers by a LambdaFunction. + */ + public void defineConstructorMethod(Scriptable scope, String name, int length, Callable target) { + LambdaFunction f = new LambdaFunction(scope, name, length, target); + defineProperty(name, f, DONTENUM); + } + + /** + * A convenience method to convert JavaScript's "this" object into a target class and + * throw a TypeError if it does not match. This is useful for implementing lambda + * functions, as "this" in JavaScript doesn't necessarily map to an instance of the + * class. + */ + @SuppressWarnings("unchecked") + public static T convertThisObject(Scriptable thisObj, Class targetClass) { + if (!targetClass.isInstance(thisObj)) { + throw ScriptRuntime.typeErrorById("msg.this.not.instance"); + } + return (T)thisObj; + } + + private ScriptableObject getPrototypeScriptable() { + Object prop = getPrototypeProperty(); + if (!(prop instanceof ScriptableObject)) { + throw ScriptRuntime.typeError("Not properly a lambda constructor"); + } + return (ScriptableObject)prop; + } +} diff --git a/src/org/mozilla/javascript/LambdaFunction.java b/src/org/mozilla/javascript/LambdaFunction.java new file mode 100644 index 0000000000..c2725d7cd6 --- /dev/null +++ b/src/org/mozilla/javascript/LambdaFunction.java @@ -0,0 +1,76 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript; + +/** + * This class implements a single JavaScript function that has the prototype of the built-in + * Function class, and which is implemented using a single function that can easily be implemented + * using a lambda expression. + */ +public class LambdaFunction + extends BaseFunction { + + private static final long serialVersionUID = -8388132362854748293L; + + // The target is expected to be a lambda -- lambdas should not be serialized. + private transient final Callable target; + private final String name; + private final int length; + + /** + * Create a new function. The new object will have the Function prototype and no parent. The + * caller is responsible for binding this object to the appropriate scope. + * + * @param scope scope of the calling context + * @param name name of the function + * @param length the arity of the function + * @param target an object that implements the function in Java. Since Callable is a + * single-function interface this will typically be implemented as a lambda. + */ + public LambdaFunction(Scriptable scope, String name, int length, Callable target) { + this.target = target; + this.name = name; + this.length = length; + ScriptRuntime.setFunctionProtoAndParent(this, scope); + setupDefaultPrototype(); + } + + /** + * Create a new built-in function, with no name, and no default prototype. + */ + public LambdaFunction(Scriptable scope, int length, Callable target) { + this.target = target; + this.length = length; + this.name = ""; + ScriptRuntime.setFunctionProtoAndParent(this, scope); + } + + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return target.call(cx, scope, thisObj, args); + } + + @Override + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { + throw ScriptRuntime.typeErrorById("msg.no.new", getFunctionName()); + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getArity() { + return length; + } + + @Override + public String getFunctionName() { + return name; + } +} diff --git a/src/org/mozilla/javascript/resources/Messages.properties b/src/org/mozilla/javascript/resources/Messages.properties index 9fe5a2305d..4971b64d4a 100644 --- a/src/org/mozilla/javascript/resources/Messages.properties +++ b/src/org/mozilla/javascript/resources/Messages.properties @@ -911,3 +911,8 @@ msg.no.new =\ msg.map.function.not =\ Map function is not actually a function + +msg.constructor.no.function \= + The constructor for {0} may not be invoked as a function + +msg.this.not.instance \= \"this\" is not an instance of the class \ No newline at end of file diff --git a/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java b/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java new file mode 100644 index 0000000000..b21fe4cca6 --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java @@ -0,0 +1,222 @@ +package org.mozilla.javascript.tests; + +import static org.junit.Assert.assertThrows; + +import java.io.FileReader; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.LambdaConstructor; +import org.mozilla.javascript.LambdaFunction; +import org.mozilla.javascript.RhinoException; +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.SymbolKey; + +public class LambdaFunctionTest { + + private Context cx; + private Scriptable root; + + @Before + public void init() throws IOException { + cx = Context.enter(); + cx.setLanguageVersion(Context.VERSION_ES6); + root = cx.initStandardObjects(); + try (FileReader rdr = new FileReader("testsrc/assert.js")) { + cx.evaluateReader(root, rdr, "assert.js", 1, null); + } + } + + @After + public void cleanup() { + Context.exit(); + } + + @Test + public void testNativeFunction() { + cx.evaluateString(root, "function foo() { return 'Hello'; }\n" + + "assertEquals(foo.name, 'foo');\n" + + "assertEquals(foo.length, 0);\n" + + "assertEquals(typeof foo, 'function');\n" + + "assertEquals(foo(), 'Hello');\n" + + "assertTrue(foo.toString().length > 0);\n" + + "assertTrue(foo.prototype !== undefined);\n" + + "assertTrue(foo.prototype.toString !== undefined);", + "test", 1, null); + } + + @Test + public void testNoArgLambdaFunction() { + LambdaFunction f = new LambdaFunction(root, "foo", 0, + (Context cx, Scriptable scope, Scriptable thisObj, Object[] args) -> { + return "Hello"; + }); + ScriptableObject.putProperty(root, "foo", f); + cx.evaluateString(root, "assertEquals(foo.name, 'foo');\n" + + "assertEquals(foo.length, 0);\n" + + "assertEquals(typeof foo, 'function');\n" + + "assertEquals(foo(), 'Hello');\n" + + "assertTrue(foo.toString().length > 0);\n" + + "assertTrue(foo.prototype.toString !== undefined);", + "test", 1, null); + } + + @Test + public void testConstructLambdaClass() { + TestClass.init(root); + cx.evaluateString(root, "let tc = new TestClass('foo');\n" + + "assertEquals(tc.value, 'foo');\n" + + "tc.value = 'bar';\n" + + "assertEquals(tc.value, 'bar');\n" + + "tc.anotherValue = 123;\n" + + "assertEquals(tc.anotherValue, 123);\n" + + "assertEquals(TestClass.name, 'TestClass');\n" + + "assertEquals(TestClass.length, 1);\n" + + "assertEquals(typeof TestClass, 'function');\n" + + "assertTrue(tc instanceof TestClass);\n", + "test", 1, null); + } + + @Test + public void testNativePrototypeFunctions() { + cx.evaluateString(root, + "function TestClass(v) { this.value = v; }\n" + + "TestClass.prototype.appendToValue = function(x) { return this.value + x; }\n" + + "let tc = new TestClass('foo');\n" + + "assertEquals(tc.value, 'foo');\n" + + "assertEquals(tc.appendToValue('bar'), 'foobar');\n" + + "tc.value = 'x';\n" + + "assertEquals(tc.appendToValue('x'), 'xx');\n" + + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n" + + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');", + "test", 1, null); + } + + @Test + public void testLambdaPrototypeFunctions() { + TestClass.init(root); + cx.evaluateString(root, "let tc = new TestClass('foo');\n" + + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');\n" + + "assertEquals(tc.value, 'foo');\n" + + "assertEquals(tc.appendToValue('bar', 'baz'), 'foobarbaz');\n" + + "tc.value = 'x';\n" + + "assertEquals(tc.appendToValue('x'), 'xx');\n" + + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n", + "test", 1, null); + } + + @Test + public void testLambdaPrototypeFunctionNotFound() { + TestClass.init(root); + assertThrows(RhinoException.class, () -> { + cx.evaluateString(root, "let tc = new TestClass('foo');\n" + + "tc.notFound();", + "test", 1, null); + }); + } + + @Test + public void testLambdaPrototypeFunctionInvalidThis() { + TestClass.init(root); + cx.evaluateString(root, "let tc = new TestClass();\n" + + "assertThrows(function() { tc.appendToValue.call(null, 'invalid'); }, TypeError);\n" + + "assertThrows(function() { tc.appendToValue.call(undefined, 'invalid'); }, TypeError);\n" + + "assertThrows(function() { tc.appendToValue.call({}, 'invalid'); }, TypeError);\n", + "test", 1, null); + } + + @Test + public void testLambdaConstructorFunctions() { + TestClass.init(root); + cx.evaluateString(root, "assertEquals(TestClass.sayHello('World'), 'Hello, World!');\n" + + "assertEquals(TestClass.sayHello.name, 'sayHello');\n" + + "assertEquals(TestClass.sayHello.length, 1);\n" + + "assertEquals(typeof TestClass.sayHello, 'function');", + "test", 1, null); + } + + @Test + public void testLambdaConstructorValues() { + TestClass.init(root); + cx.evaluateString(root, "let tc = new TestClass();\n" + + "assertEquals(tc.protoValue, 123);\n" + + "assertEquals(tc[Symbol.species], 456);\n", + "test", 1, null); + } + + private static class TestClass + extends ScriptableObject { + + private String instanceVal; + + public static void init(Scriptable scope) { + LambdaConstructor constructor = new LambdaConstructor(scope, "TestClass", 1, + (Context cx, Scriptable s, Object[] args) -> { + TestClass tc = new TestClass(); + if (args.length > 0) { + tc.instanceVal = ScriptRuntime.toString(args[0]); + } + return tc; + }); + constructor.defineConstructorMethod(scope, "sayHello", 1, + (Context cx, Scriptable s, Scriptable thisObj, Object[] args) -> TestClass.sayHello(args)); + constructor.definePrototypeMethod(scope, "appendToValue", 1, + (Context cx, Scriptable s, Scriptable thisObj, Object[] args) -> { + TestClass self = LambdaConstructor.convertThisObject(thisObj, TestClass.class); + return self.appendToValue(args); + }); + constructor.definePrototypeProperty("protoValue", 123, 0); + constructor.definePrototypeProperty(SymbolKey.SPECIES, 456, 0); + ScriptableObject.defineProperty(scope, "TestClass", constructor, PERMANENT); + } + + @Override + public String getClassName() { + return "TestClass"; + } + + @Override + public Object get(String name, Scriptable start) { + if ("value".equals(name)) { + return instanceVal; + } + return super.get(name, start); + } + + @Override + public boolean has(String name, Scriptable start) { + if ("value".equals(name)) { + return true; + } + return super.has(name, start); + } + + @Override + public void put(String name, Scriptable start, Object value) { + if ("value".equals(name)) { + instanceVal = ScriptRuntime.toString(value); + } else { + super.put(name, start, value); + } + } + + private Object appendToValue(Object[] args) { + StringBuilder sb = new StringBuilder(instanceVal); + for (Object arg : args) { + sb.append(ScriptRuntime.toString(arg)); + } + return sb.toString(); + } + + private static Object sayHello(Object[] args) { + if (args.length != 1) { + throw ScriptRuntime.typeError("Expected an argument"); + } + return "Hello, " + ScriptRuntime.toString(args[0]) + '!'; + } + } +} From 1c7f79cf26f9d851619dd4b6944ac6ed6a25755a Mon Sep 17 00:00:00 2001 From: Gregory Brail Date: Thu, 15 Apr 2021 11:49:06 -0700 Subject: [PATCH 2/3] Formatting changes. --- .../benchmarks/BuiltinBenchmark.java | 655 +++++++++--------- src/org/mozilla/javascript/BaseFunction.java | 512 +++++++------- src/org/mozilla/javascript/Constructable.java | 30 +- src/org/mozilla/javascript/Function.java | 28 +- .../mozilla/javascript/LambdaConstructor.java | 207 +++--- .../mozilla/javascript/LambdaFunction.java | 103 ++- .../javascript/tests/LambdaFunctionTest.java | 425 +++++++----- 7 files changed, 996 insertions(+), 964 deletions(-) diff --git a/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java b/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java index 1e0e151332..f1e81d25e4 100644 --- a/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java +++ b/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java @@ -15,364 +15,369 @@ public class BuiltinBenchmark { - @State(Scope.Thread) - public static class AbstractClassState { - - public void init() - throws IllegalAccessException, InvocationTargetException, InstantiationException { - cx = Context.enter(); - cx.setOptimizationLevel(9); - cx.setLanguageVersion(Context.VERSION_ES6); - - scope = cx.initStandardObjects(); - ScriptableObject.defineClass(scope, AnnotatedClass.class); - IdClass.init(scope); - DumbLambdaClass.init(scope); + @State(Scope.Thread) + public static class AbstractClassState { + + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + cx = Context.enter(); + cx.setOptimizationLevel(9); + cx.setLanguageVersion(Context.VERSION_ES6); + + scope = cx.initStandardObjects(); + ScriptableObject.defineClass(scope, AnnotatedClass.class); + IdClass.init(scope); + DumbLambdaClass.init(scope); + } + + void compileScript(String testClassName) { + testScript = + cx.compileString( + "o = new " + + testClassName + + "();\n" + + "o.setValue(99);\n" + + "for (var i = 0; i < 1000; i++) {\n" + + "if (o.getValue() !== 99) { throw 'Not working!'; }\n" + + "}\n" + + "o.getValue();", + "test.js", + 1, + null); + } + + @TearDown(Level.Trial) + public void close() { + Context.exit(); + } + + Context cx; + Scriptable scope; + Script testScript; } - void compileScript(String testClassName) { - testScript = cx.compileString( - "o = new " + testClassName + "();\n" + - "o.setValue(99);\n" + - "for (var i = 0; i < 1000; i++) {\n" + - "if (o.getValue() !== 99) { throw 'Not working!'; }\n" + - "}\n" + - "o.getValue();", - "test.js", 1, null); - } - - @TearDown(Level.Trial) - public void close() { - Context.exit(); - } - - Context cx; - Scriptable scope; - Script testScript; - } - - @State(Scope.Thread) - public static class AnnotatedClassState extends AbstractClassState { - - @Setup(Level.Trial) - public void init() - throws IllegalAccessException, InvocationTargetException, InstantiationException { - super.init(); - compileScript("AnnotatedClass"); - } - } - - @Benchmark - public Object annotatedClassMethods(AnnotatedClassState state) { - return state.testScript.exec(state.cx, state.scope); - } - - public static class AnnotatedClass - extends ScriptableObject { - - @Override - public String getClassName() { - return "AnnotatedClass"; - } - - @JSFunction - public void one() { - } - - @JSFunction - public void two() { - } - - @JSFunction - public void three() { - } - - @JSFunction - public void four() { - } + @State(Scope.Thread) + public static class AnnotatedClassState extends AbstractClassState { - @JSFunction - public void five() { + @Setup(Level.Trial) + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + super.init(); + compileScript("AnnotatedClass"); + } } - @JSFunction - public void six() { + @Benchmark + public Object annotatedClassMethods(AnnotatedClassState state) { + return state.testScript.exec(state.cx, state.scope); } - @JSFunction - public void seven() { - } + public static class AnnotatedClass extends ScriptableObject { - @JSFunction - public void eight() { - } + @Override + public String getClassName() { + return "AnnotatedClass"; + } - @JSFunction - public void nine() { - } + @JSFunction + public void one() {} - @JSFunction - public void setValue(int value) { - this.value = value; - } + @JSFunction + public void two() {} - @JSFunction - public int getValue() { - return value; - } + @JSFunction + public void three() {} - private int value; - } + @JSFunction + public void four() {} - @State(Scope.Thread) - public static class IdClassState extends AbstractClassState { + @JSFunction + public void five() {} - @Setup(Level.Trial) - public void init() - throws IllegalAccessException, InvocationTargetException, InstantiationException { - super.init(); - compileScript("IdClass"); - } - } + @JSFunction + public void six() {} - @Benchmark - public Object idClassMethods(IdClassState state) { - return state.testScript.exec(state.cx, state.scope); - } + @JSFunction + public void seven() {} - public static class IdClass - extends IdScriptableObject { + @JSFunction + public void eight() {} - private static final String TAG = "IdClass"; + @JSFunction + public void nine() {} - public static void init(Scriptable scope) { - IdClass idc = new IdClass(); - idc.exportAsJSClass(MAX_ID, scope, false); - } + @JSFunction + public void setValue(int value) { + this.value = value; + } - @Override - public String getClassName() { - return "IdClass"; - } + @JSFunction + public int getValue() { + return value; + } - @Override - protected void initPrototypeId(int id) { - String s, fnName = null; - int arity; - switch (id) { - case Id_one: - arity = 0; - s = "one"; - break; - case Id_two: - arity = 0; - s = "two"; - break; - case Id_three: - arity = 0; - s = "three"; - break; - case Id_four: - arity = 0; - s = "four"; - break; - case Id_five: - arity = 0; - s = "five"; - break; - case Id_six: - arity = 0; - s = "six"; - break; - case Id_seven: - arity = 0; - s = "seven"; - break; - case Id_eight: - arity = 0; - s = "eight"; - break; - case Id_nine: - arity = 0; - s = "nine"; - break; - case Id_setValue: - arity = 1; - s = "setValue"; - break; - case Id_getValue: - arity = 0; - s = "getValue"; - break; - case Id_constructor: - arity = 0; - s = "constructor"; - break; - default: - throw new IllegalArgumentException(String.valueOf(id)); - } - - initPrototypeMethod(TAG, id, s, fnName, arity); + private int value; } - @Override - public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, - Scriptable thisObj, Object[] args) { - if (!f.hasTag(TAG)) { - return super.execIdCall(f, cx, scope, thisObj, args); - } - int id = f.methodId(); - IdClass self; - switch (id) { - case Id_constructor: - return new IdClass(); - case Id_setValue: - self = (IdClass) thisObj; - if (args.length < 1) { - throw ScriptRuntime.throwError(cx, scope, "not enough args"); - } - self.value = ScriptRuntime.toInt32(args[0]); - break; - case Id_getValue: - self = (IdClass) thisObj; - return self.value; - default: - throw new IllegalArgumentException( - "Array.prototype has no method: " + f.getFunctionName()); - } - return Undefined.instance; - } + @State(Scope.Thread) + public static class IdClassState extends AbstractClassState { - // #string_id_map# - - @Override - protected int findPrototypeId(String s) { - int id; - // #generated# Last update: 2021-04-13 16:17:26 PDT - switch (s) { - case "one": - id = Id_one; - break; - case "two": - id = Id_two; - break; - case "three": - id = Id_three; - break; - case "four": - id = Id_four; - break; - case "five": - id = Id_five; - break; - case "six": - id = Id_six; - break; - case "seven": - id = Id_seven; - break; - case "eight": - id = Id_eight; - break; - case "nine": - id = Id_nine; - break; - case "getValue": - id = Id_getValue; - break; - case "setValue": - id = Id_setValue; - break; - case "constructor": - id = Id_constructor; - break; - default: - id = 0; - break; - } - // #/generated# - return id; + @Setup(Level.Trial) + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + super.init(); + compileScript("IdClass"); + } } - private static final int - Id_one = 1, - Id_two = 2, - Id_three = 3, - Id_four = 4, - Id_five = 5, - Id_six = 6, - Id_seven = 7, - Id_eight = 8, - Id_nine = 9, - Id_getValue = 10, - Id_setValue = 11, - Id_constructor = 12, - MAX_ID = Id_constructor; - - // #/string_id_map# - - private int value; - } - - @State(Scope.Thread) - public static class DumbLambdaState extends AbstractClassState { - - @Setup(Level.Trial) - public void init() - throws IllegalAccessException, InvocationTargetException, InstantiationException { - super.init(); - compileScript("DumbLambdaClass"); + @Benchmark + public Object idClassMethods(IdClassState state) { + return state.testScript.exec(state.cx, state.scope); } - } - @Benchmark - public Object dumbLambdaClassMethods(DumbLambdaState state) { - return state.testScript.exec(state.cx, state.scope); - } - - private static class DumbLambdaClass extends ScriptableObject { - - private static Object noop(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - return Undefined.instance; + public static class IdClass extends IdScriptableObject { + + private static final String TAG = "IdClass"; + + public static void init(Scriptable scope) { + IdClass idc = new IdClass(); + idc.exportAsJSClass(MAX_ID, scope, false); + } + + @Override + public String getClassName() { + return "IdClass"; + } + + @Override + protected void initPrototypeId(int id) { + String s, fnName = null; + int arity; + switch (id) { + case Id_one: + arity = 0; + s = "one"; + break; + case Id_two: + arity = 0; + s = "two"; + break; + case Id_three: + arity = 0; + s = "three"; + break; + case Id_four: + arity = 0; + s = "four"; + break; + case Id_five: + arity = 0; + s = "five"; + break; + case Id_six: + arity = 0; + s = "six"; + break; + case Id_seven: + arity = 0; + s = "seven"; + break; + case Id_eight: + arity = 0; + s = "eight"; + break; + case Id_nine: + arity = 0; + s = "nine"; + break; + case Id_setValue: + arity = 1; + s = "setValue"; + break; + case Id_getValue: + arity = 0; + s = "getValue"; + break; + case Id_constructor: + arity = 0; + s = "constructor"; + break; + default: + throw new IllegalArgumentException(String.valueOf(id)); + } + + initPrototypeMethod(TAG, id, s, fnName, arity); + } + + @Override + public Object execIdCall( + IdFunctionObject f, + Context cx, + Scriptable scope, + Scriptable thisObj, + Object[] args) { + if (!f.hasTag(TAG)) { + return super.execIdCall(f, cx, scope, thisObj, args); + } + int id = f.methodId(); + IdClass self; + switch (id) { + case Id_constructor: + return new IdClass(); + case Id_setValue: + self = (IdClass) thisObj; + if (args.length < 1) { + throw ScriptRuntime.throwError(cx, scope, "not enough args"); + } + self.value = ScriptRuntime.toInt32(args[0]); + break; + case Id_getValue: + self = (IdClass) thisObj; + return self.value; + default: + throw new IllegalArgumentException( + "Array.prototype has no method: " + f.getFunctionName()); + } + return Undefined.instance; + } + + // #string_id_map# + + @Override + protected int findPrototypeId(String s) { + int id; + // #generated# Last update: 2021-04-13 16:17:26 PDT + switch (s) { + case "one": + id = Id_one; + break; + case "two": + id = Id_two; + break; + case "three": + id = Id_three; + break; + case "four": + id = Id_four; + break; + case "five": + id = Id_five; + break; + case "six": + id = Id_six; + break; + case "seven": + id = Id_seven; + break; + case "eight": + id = Id_eight; + break; + case "nine": + id = Id_nine; + break; + case "getValue": + id = Id_getValue; + break; + case "setValue": + id = Id_setValue; + break; + case "constructor": + id = Id_constructor; + break; + default: + id = 0; + break; + } + // #/generated# + return id; + } + + private static final int Id_one = 1, + Id_two = 2, + Id_three = 3, + Id_four = 4, + Id_five = 5, + Id_six = 6, + Id_seven = 7, + Id_eight = 8, + Id_nine = 9, + Id_getValue = 10, + Id_setValue = 11, + Id_constructor = 12, + MAX_ID = Id_constructor; + + // #/string_id_map# + + private int value; } - private static Object setValue(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - if (args.length < 1) { - throw ScriptRuntime.throwError(cx, scope, "Not enough args"); - } - DumbLambdaClass self = LambdaConstructor.convertThisObject(thisObj, DumbLambdaClass.class); - self.value = ScriptRuntime.toInt32(args[0]); - return Undefined.instance; - } + @State(Scope.Thread) + public static class DumbLambdaState extends AbstractClassState { - private static Object getValue(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - DumbLambdaClass self = LambdaConstructor.convertThisObject(thisObj, DumbLambdaClass.class); - return self.value; + @Setup(Level.Trial) + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + super.init(); + compileScript("DumbLambdaClass"); + } } - public static void init(Scriptable scope) { - LambdaConstructor cons = - new LambdaConstructor(scope, "DumbLambdaClass", 0, - (Context cx, Scriptable s, Object[] args) -> new DumbLambdaClass()); - cons.definePrototypeMethod(scope, "one", 0, DumbLambdaClass::noop); - cons.definePrototypeMethod(scope, "two", 0, DumbLambdaClass::noop); - cons.definePrototypeMethod(scope, "three", 0, DumbLambdaClass::noop); - cons.definePrototypeMethod(scope, "four", 0, DumbLambdaClass::noop); - cons.definePrototypeMethod(scope, "five", 0, DumbLambdaClass::noop); - cons.definePrototypeMethod(scope, "six", 0, DumbLambdaClass::noop); - cons.definePrototypeMethod(scope, "seven", 0, DumbLambdaClass::noop); - cons.definePrototypeMethod(scope, "eight", 0, DumbLambdaClass::noop); - cons.definePrototypeMethod(scope, "nine", 0, DumbLambdaClass::noop); - cons.definePrototypeMethod(scope, "setValue", 1, DumbLambdaClass::setValue); - cons.definePrototypeMethod(scope, "getValue", 1, DumbLambdaClass::getValue); - ScriptableObject.putProperty(scope, "DumbLambdaClass", cons); + @Benchmark + public Object dumbLambdaClassMethods(DumbLambdaState state) { + return state.testScript.exec(state.cx, state.scope); } - @Override - public String getClassName() { - return "DumbLambdaClass"; + private static class DumbLambdaClass extends ScriptableObject { + + private static Object noop( + Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return Undefined.instance; + } + + private static Object setValue( + Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + if (args.length < 1) { + throw ScriptRuntime.throwError(cx, scope, "Not enough args"); + } + DumbLambdaClass self = + LambdaConstructor.convertThisObject(thisObj, DumbLambdaClass.class); + self.value = ScriptRuntime.toInt32(args[0]); + return Undefined.instance; + } + + private static Object getValue( + Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + DumbLambdaClass self = + LambdaConstructor.convertThisObject(thisObj, DumbLambdaClass.class); + return self.value; + } + + public static void init(Scriptable scope) { + LambdaConstructor cons = + new LambdaConstructor( + scope, + "DumbLambdaClass", + 0, + (Context cx, Scriptable s, Object[] args) -> new DumbLambdaClass()); + cons.definePrototypeMethod(scope, "one", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "two", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "three", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "four", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "five", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "six", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "seven", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "eight", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "nine", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "setValue", 1, DumbLambdaClass::setValue); + cons.definePrototypeMethod(scope, "getValue", 1, DumbLambdaClass::getValue); + ScriptableObject.putProperty(scope, "DumbLambdaClass", cons); + } + + @Override + public String getClassName() { + return "DumbLambdaClass"; + } + + private int value; } - - private int value; - } } diff --git a/src/org/mozilla/javascript/BaseFunction.java b/src/org/mozilla/javascript/BaseFunction.java index bc4d834bcc..ed95eedd29 100644 --- a/src/org/mozilla/javascript/BaseFunction.java +++ b/src/org/mozilla/javascript/BaseFunction.java @@ -7,31 +7,29 @@ package org.mozilla.javascript; /** - * The base class for Function objects. That is one of two purposes. It is also - * the prototype for every "function" defined except those that are used - * as GeneratorFunctions via the ES6 "function *" syntax. + * The base class for Function objects. That is one of two purposes. It is also the prototype for + * every "function" defined except those that are used as GeneratorFunctions via the ES6 "function + * *" syntax. + * + *

See ECMA 15.3. * - * See ECMA 15.3. * @author Norris Boyd */ -public class BaseFunction extends IdScriptableObject implements Function -{ +public class BaseFunction extends IdScriptableObject implements Function { private static final long serialVersionUID = 5311394446546053859L; private static final Object FUNCTION_TAG = "Function"; private static final String FUNCTION_CLASS = "Function"; static final String GENERATOR_FUNCTION_CLASS = "__GeneratorFunction"; - static void init(Scriptable scope, boolean sealed) - { + static void init(Scriptable scope, boolean sealed) { BaseFunction obj = new BaseFunction(); // Function.prototype attributes: see ECMA 15.3.3.1 obj.prototypePropertyAttributes = DONTENUM | READONLY | PERMANENT; obj.exportAsJSClass(MAX_PROTOTYPE_ID, scope, sealed); } - static Object initAsGeneratorFunction(Scriptable scope, boolean sealed) - { + static Object initAsGeneratorFunction(Scriptable scope, boolean sealed) { BaseFunction obj = new BaseFunction(true); // Function.prototype attributes: see ECMA 15.3.3.1 obj.prototypePropertyAttributes = READONLY | PERMANENT; @@ -63,143 +61,142 @@ protected boolean isGeneratorFunction() { /** * Gets the value returned by calling the typeof operator on this object. + * * @see org.mozilla.javascript.ScriptableObject#getTypeOf() - * @return "function" or "undefined" if {@link #avoidObjectDetection()} returns true + * @return "function" or "undefined" if {@link #avoidObjectDetection()} returns true + * */ @Override - public String getTypeOf() - { + public String getTypeOf() { return avoidObjectDetection() ? "undefined" : "function"; } /** * Implements the instanceof operator for JavaScript Function objects. - *

- * + * + *

* foo = new Foo();
* foo instanceof Foo; // true
*
* - * @param instance The value that appeared on the LHS of the instanceof - * operator - * @return true if the "prototype" property of "this" appears in - * value's prototype chain - * + * @param instance The value that appeared on the LHS of the instanceof operator + * @return true if the "prototype" property of "this" appears in value's prototype chain */ @Override - public boolean hasInstance(Scriptable instance) - { + public boolean hasInstance(Scriptable instance) { Object protoProp = ScriptableObject.getProperty(this, "prototype"); if (protoProp instanceof Scriptable) { - return ScriptRuntime.jsDelegatesTo(instance, (Scriptable)protoProp); + return ScriptRuntime.jsDelegatesTo(instance, (Scriptable) protoProp); } - throw ScriptRuntime.typeErrorById("msg.instanceof.bad.prototype", - getFunctionName()); + throw ScriptRuntime.typeErrorById("msg.instanceof.bad.prototype", getFunctionName()); } -// #string_id_map# - - private static final int - Id_length = 1, - Id_arity = 2, - Id_name = 3, - Id_prototype = 4, - Id_arguments = 5, + // #string_id_map# - MAX_INSTANCE_ID = 5; + private static final int Id_length = 1, + Id_arity = 2, + Id_name = 3, + Id_prototype = 4, + Id_arguments = 5, + MAX_INSTANCE_ID = 5; @Override - protected int getMaxInstanceId() - { + protected int getMaxInstanceId() { return MAX_INSTANCE_ID; } @Override - protected int findInstanceIdInfo(String s) - { + protected int findInstanceIdInfo(String s) { int id; -// #generated# Last update: 2021-03-21 09:52:05 MEZ + // #generated# Last update: 2021-03-21 09:52:05 MEZ switch (s) { - case "length": - id = Id_length; - break; - case "arity": - id = Id_arity; - break; - case "name": - id = Id_name; - break; - case "prototype": - id = Id_prototype; - break; - case "arguments": - id = Id_arguments; - break; - default: - id = 0; - break; + case "length": + id = Id_length; + break; + case "arity": + id = Id_arity; + break; + case "name": + id = Id_name; + break; + case "prototype": + id = Id_prototype; + break; + case "arguments": + id = Id_arguments; + break; + default: + id = 0; + break; } -// #/generated# -// #/string_id_map# + // #/generated# + // #/string_id_map# if (id == 0) return super.findInstanceIdInfo(s); int attr; switch (id) { - case Id_length: - case Id_arity: - case Id_name: - attr = DONTENUM | READONLY | PERMANENT; - break; - case Id_prototype: - // some functions such as built-ins don't have a prototype property - if (!hasPrototypeProperty()) { - return 0; - } - attr = prototypePropertyAttributes; - break; - case Id_arguments: - attr = argumentsAttributes; - break; - default: throw new IllegalStateException(); + case Id_length: + case Id_arity: + case Id_name: + attr = DONTENUM | READONLY | PERMANENT; + break; + case Id_prototype: + // some functions such as built-ins don't have a prototype property + if (!hasPrototypeProperty()) { + return 0; + } + attr = prototypePropertyAttributes; + break; + case Id_arguments: + attr = argumentsAttributes; + break; + default: + throw new IllegalStateException(); } return instanceIdInfo(attr, id); } @Override - protected String getInstanceIdName(int id) - { + protected String getInstanceIdName(int id) { switch (id) { - case Id_length: return "length"; - case Id_arity: return "arity"; - case Id_name: return "name"; - case Id_prototype: return "prototype"; - case Id_arguments: return "arguments"; + case Id_length: + return "length"; + case Id_arity: + return "arity"; + case Id_name: + return "name"; + case Id_prototype: + return "prototype"; + case Id_arguments: + return "arguments"; } return super.getInstanceIdName(id); } @Override - protected Object getInstanceIdValue(int id) - { + protected Object getInstanceIdValue(int id) { switch (id) { - case Id_length: return ScriptRuntime.wrapInt(getLength()); - case Id_arity: return ScriptRuntime.wrapInt(getArity()); - case Id_name: return getFunctionName(); - case Id_prototype: return getPrototypeProperty(); - case Id_arguments: return getArguments(); + case Id_length: + return ScriptRuntime.wrapInt(getLength()); + case Id_arity: + return ScriptRuntime.wrapInt(getArity()); + case Id_name: + return getFunctionName(); + case Id_prototype: + return getPrototypeProperty(); + case Id_arguments: + return getArguments(); } return super.getInstanceIdValue(id); } @Override - protected void setInstanceIdValue(int id, Object value) - { + protected void setInstanceIdValue(int id, Object value) { switch (id) { case Id_prototype: if ((prototypePropertyAttributes & READONLY) == 0) { - prototypeProperty = (value != null) - ? value : UniqueTag.NULL_VALUE; + prototypeProperty = (value != null) ? value : UniqueTag.NULL_VALUE; } return; case Id_arguments: @@ -222,8 +219,7 @@ protected void setInstanceIdValue(int id, Object value) } @Override - protected void setInstanceIdAttributes(int id, int attr) - { + protected void setInstanceIdAttributes(int id, int attr) { switch (id) { case Id_prototype: prototypePropertyAttributes = attr; @@ -236,8 +232,7 @@ protected void setInstanceIdAttributes(int id, int attr) } @Override - protected void fillConstructorProperties(IdFunctionObject ctor) - { + protected void fillConstructorProperties(IdFunctionObject ctor) { // Fix up bootstrapping problem: getPrototype of the IdFunctionObject // can not return Function.prototype because Function object is not // yet defined. @@ -246,18 +241,36 @@ protected void fillConstructorProperties(IdFunctionObject ctor) } @Override - protected void initPrototypeId(int id) - { + protected void initPrototypeId(int id) { String s; int arity; switch (id) { - case Id_constructor: arity=1; s="constructor"; break; - case Id_toString: arity=0; s="toString"; break; - case Id_toSource: arity=1; s="toSource"; break; - case Id_apply: arity=2; s="apply"; break; - case Id_call: arity=1; s="call"; break; - case Id_bind: arity=1; s="bind"; break; - default: throw new IllegalArgumentException(String.valueOf(id)); + case Id_constructor: + arity = 1; + s = "constructor"; + break; + case Id_toString: + arity = 0; + s = "toString"; + break; + case Id_toSource: + arity = 1; + s = "toSource"; + break; + case Id_apply: + arity = 2; + s = "apply"; + break; + case Id_call: + arity = 1; + s = "call"; + break; + case Id_bind: + arity = 1; + s = "bind"; + break; + default: + throw new IllegalArgumentException(String.valueOf(id)); } initPrototypeMethod(FUNCTION_TAG, id, s, arity); } @@ -267,8 +280,8 @@ static boolean isApply(IdFunctionObject f) { } static boolean isApplyOrCall(IdFunctionObject f) { - if(f.hasTag(FUNCTION_TAG)) { - switch(f.methodId()) { + if (f.hasTag(FUNCTION_TAG)) { + switch (f.methodId()) { case Id_apply: case Id_call: return true; @@ -278,79 +291,74 @@ static boolean isApplyOrCall(IdFunctionObject f) { } @Override - public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, - Scriptable thisObj, Object[] args) - { + public Object execIdCall( + IdFunctionObject f, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { if (!f.hasTag(FUNCTION_TAG)) { return super.execIdCall(f, cx, scope, thisObj, args); } int id = f.methodId(); switch (id) { - case Id_constructor: - return jsConstructor(cx, scope, args); - - case Id_toString: { - BaseFunction realf = realFunction(thisObj, f); - int indent = ScriptRuntime.toInt32(args, 0); - return realf.decompile(indent, 0); - } - - case Id_toSource: { - BaseFunction realf = realFunction(thisObj, f); - int indent = 0; - int flags = Decompiler.TO_SOURCE_FLAG; - if (args.length != 0) { - indent = ScriptRuntime.toInt32(args[0]); - if (indent >= 0) { - flags = 0; - } else { - indent = 0; + case Id_constructor: + return jsConstructor(cx, scope, args); + + case Id_toString: + { + BaseFunction realf = realFunction(thisObj, f); + int indent = ScriptRuntime.toInt32(args, 0); + return realf.decompile(indent, 0); } - } - return realf.decompile(indent, flags); - } - case Id_apply: - case Id_call: - return ScriptRuntime.applyOrCall(id == Id_apply, - cx, scope, thisObj, args); + case Id_toSource: + { + BaseFunction realf = realFunction(thisObj, f); + int indent = 0; + int flags = Decompiler.TO_SOURCE_FLAG; + if (args.length != 0) { + indent = ScriptRuntime.toInt32(args[0]); + if (indent >= 0) { + flags = 0; + } else { + indent = 0; + } + } + return realf.decompile(indent, flags); + } - case Id_bind: - if ( !(thisObj instanceof Callable) ) { - throw ScriptRuntime.notFunctionError(thisObj); - } - Callable targetFunction = (Callable) thisObj; - int argc = args.length; - final Scriptable boundThis; - final Object[] boundArgs; - if (argc > 0) { - boundThis = ScriptRuntime.toObjectOrNull(cx, args[0], scope); - boundArgs = new Object[argc-1]; - System.arraycopy(args, 1, boundArgs, 0, argc-1); - } else { - boundThis = null; - boundArgs = ScriptRuntime.emptyArgs; - } - return new BoundFunction(cx, scope, targetFunction, boundThis, boundArgs); + case Id_apply: + case Id_call: + return ScriptRuntime.applyOrCall(id == Id_apply, cx, scope, thisObj, args); + + case Id_bind: + if (!(thisObj instanceof Callable)) { + throw ScriptRuntime.notFunctionError(thisObj); + } + Callable targetFunction = (Callable) thisObj; + int argc = args.length; + final Scriptable boundThis; + final Object[] boundArgs; + if (argc > 0) { + boundThis = ScriptRuntime.toObjectOrNull(cx, args[0], scope); + boundArgs = new Object[argc - 1]; + System.arraycopy(args, 1, boundArgs, 0, argc - 1); + } else { + boundThis = null; + boundArgs = ScriptRuntime.emptyArgs; + } + return new BoundFunction(cx, scope, targetFunction, boundThis, boundArgs); } throw new IllegalArgumentException(String.valueOf(id)); } - private static BaseFunction realFunction(Scriptable thisObj, IdFunctionObject f) - { + private static BaseFunction realFunction(Scriptable thisObj, IdFunctionObject f) { Object x = thisObj.getDefaultValue(ScriptRuntime.FunctionClass); if (x instanceof Delegator) { - x = ((Delegator)x).getDelegee(); + x = ((Delegator) x).getDelegee(); } return ensureType(x, BaseFunction.class, f); } - /** - * Make value as DontEnum, DontDelete, ReadOnly - * prototype property of this Function object - */ - public void setImmunePrototypeProperty(Object value) - { + /** Make value as DontEnum, DontDelete, ReadOnly prototype property of this Function object */ + public void setImmunePrototypeProperty(Object value) { if ((prototypePropertyAttributes & READONLY) != 0) { throw new IllegalStateException(); } @@ -358,8 +366,7 @@ public void setImmunePrototypeProperty(Object value) prototypePropertyAttributes = DONTENUM | PERMANENT | READONLY; } - protected Scriptable getClassPrototype() - { + protected Scriptable getClassPrototype() { Object protoVal = getPrototypeProperty(); if (protoVal instanceof Scriptable) { return (Scriptable) protoVal; @@ -367,24 +374,19 @@ protected Scriptable getClassPrototype() return ScriptableObject.getObjectPrototype(this); } - /** - * Should be overridden. - */ + /** Should be overridden. */ @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, - Object[] args) - { + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { return Undefined.instance; } @Override - public Scriptable construct(Context cx, Scriptable scope, Object[] args) - { + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { Scriptable result = createObject(cx, scope); if (result != null) { Object val = call(cx, scope, result, args); if (val instanceof Scriptable) { - result = (Scriptable)val; + result = (Scriptable) val; } } else { Object val = call(cx, scope, null, args); @@ -392,10 +394,12 @@ public Scriptable construct(Context cx, Scriptable scope, Object[] args) // It is program error not to return Scriptable from // the call method if createObject returns null. throw new IllegalStateException( - "Bad implementaion of call as constructor, name=" - +getFunctionName()+" in "+getClass().getName()); + "Bad implementaion of call as constructor, name=" + + getFunctionName() + + " in " + + getClass().getName()); } - result = (Scriptable)val; + result = (Scriptable) val; if (result.getPrototype() == null) { Scriptable proto = getClassPrototype(); if (result != proto) { @@ -413,17 +417,13 @@ public Scriptable construct(Context cx, Scriptable scope, Object[] args) } /** - * Creates new script object. - * The default implementation of {@link #construct} uses the method to - * to get the value for thisObj argument when invoking - * {@link #call}. - * The methos is allowed to return null to indicate that - * {@link #call} will create a new object itself. In this case - * {@link #construct} will set scope and prototype on the result + * Creates new script object. The default implementation of {@link #construct} uses the method + * to to get the value for thisObj argument when invoking {@link #call}. The methos + * is allowed to return null to indicate that {@link #call} will create a new + * object itself. In this case {@link #construct} will set scope and prototype on the result * {@link #call} unless they are already set. */ - public Scriptable createObject(Context cx, Scriptable scope) - { + public Scriptable createObject(Context cx, Scriptable scope) { Scriptable newInstance = new NativeObject(); newInstance.setPrototype(getClassPrototype()); newInstance.setParentScope(getParentScope()); @@ -431,15 +431,12 @@ public Scriptable createObject(Context cx, Scriptable scope) } /** - * Decompile the source information associated with this js - * function/script back into a string. + * Decompile the source information associated with this js function/script back into a string. * * @param indent How much to indent the decompiled result. - * * @param flags Flags specifying format of decompilation output. */ - String decompile(int indent, int flags) - { + String decompile(int indent, int flags) { StringBuilder sb = new StringBuilder(); boolean justbody = (0 != (flags & Decompiler.ONLY_BODY_FLAG)); if (!justbody) { @@ -456,9 +453,13 @@ String decompile(int indent, int flags) return sb.toString(); } - public int getArity() { return 0; } + public int getArity() { + return 0; + } - public int getLength() { return 0; } + public int getLength() { + return 0; + } public String getFunctionName() { return ""; @@ -507,29 +508,25 @@ protected synchronized Object setupDefaultPrototype() { return obj; } - private Object getArguments() - { - // .arguments is deprecated, so we use a slow - // way of getting it that doesn't add to the invocation cost. - // TODO: add warning, error based on version - Object value = defaultHas("arguments") ? defaultGet("arguments") : argumentsObj; - if (value != NOT_FOUND) { - // Should after changing .arguments its - // activation still be available during Function call? - // This code assumes it should not: - // defaultGet("arguments") != NOT_FOUND - // means assigned arguments - return value; - } - Context cx = Context.getContext(); - NativeCall activation = ScriptRuntime.findFunctionActivation(cx, this); - return (activation == null) - ? null - : activation.get("arguments", activation); - } - - private Object jsConstructor(Context cx, Scriptable scope, Object[] args) - { + private Object getArguments() { + // .arguments is deprecated, so we use a slow + // way of getting it that doesn't add to the invocation cost. + // TODO: add warning, error based on version + Object value = defaultHas("arguments") ? defaultGet("arguments") : argumentsObj; + if (value != NOT_FOUND) { + // Should after changing .arguments its + // activation still be available during Function call? + // This code assumes it should not: + // defaultGet("arguments") != NOT_FOUND + // means assigned arguments + return value; + } + Context cx = Context.getContext(); + NativeCall activation = ScriptRuntime.findFunctionActivation(cx, this); + return (activation == null) ? null : activation.get("arguments", activation); + } + + private Object jsConstructor(Context cx, Scriptable scope, Object[] args) { int arglen = args.length; StringBuilder sourceBuf = new StringBuilder(); @@ -571,8 +568,7 @@ private Object jsConstructor(Context cx, Scriptable scope, Object[] args) linep[0] = 1; } - String sourceURI = ScriptRuntime. - makeUrlForGeneratedScript(false, filename, linep[0]); + String sourceURI = ScriptRuntime.makeUrlForGeneratedScript(false, filename, linep[0]); Scriptable global = ScriptableObject.getTopLevelScope(scope); @@ -581,60 +577,55 @@ private Object jsConstructor(Context cx, Scriptable scope, Object[] args) Evaluator evaluator = Context.createInterpreter(); if (evaluator == null) { - throw new JavaScriptException("Interpreter not present", - filename, linep[0]); + throw new JavaScriptException("Interpreter not present", filename, linep[0]); } // Compile with explicit interpreter instance to force interpreter // mode. - return cx.compileFunction(global, source, evaluator, reporter, - sourceURI, 1, null); + return cx.compileFunction(global, source, evaluator, reporter, sourceURI, 1, null); } @Override - protected int findPrototypeId(String s) - { + protected int findPrototypeId(String s) { int id; -// #string_id_map# -// #generated# Last update: 2021-03-21 09:52:05 MEZ + // #string_id_map# + // #generated# Last update: 2021-03-21 09:52:05 MEZ switch (s) { - case "constructor": - id = Id_constructor; - break; - case "toString": - id = Id_toString; - break; - case "toSource": - id = Id_toSource; - break; - case "apply": - id = Id_apply; - break; - case "call": - id = Id_call; - break; - case "bind": - id = Id_bind; - break; - default: - id = 0; - break; + case "constructor": + id = Id_constructor; + break; + case "toString": + id = Id_toString; + break; + case "toSource": + id = Id_toSource; + break; + case "apply": + id = Id_apply; + break; + case "call": + id = Id_call; + break; + case "bind": + id = Id_bind; + break; + default: + id = 0; + break; } -// #/generated# + // #/generated# return id; } - private static final int - Id_constructor = 1, - Id_toString = 2, - Id_toSource = 3, - Id_apply = 4, - Id_call = 5, - Id_bind = 6, + private static final int Id_constructor = 1, + Id_toString = 2, + Id_toSource = 3, + Id_apply = 4, + Id_call = 5, + Id_bind = 6, + MAX_PROTOTYPE_ID = Id_bind; - MAX_PROTOTYPE_ID = Id_bind; - -// #/string_id_map# + // #/string_id_map# private Object prototypeProperty; private Object argumentsObj = NOT_FOUND; @@ -643,7 +634,6 @@ protected int findPrototypeId(String s) // For function object instances, attributes are // {configurable:false, enumerable:false}; // see ECMA 15.3.5.2 - private int prototypePropertyAttributes = PERMANENT|DONTENUM; - private int argumentsAttributes = PERMANENT|DONTENUM; + private int prototypePropertyAttributes = PERMANENT | DONTENUM; + private int argumentsAttributes = PERMANENT | DONTENUM; } - diff --git a/src/org/mozilla/javascript/Constructable.java b/src/org/mozilla/javascript/Constructable.java index a15a10ba78..9f3d53bc72 100644 --- a/src/org/mozilla/javascript/Constructable.java +++ b/src/org/mozilla/javascript/Constructable.java @@ -1,21 +1,19 @@ package org.mozilla.javascript; -/** - * An interface that can be used to implement a constructor function as a lambda. - */ +/** An interface that can be used to implement a constructor function as a lambda. */ public interface Constructable { - /** - * Call the function as a constructor. - * - * This method is invoked by the runtime in order to satisfy a use of the JavaScript - * new operator. This method is expected to create a new object and return it. - * - * @param cx the current Context for this thread - * @param scope an enclosing scope of the caller except when the function is called from a - * closure. - * @param args the array of arguments - * @return the allocated object - */ - Scriptable construct(Context cx, Scriptable scope, Object[] args); + /** + * Call the function as a constructor. + * + *

This method is invoked by the runtime in order to satisfy a use of the JavaScript + * new operator. This method is expected to create a new object and return it. + * + * @param cx the current Context for this thread + * @param scope an enclosing scope of the caller except when the function is called from a + * closure. + * @param args the array of arguments + * @return the allocated object + */ + Scriptable construct(Context cx, Scriptable scope, Object[] args); } diff --git a/src/org/mozilla/javascript/Function.java b/src/org/mozilla/javascript/Function.java index 0992317019..4d70231883 100644 --- a/src/org/mozilla/javascript/Function.java +++ b/src/org/mozilla/javascript/Function.java @@ -9,43 +9,37 @@ package org.mozilla.javascript; /** - * This is interface that all functions in JavaScript must implement. - * The interface provides for calling functions and constructors. + * This is interface that all functions in JavaScript must implement. The interface provides for + * calling functions and constructors. * * @see org.mozilla.javascript.Scriptable * @author Norris Boyd */ - -public interface Function extends Scriptable, Callable, Constructable -{ +public interface Function extends Scriptable, Callable, Constructable { /** * Call the function. * - * Note that the array of arguments is not guaranteed to have - * length greater than 0. + *

Note that the array of arguments is not guaranteed to have length greater than 0. * * @param cx the current Context for this thread - * @param scope the scope to execute the function relative to. This is - * set to the value returned by getParentScope() except - * when the function is called from a closure. + * @param scope the scope to execute the function relative to. This is set to the value returned + * by getParentScope() except when the function is called from a closure. * @param thisObj the JavaScript this object * @param args the array of arguments * @return the result of the call */ @Override - Object call(Context cx, Scriptable scope, Scriptable thisObj, - Object[] args); + Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args); /** * Call the function as a constructor. * - * This method is invoked by the runtime in order to satisfy a use - * of the JavaScript new operator. This method is - * expected to create a new object and return it. + *

This method is invoked by the runtime in order to satisfy a use of the JavaScript + * new operator. This method is expected to create a new object and return it. * * @param cx the current Context for this thread - * @param scope an enclosing scope of the caller except - * when the function is called from a closure. + * @param scope an enclosing scope of the caller except when the function is called from a + * closure. * @param args the array of arguments * @return the allocated object */ diff --git a/src/org/mozilla/javascript/LambdaConstructor.java b/src/org/mozilla/javascript/LambdaConstructor.java index 8f5d7bbf47..914cb1094b 100644 --- a/src/org/mozilla/javascript/LambdaConstructor.java +++ b/src/org/mozilla/javascript/LambdaConstructor.java @@ -11,127 +11,124 @@ * interface that can be easily implemented as a lambda. The LambdaFunction class may be used to add * functions to the prototype that are also implemented as lambdas. * - * In micro benchmarks (as of 2021) using this class to implement a built-in class is about - * 15% more efficient than using IdScriptableObject, and about 25% faster than using reflection - * via the ScriptableObject.defineClass() family of methods. Furthermore, it results in - * code that more directly maps to JavaScript idioms than either methods, it is much easier - * to implement than IdScriptableObject, and the lambda pattern makes it easier to maintain - * state in various ways that don't always map directly to the existing concepts. + *

In micro benchmarks (as of 2021) using this class to implement a built-in class is about 15% + * more efficient than using IdScriptableObject, and about 25% faster than using reflection via the + * ScriptableObject.defineClass() family of methods. Furthermore, it results in code that more + * directly maps to JavaScript idioms than either methods, it is much easier to implement than + * IdScriptableObject, and the lambda pattern makes it easier to maintain state in various ways that + * don't always map directly to the existing concepts. */ -public class LambdaConstructor - extends LambdaFunction { +public class LambdaConstructor extends LambdaFunction { - private static final long serialVersionUID = 2691205302914111400L; + private static final long serialVersionUID = 2691205302914111400L; - /** If this flag is set, the constructor may be invoked as an ordinary function */ - public static final int CONSTRUCTOR_FUNCTION = 1; - /** If this flag is set, the constructor may be invoked using "new" */ - public static final int CONSTRUCTOR_NEW = 1 << 1; - /** By default, the constructor may be invoked either way */ - public static final int CONSTRUCTOR_DEFAULT = CONSTRUCTOR_FUNCTION | CONSTRUCTOR_NEW; + /** If this flag is set, the constructor may be invoked as an ordinary function */ + public static final int CONSTRUCTOR_FUNCTION = 1; + /** If this flag is set, the constructor may be invoked using "new" */ + public static final int CONSTRUCTOR_NEW = 1 << 1; + /** By default, the constructor may be invoked either way */ + public static final int CONSTRUCTOR_DEFAULT = CONSTRUCTOR_FUNCTION | CONSTRUCTOR_NEW; - // Lambdas should not be serialized. - private transient final Constructable targetConstructor; - private final int flags; + // Lambdas should not be serialized. + private final transient Constructable targetConstructor; + private final int flags; - /** - * Create a new function that may be used as a constructor. The new object will have the - * Function prototype and no parent. The caller is responsible for binding this object - * to the appropriate scope. - * - * @param scope scope of the calling context - * @param name name of the function - * @param length the arity of the function - * @param target an object that implements the function in Java. Since Constructable is a - * single-function interface this will typically be implemented as a lambda. - */ - public LambdaConstructor(Scriptable scope, String name, int length, Constructable target) { - super(scope, name, length, null); - this.targetConstructor = target; - this.flags = CONSTRUCTOR_DEFAULT; - } + /** + * Create a new function that may be used as a constructor. The new object will have the + * Function prototype and no parent. The caller is responsible for binding this object to the + * appropriate scope. + * + * @param scope scope of the calling context + * @param name name of the function + * @param length the arity of the function + * @param target an object that implements the function in Java. Since Constructable is a + * single-function interface this will typically be implemented as a lambda. + */ + public LambdaConstructor(Scriptable scope, String name, int length, Constructable target) { + super(scope, name, length, null); + this.targetConstructor = target; + this.flags = CONSTRUCTOR_DEFAULT; + } - /** - * Create a new function and control whether it may be invoked using new, as a function, - * or both. - */ - public LambdaConstructor(Scriptable scope, String name, int length, int flags, - Constructable target) { - super(scope, name, length, null); - this.targetConstructor = target; - this.flags = flags; - } + /** + * Create a new function and control whether it may be invoked using new, as a function, or + * both. + */ + public LambdaConstructor( + Scriptable scope, String name, int length, int flags, Constructable target) { + super(scope, name, length, null); + this.targetConstructor = target; + this.flags = flags; + } - @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - if ((flags & CONSTRUCTOR_FUNCTION) == 0) { - throw ScriptRuntime.typeErrorById("msg.constructor.no.function", getFunctionName()); + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + if ((flags & CONSTRUCTOR_FUNCTION) == 0) { + throw ScriptRuntime.typeErrorById("msg.constructor.no.function", getFunctionName()); + } + return targetConstructor.construct(cx, scope, args); } - return targetConstructor.construct(cx, scope, args); - } - @Override - public Scriptable construct(Context cx, Scriptable scope, Object[] args) { - if ((flags & CONSTRUCTOR_NEW) == 0) { - throw ScriptRuntime.typeErrorById("msg.no.new", getFunctionName()); + @Override + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { + if ((flags & CONSTRUCTOR_NEW) == 0) { + throw ScriptRuntime.typeErrorById("msg.no.new", getFunctionName()); + } + Scriptable obj = targetConstructor.construct(cx, scope, args); + obj.setPrototype(getClassPrototype()); + obj.setParentScope(scope); + return obj; } - Scriptable obj = targetConstructor.construct(cx, scope, args); - obj.setPrototype(getClassPrototype()); - obj.setParentScope(scope); - return obj; - } - /** - * Define a function property on the prototype of the constructor using a LambdaFunction under the - * covers. - */ - public void definePrototypeMethod(Scriptable scope, String name, int length, Callable target) { - LambdaFunction f = new LambdaFunction(scope, name, length, target); - ScriptableObject proto = getPrototypeScriptable(); - proto.defineProperty(name, f, 0); - } + /** + * Define a function property on the prototype of the constructor using a LambdaFunction under + * the covers. + */ + public void definePrototypeMethod(Scriptable scope, String name, int length, Callable target) { + LambdaFunction f = new LambdaFunction(scope, name, length, target); + ScriptableObject proto = getPrototypeScriptable(); + proto.defineProperty(name, f, 0); + } - /** - * Define a property that may be of any type on the prototype of this constructor. - */ - public void definePrototypeProperty(String name, Object value, int attributes) { - ScriptableObject proto = getPrototypeScriptable(); - proto.defineProperty(name, value, 0); - } + /** Define a property that may be of any type on the prototype of this constructor. */ + public void definePrototypeProperty(String name, Object value, int attributes) { + ScriptableObject proto = getPrototypeScriptable(); + proto.defineProperty(name, value, 0); + } - public void definePrototypeProperty(Symbol key, Object value, int attributes) { - ScriptableObject proto = getPrototypeScriptable(); - proto.defineProperty(key, value, 0); - } + public void definePrototypeProperty(Symbol key, Object value, int attributes) { + ScriptableObject proto = getPrototypeScriptable(); + proto.defineProperty(key, value, 0); + } - /** - * Define a function property directly on the constructor that is implemented under the - * covers by a LambdaFunction. - */ - public void defineConstructorMethod(Scriptable scope, String name, int length, Callable target) { - LambdaFunction f = new LambdaFunction(scope, name, length, target); - defineProperty(name, f, DONTENUM); - } + /** + * Define a function property directly on the constructor that is implemented under the covers + * by a LambdaFunction. + */ + public void defineConstructorMethod( + Scriptable scope, String name, int length, Callable target) { + LambdaFunction f = new LambdaFunction(scope, name, length, target); + defineProperty(name, f, DONTENUM); + } - /** - * A convenience method to convert JavaScript's "this" object into a target class and - * throw a TypeError if it does not match. This is useful for implementing lambda - * functions, as "this" in JavaScript doesn't necessarily map to an instance of the - * class. - */ - @SuppressWarnings("unchecked") - public static T convertThisObject(Scriptable thisObj, Class targetClass) { - if (!targetClass.isInstance(thisObj)) { - throw ScriptRuntime.typeErrorById("msg.this.not.instance"); + /** + * A convenience method to convert JavaScript's "this" object into a target class and throw a + * TypeError if it does not match. This is useful for implementing lambda functions, as "this" + * in JavaScript doesn't necessarily map to an instance of the class. + */ + @SuppressWarnings("unchecked") + public static T convertThisObject(Scriptable thisObj, Class targetClass) { + if (!targetClass.isInstance(thisObj)) { + throw ScriptRuntime.typeErrorById("msg.this.not.instance"); + } + return (T) thisObj; } - return (T)thisObj; - } - private ScriptableObject getPrototypeScriptable() { - Object prop = getPrototypeProperty(); - if (!(prop instanceof ScriptableObject)) { - throw ScriptRuntime.typeError("Not properly a lambda constructor"); + private ScriptableObject getPrototypeScriptable() { + Object prop = getPrototypeProperty(); + if (!(prop instanceof ScriptableObject)) { + throw ScriptRuntime.typeError("Not properly a lambda constructor"); + } + return (ScriptableObject) prop; } - return (ScriptableObject)prop; - } } diff --git a/src/org/mozilla/javascript/LambdaFunction.java b/src/org/mozilla/javascript/LambdaFunction.java index c2725d7cd6..d27e54f2b8 100644 --- a/src/org/mozilla/javascript/LambdaFunction.java +++ b/src/org/mozilla/javascript/LambdaFunction.java @@ -11,66 +11,63 @@ * Function class, and which is implemented using a single function that can easily be implemented * using a lambda expression. */ -public class LambdaFunction - extends BaseFunction { +public class LambdaFunction extends BaseFunction { - private static final long serialVersionUID = -8388132362854748293L; + private static final long serialVersionUID = -8388132362854748293L; - // The target is expected to be a lambda -- lambdas should not be serialized. - private transient final Callable target; - private final String name; - private final int length; + // The target is expected to be a lambda -- lambdas should not be serialized. + private final transient Callable target; + private final String name; + private final int length; - /** - * Create a new function. The new object will have the Function prototype and no parent. The - * caller is responsible for binding this object to the appropriate scope. - * - * @param scope scope of the calling context - * @param name name of the function - * @param length the arity of the function - * @param target an object that implements the function in Java. Since Callable is a - * single-function interface this will typically be implemented as a lambda. - */ - public LambdaFunction(Scriptable scope, String name, int length, Callable target) { - this.target = target; - this.name = name; - this.length = length; - ScriptRuntime.setFunctionProtoAndParent(this, scope); - setupDefaultPrototype(); - } + /** + * Create a new function. The new object will have the Function prototype and no parent. The + * caller is responsible for binding this object to the appropriate scope. + * + * @param scope scope of the calling context + * @param name name of the function + * @param length the arity of the function + * @param target an object that implements the function in Java. Since Callable is a + * single-function interface this will typically be implemented as a lambda. + */ + public LambdaFunction(Scriptable scope, String name, int length, Callable target) { + this.target = target; + this.name = name; + this.length = length; + ScriptRuntime.setFunctionProtoAndParent(this, scope); + setupDefaultPrototype(); + } - /** - * Create a new built-in function, with no name, and no default prototype. - */ - public LambdaFunction(Scriptable scope, int length, Callable target) { - this.target = target; - this.length = length; - this.name = ""; - ScriptRuntime.setFunctionProtoAndParent(this, scope); - } + /** Create a new built-in function, with no name, and no default prototype. */ + public LambdaFunction(Scriptable scope, int length, Callable target) { + this.target = target; + this.length = length; + this.name = ""; + ScriptRuntime.setFunctionProtoAndParent(this, scope); + } - @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - return target.call(cx, scope, thisObj, args); - } + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return target.call(cx, scope, thisObj, args); + } - @Override - public Scriptable construct(Context cx, Scriptable scope, Object[] args) { - throw ScriptRuntime.typeErrorById("msg.no.new", getFunctionName()); - } + @Override + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { + throw ScriptRuntime.typeErrorById("msg.no.new", getFunctionName()); + } - @Override - public int getLength() { - return length; - } + @Override + public int getLength() { + return length; + } - @Override - public int getArity() { - return length; - } + @Override + public int getArity() { + return length; + } - @Override - public String getFunctionName() { - return name; - } + @Override + public String getFunctionName() { + return name; + } } diff --git a/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java b/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java index b21fe4cca6..5d3f810337 100644 --- a/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java +++ b/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java @@ -18,205 +18,256 @@ public class LambdaFunctionTest { - private Context cx; - private Scriptable root; - - @Before - public void init() throws IOException { - cx = Context.enter(); - cx.setLanguageVersion(Context.VERSION_ES6); - root = cx.initStandardObjects(); - try (FileReader rdr = new FileReader("testsrc/assert.js")) { - cx.evaluateReader(root, rdr, "assert.js", 1, null); + private Context cx; + private Scriptable root; + + @Before + public void init() throws IOException { + cx = Context.enter(); + cx.setLanguageVersion(Context.VERSION_ES6); + root = cx.initStandardObjects(); + try (FileReader rdr = new FileReader("testsrc/assert.js")) { + cx.evaluateReader(root, rdr, "assert.js", 1, null); + } } - } - - @After - public void cleanup() { - Context.exit(); - } - - @Test - public void testNativeFunction() { - cx.evaluateString(root, "function foo() { return 'Hello'; }\n" - + "assertEquals(foo.name, 'foo');\n" - + "assertEquals(foo.length, 0);\n" - + "assertEquals(typeof foo, 'function');\n" - + "assertEquals(foo(), 'Hello');\n" - + "assertTrue(foo.toString().length > 0);\n" - + "assertTrue(foo.prototype !== undefined);\n" - + "assertTrue(foo.prototype.toString !== undefined);", - "test", 1, null); - } - - @Test - public void testNoArgLambdaFunction() { - LambdaFunction f = new LambdaFunction(root, "foo", 0, - (Context cx, Scriptable scope, Scriptable thisObj, Object[] args) -> { - return "Hello"; - }); - ScriptableObject.putProperty(root, "foo", f); - cx.evaluateString(root, "assertEquals(foo.name, 'foo');\n" - + "assertEquals(foo.length, 0);\n" - + "assertEquals(typeof foo, 'function');\n" - + "assertEquals(foo(), 'Hello');\n" - + "assertTrue(foo.toString().length > 0);\n" - + "assertTrue(foo.prototype.toString !== undefined);", - "test", 1, null); - } - - @Test - public void testConstructLambdaClass() { - TestClass.init(root); - cx.evaluateString(root, "let tc = new TestClass('foo');\n" - + "assertEquals(tc.value, 'foo');\n" - + "tc.value = 'bar';\n" - + "assertEquals(tc.value, 'bar');\n" - + "tc.anotherValue = 123;\n" - + "assertEquals(tc.anotherValue, 123);\n" - + "assertEquals(TestClass.name, 'TestClass');\n" - + "assertEquals(TestClass.length, 1);\n" - + "assertEquals(typeof TestClass, 'function');\n" - + "assertTrue(tc instanceof TestClass);\n", - "test", 1, null); - } - - @Test - public void testNativePrototypeFunctions() { - cx.evaluateString(root, - "function TestClass(v) { this.value = v; }\n" - + "TestClass.prototype.appendToValue = function(x) { return this.value + x; }\n" - + "let tc = new TestClass('foo');\n" - + "assertEquals(tc.value, 'foo');\n" - + "assertEquals(tc.appendToValue('bar'), 'foobar');\n" - + "tc.value = 'x';\n" - + "assertEquals(tc.appendToValue('x'), 'xx');\n" - + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n" - + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');", - "test", 1, null); - } - - @Test - public void testLambdaPrototypeFunctions() { - TestClass.init(root); - cx.evaluateString(root, "let tc = new TestClass('foo');\n" - + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');\n" - + "assertEquals(tc.value, 'foo');\n" - + "assertEquals(tc.appendToValue('bar', 'baz'), 'foobarbaz');\n" - + "tc.value = 'x';\n" - + "assertEquals(tc.appendToValue('x'), 'xx');\n" - + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n", - "test", 1, null); - } - - @Test - public void testLambdaPrototypeFunctionNotFound() { - TestClass.init(root); - assertThrows(RhinoException.class, () -> { - cx.evaluateString(root, "let tc = new TestClass('foo');\n" - + "tc.notFound();", - "test", 1, null); - }); - } - - @Test - public void testLambdaPrototypeFunctionInvalidThis() { - TestClass.init(root); - cx.evaluateString(root, "let tc = new TestClass();\n" - + "assertThrows(function() { tc.appendToValue.call(null, 'invalid'); }, TypeError);\n" - + "assertThrows(function() { tc.appendToValue.call(undefined, 'invalid'); }, TypeError);\n" - + "assertThrows(function() { tc.appendToValue.call({}, 'invalid'); }, TypeError);\n", - "test", 1, null); - } - - @Test - public void testLambdaConstructorFunctions() { - TestClass.init(root); - cx.evaluateString(root, "assertEquals(TestClass.sayHello('World'), 'Hello, World!');\n" - + "assertEquals(TestClass.sayHello.name, 'sayHello');\n" - + "assertEquals(TestClass.sayHello.length, 1);\n" - + "assertEquals(typeof TestClass.sayHello, 'function');", - "test", 1, null); - } - - @Test - public void testLambdaConstructorValues() { - TestClass.init(root); - cx.evaluateString(root, "let tc = new TestClass();\n" - + "assertEquals(tc.protoValue, 123);\n" - + "assertEquals(tc[Symbol.species], 456);\n", - "test", 1, null); - } - - private static class TestClass - extends ScriptableObject { - - private String instanceVal; - - public static void init(Scriptable scope) { - LambdaConstructor constructor = new LambdaConstructor(scope, "TestClass", 1, - (Context cx, Scriptable s, Object[] args) -> { - TestClass tc = new TestClass(); - if (args.length > 0) { - tc.instanceVal = ScriptRuntime.toString(args[0]); - } - return tc; - }); - constructor.defineConstructorMethod(scope, "sayHello", 1, - (Context cx, Scriptable s, Scriptable thisObj, Object[] args) -> TestClass.sayHello(args)); - constructor.definePrototypeMethod(scope, "appendToValue", 1, - (Context cx, Scriptable s, Scriptable thisObj, Object[] args) -> { - TestClass self = LambdaConstructor.convertThisObject(thisObj, TestClass.class); - return self.appendToValue(args); - }); - constructor.definePrototypeProperty("protoValue", 123, 0); - constructor.definePrototypeProperty(SymbolKey.SPECIES, 456, 0); - ScriptableObject.defineProperty(scope, "TestClass", constructor, PERMANENT); + + @After + public void cleanup() { + Context.exit(); + } + + @Test + public void testNativeFunction() { + cx.evaluateString( + root, + "function foo() { return 'Hello'; }\n" + + "assertEquals(foo.name, 'foo');\n" + + "assertEquals(foo.length, 0);\n" + + "assertEquals(typeof foo, 'function');\n" + + "assertEquals(foo(), 'Hello');\n" + + "assertTrue(foo.toString().length > 0);\n" + + "assertTrue(foo.prototype !== undefined);\n" + + "assertTrue(foo.prototype.toString !== undefined);", + "test", + 1, + null); + } + + @Test + public void testNoArgLambdaFunction() { + LambdaFunction f = + new LambdaFunction( + root, + "foo", + 0, + (Context cx, Scriptable scope, Scriptable thisObj, Object[] args) -> { + return "Hello"; + }); + ScriptableObject.putProperty(root, "foo", f); + cx.evaluateString( + root, + "assertEquals(foo.name, 'foo');\n" + + "assertEquals(foo.length, 0);\n" + + "assertEquals(typeof foo, 'function');\n" + + "assertEquals(foo(), 'Hello');\n" + + "assertTrue(foo.toString().length > 0);\n" + + "assertTrue(foo.prototype.toString !== undefined);", + "test", + 1, + null); + } + + @Test + public void testConstructLambdaClass() { + TestClass.init(root); + cx.evaluateString( + root, + "let tc = new TestClass('foo');\n" + + "assertEquals(tc.value, 'foo');\n" + + "tc.value = 'bar';\n" + + "assertEquals(tc.value, 'bar');\n" + + "tc.anotherValue = 123;\n" + + "assertEquals(tc.anotherValue, 123);\n" + + "assertEquals(TestClass.name, 'TestClass');\n" + + "assertEquals(TestClass.length, 1);\n" + + "assertEquals(typeof TestClass, 'function');\n" + + "assertTrue(tc instanceof TestClass);\n", + "test", + 1, + null); } - @Override - public String getClassName() { - return "TestClass"; + @Test + public void testNativePrototypeFunctions() { + cx.evaluateString( + root, + "function TestClass(v) { this.value = v; }\n" + + "TestClass.prototype.appendToValue = function(x) { return this.value + x; }\n" + + "let tc = new TestClass('foo');\n" + + "assertEquals(tc.value, 'foo');\n" + + "assertEquals(tc.appendToValue('bar'), 'foobar');\n" + + "tc.value = 'x';\n" + + "assertEquals(tc.appendToValue('x'), 'xx');\n" + + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n" + + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');", + "test", + 1, + null); } - @Override - public Object get(String name, Scriptable start) { - if ("value".equals(name)) { - return instanceVal; - } - return super.get(name, start); + @Test + public void testLambdaPrototypeFunctions() { + TestClass.init(root); + cx.evaluateString( + root, + "let tc = new TestClass('foo');\n" + + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');\n" + + "assertEquals(tc.value, 'foo');\n" + + "assertEquals(tc.appendToValue('bar', 'baz'), 'foobarbaz');\n" + + "tc.value = 'x';\n" + + "assertEquals(tc.appendToValue('x'), 'xx');\n" + + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n", + "test", + 1, + null); } - @Override - public boolean has(String name, Scriptable start) { - if ("value".equals(name)) { - return true; - } - return super.has(name, start); + @Test + public void testLambdaPrototypeFunctionNotFound() { + TestClass.init(root); + assertThrows( + RhinoException.class, + () -> { + cx.evaluateString( + root, + "let tc = new TestClass('foo');\n" + "tc.notFound();", + "test", + 1, + null); + }); } - @Override - public void put(String name, Scriptable start, Object value) { - if ("value".equals(name)) { - instanceVal = ScriptRuntime.toString(value); - } else { - super.put(name, start, value); - } + @Test + public void testLambdaPrototypeFunctionInvalidThis() { + TestClass.init(root); + cx.evaluateString( + root, + "let tc = new TestClass();\n" + + "assertThrows(function() { tc.appendToValue.call(null, 'invalid'); }, TypeError);\n" + + "assertThrows(function() { tc.appendToValue.call(undefined, 'invalid'); }, TypeError);\n" + + "assertThrows(function() { tc.appendToValue.call({}, 'invalid'); }, TypeError);\n", + "test", + 1, + null); } - private Object appendToValue(Object[] args) { - StringBuilder sb = new StringBuilder(instanceVal); - for (Object arg : args) { - sb.append(ScriptRuntime.toString(arg)); - } - return sb.toString(); + @Test + public void testLambdaConstructorFunctions() { + TestClass.init(root); + cx.evaluateString( + root, + "assertEquals(TestClass.sayHello('World'), 'Hello, World!');\n" + + "assertEquals(TestClass.sayHello.name, 'sayHello');\n" + + "assertEquals(TestClass.sayHello.length, 1);\n" + + "assertEquals(typeof TestClass.sayHello, 'function');", + "test", + 1, + null); } - private static Object sayHello(Object[] args) { - if (args.length != 1) { - throw ScriptRuntime.typeError("Expected an argument"); - } - return "Hello, " + ScriptRuntime.toString(args[0]) + '!'; + @Test + public void testLambdaConstructorValues() { + TestClass.init(root); + cx.evaluateString( + root, + "let tc = new TestClass();\n" + + "assertEquals(tc.protoValue, 123);\n" + + "assertEquals(tc[Symbol.species], 456);\n", + "test", + 1, + null); + } + + private static class TestClass extends ScriptableObject { + + private String instanceVal; + + public static void init(Scriptable scope) { + LambdaConstructor constructor = + new LambdaConstructor( + scope, + "TestClass", + 1, + (Context cx, Scriptable s, Object[] args) -> { + TestClass tc = new TestClass(); + if (args.length > 0) { + tc.instanceVal = ScriptRuntime.toString(args[0]); + } + return tc; + }); + constructor.defineConstructorMethod( + scope, + "sayHello", + 1, + (Context cx, Scriptable s, Scriptable thisObj, Object[] args) -> + TestClass.sayHello(args)); + constructor.definePrototypeMethod( + scope, + "appendToValue", + 1, + (Context cx, Scriptable s, Scriptable thisObj, Object[] args) -> { + TestClass self = + LambdaConstructor.convertThisObject(thisObj, TestClass.class); + return self.appendToValue(args); + }); + constructor.definePrototypeProperty("protoValue", 123, 0); + constructor.definePrototypeProperty(SymbolKey.SPECIES, 456, 0); + ScriptableObject.defineProperty(scope, "TestClass", constructor, PERMANENT); + } + + @Override + public String getClassName() { + return "TestClass"; + } + + @Override + public Object get(String name, Scriptable start) { + if ("value".equals(name)) { + return instanceVal; + } + return super.get(name, start); + } + + @Override + public boolean has(String name, Scriptable start) { + if ("value".equals(name)) { + return true; + } + return super.has(name, start); + } + + @Override + public void put(String name, Scriptable start, Object value) { + if ("value".equals(name)) { + instanceVal = ScriptRuntime.toString(value); + } else { + super.put(name, start, value); + } + } + + private Object appendToValue(Object[] args) { + StringBuilder sb = new StringBuilder(instanceVal); + for (Object arg : args) { + sb.append(ScriptRuntime.toString(arg)); + } + return sb.toString(); + } + + private static Object sayHello(Object[] args) { + if (args.length != 1) { + throw ScriptRuntime.typeError("Expected an argument"); + } + return "Hello, " + ScriptRuntime.toString(args[0]) + '!'; + } } - } } From 287ccfb03f7fffae92d57eafe35e427974f4b100 Mon Sep 17 00:00:00 2001 From: Gregory Brail Date: Thu, 15 Apr 2021 17:53:05 -0700 Subject: [PATCH 3/3] Improve test coverage for lambda constructors and functions --- .../javascript/tests/LambdaFunctionTest.java | 121 ++++++++++-------- 1 file changed, 67 insertions(+), 54 deletions(-) diff --git a/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java b/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java index 5d3f810337..2021f61ca5 100644 --- a/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java +++ b/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java @@ -36,10 +36,13 @@ public void cleanup() { Context.exit(); } + private void eval(String source) { + cx.evaluateString(root, source, "test.js", 1, null); + } + @Test public void testNativeFunction() { - cx.evaluateString( - root, + eval( "function foo() { return 'Hello'; }\n" + "assertEquals(foo.name, 'foo');\n" + "assertEquals(foo.length, 0);\n" @@ -47,10 +50,7 @@ public void testNativeFunction() { + "assertEquals(foo(), 'Hello');\n" + "assertTrue(foo.toString().length > 0);\n" + "assertTrue(foo.prototype !== undefined);\n" - + "assertTrue(foo.prototype.toString !== undefined);", - "test", - 1, - null); + + "assertTrue(foo.prototype.toString !== undefined);"); } @Test @@ -64,24 +64,19 @@ public void testNoArgLambdaFunction() { return "Hello"; }); ScriptableObject.putProperty(root, "foo", f); - cx.evaluateString( - root, + eval( "assertEquals(foo.name, 'foo');\n" + "assertEquals(foo.length, 0);\n" + "assertEquals(typeof foo, 'function');\n" + "assertEquals(foo(), 'Hello');\n" + "assertTrue(foo.toString().length > 0);\n" - + "assertTrue(foo.prototype.toString !== undefined);", - "test", - 1, - null); + + "assertTrue(foo.prototype.toString !== undefined);"); } @Test public void testConstructLambdaClass() { TestClass.init(root); - cx.evaluateString( - root, + eval( "let tc = new TestClass('foo');\n" + "assertEquals(tc.value, 'foo');\n" + "tc.value = 'bar';\n" @@ -91,16 +86,12 @@ public void testConstructLambdaClass() { + "assertEquals(TestClass.name, 'TestClass');\n" + "assertEquals(TestClass.length, 1);\n" + "assertEquals(typeof TestClass, 'function');\n" - + "assertTrue(tc instanceof TestClass);\n", - "test", - 1, - null); + + "assertTrue(tc instanceof TestClass);\n"); } @Test public void testNativePrototypeFunctions() { - cx.evaluateString( - root, + eval( "function TestClass(v) { this.value = v; }\n" + "TestClass.prototype.appendToValue = function(x) { return this.value + x; }\n" + "let tc = new TestClass('foo');\n" @@ -109,27 +100,20 @@ public void testNativePrototypeFunctions() { + "tc.value = 'x';\n" + "assertEquals(tc.appendToValue('x'), 'xx');\n" + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n" - + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');", - "test", - 1, - null); + + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');"); } @Test public void testLambdaPrototypeFunctions() { TestClass.init(root); - cx.evaluateString( - root, + eval( "let tc = new TestClass('foo');\n" + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');\n" + "assertEquals(tc.value, 'foo');\n" + "assertEquals(tc.appendToValue('bar', 'baz'), 'foobarbaz');\n" + "tc.value = 'x';\n" + "assertEquals(tc.appendToValue('x'), 'xx');\n" - + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n", - "test", - 1, - null); + + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n"); } @Test @@ -138,54 +122,83 @@ public void testLambdaPrototypeFunctionNotFound() { assertThrows( RhinoException.class, () -> { - cx.evaluateString( - root, - "let tc = new TestClass('foo');\n" + "tc.notFound();", - "test", - 1, - null); + eval("let tc = new TestClass('foo');\n" + "tc.notFound();"); }); } @Test public void testLambdaPrototypeFunctionInvalidThis() { TestClass.init(root); - cx.evaluateString( - root, + eval( "let tc = new TestClass();\n" + "assertThrows(function() { tc.appendToValue.call(null, 'invalid'); }, TypeError);\n" + "assertThrows(function() { tc.appendToValue.call(undefined, 'invalid'); }, TypeError);\n" - + "assertThrows(function() { tc.appendToValue.call({}, 'invalid'); }, TypeError);\n", - "test", - 1, - null); + + "assertThrows(function() { tc.appendToValue.call({}, 'invalid'); }, TypeError);\n"); } @Test public void testLambdaConstructorFunctions() { TestClass.init(root); - cx.evaluateString( - root, + eval( "assertEquals(TestClass.sayHello('World'), 'Hello, World!');\n" + "assertEquals(TestClass.sayHello.name, 'sayHello');\n" + "assertEquals(TestClass.sayHello.length, 1);\n" - + "assertEquals(typeof TestClass.sayHello, 'function');", - "test", - 1, - null); + + "assertEquals(typeof TestClass.sayHello, 'function');"); } @Test public void testLambdaConstructorValues() { TestClass.init(root); - cx.evaluateString( - root, + eval( "let tc = new TestClass();\n" + "assertEquals(tc.protoValue, 123);\n" - + "assertEquals(tc[Symbol.species], 456);\n", - "test", - 1, - null); + + "assertEquals(tc[Symbol.species], 456);\n"); + } + + @Test + public void testLambdaConstructorNewOnly() { + LambdaConstructor constructor = + new LambdaConstructor( + root, + "NewOnly", + 0, + LambdaConstructor.CONSTRUCTOR_NEW, + (Context cx, Scriptable scope, Object[] args) -> cx.newObject(scope)); + ScriptableObject.defineProperty(root, "NewOnly", constructor, 0); + eval( + "let o = new NewOnly();\n" + + "assertEquals('object', typeof o);\n" + + "assertThrows(() => { NewOnly(); }, TypeError);"); + } + + @Test + public void testLambdaConstructorFunctionOnly() { + LambdaConstructor constructor = + new LambdaConstructor( + root, + "NewOnly", + 0, + LambdaConstructor.CONSTRUCTOR_FUNCTION, + (Context cx, Scriptable scope, Object[] args) -> cx.newObject(scope)); + ScriptableObject.defineProperty(root, "NewOnly", constructor, 0); + eval( + "let o = NewOnly();\n" + + "assertEquals('object', typeof o);\n" + + "assertThrows(() => { new NewOnly(); }, TypeError);"); + } + + @Test + public void testLambdaFunctionNoNew() { + LambdaFunction func = + new LambdaFunction( + root, + 0, + (Context cx, Scriptable scope, Scriptable thisObj, Object[] args) -> true); + ScriptableObject.defineProperty(root, "noNewFunc", func, 0); + eval( + "let o = noNewFunc();\n" + + "assertEquals(true, o);\n" + + "assertThrows(() => { new noNewFunc(); }, TypeError)"); } private static class TestClass extends ScriptableObject {