diff options
author | davidot <david.tuin@gmail.com> | 2021-08-09 15:21:15 +0200 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2021-08-09 17:33:14 +0100 |
commit | e1573991a3aff3d2c32f29aaa15793b78b9fb474 (patch) | |
tree | fb367e9e32beef5014d280240b69698ebd024617 /Userland/Libraries | |
parent | 151447bdf7207b4b5cd85708f579e5c3808fe0d8 (diff) | |
download | serenity-e1573991a3aff3d2c32f29aaa15793b78b9fb474.zip |
LibJS: Fix this values in arrow functions
Also added a large this value test (and strict variant) to ensure this
values have no regressions.
Diffstat (limited to 'Userland/Libraries')
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/OrdinaryFunctionObject.cpp | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Runtime/VM.cpp | 22 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Tests/this-value-strict.js | 424 | ||||
-rw-r--r-- | Userland/Libraries/LibJS/Tests/this-value.js | 446 |
4 files changed, 889 insertions, 4 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/OrdinaryFunctionObject.cpp b/Userland/Libraries/LibJS/Runtime/OrdinaryFunctionObject.cpp index 9fbff65da7..0f1ffc0b72 100644 --- a/Userland/Libraries/LibJS/Runtime/OrdinaryFunctionObject.cpp +++ b/Userland/Libraries/LibJS/Runtime/OrdinaryFunctionObject.cpp @@ -132,6 +132,7 @@ FunctionEnvironment* OrdinaryFunctionObject::create_environment(FunctionObject& auto* environment = heap().allocate<FunctionEnvironment>(global_object(), m_environment, variables); environment->set_function_object(function_being_invoked); if (m_is_arrow_function) { + environment->set_this_binding_status(FunctionEnvironment::ThisBindingStatus::Lexical); if (is<FunctionEnvironment>(m_environment)) environment->set_new_target(static_cast<FunctionEnvironment*>(m_environment)->new_target()); } diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index bcc012411f..8e993bcd7f 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -11,6 +11,7 @@ #include <LibJS/Interpreter.h> #include <LibJS/Runtime/AbstractOperations.h> #include <LibJS/Runtime/Array.h> +#include <LibJS/Runtime/BoundFunction.h> #include <LibJS/Runtime/Error.h> #include <LibJS/Runtime/FinalizationRegistry.h> #include <LibJS/Runtime/FunctionEnvironment.h> @@ -471,7 +472,7 @@ Value VM::construct(FunctionObject& function, FunctionObject& new_target, Option if (auto* environment = callee_context.lexical_environment) { auto& function_environment = verify_cast<FunctionEnvironment>(*environment); function_environment.set_new_target(&new_target); - if (!this_argument.is_empty()) { + if (!this_argument.is_empty() && function_environment.this_binding_status() != FunctionEnvironment::ThisBindingStatus::Lexical) { function_environment.bind_this_value(global_object, this_argument); if (exception()) return {}; @@ -603,10 +604,9 @@ void VM::ordinary_call_bind_this(FunctionObject& function, ExecutionContext& cal auto* local_environment = callee_context.lexical_environment; auto& function_environment = verify_cast<FunctionEnvironment>(*local_environment); - // This is not completely as the spec describes it however without this stuff breaks - // (Could be related to the note at https://tc39.es/ecma262/#sec-runtime-semantics-instantiatearrowfunctionexpression ) + // This almost as the spec describes it however we sometimes don't have callee_realm when dealing + // with proxies and arrow functions however this does seemingly achieve spec like behavior. if (!callee_realm || this_mode == FunctionObject::ThisMode::Lexical) { - function_environment.bind_this_value(function.global_object(), callee_context.this_value); return; } @@ -629,6 +629,20 @@ Value VM::call_internal(FunctionObject& function, Value this_value, Optional<Mar VERIFY(!exception()); VERIFY(!this_value.is_empty()); + if (is<BoundFunction>(function)) { + auto& bound_function = static_cast<BoundFunction&>(function); + auto bound_arguments = bound_function.bound_arguments(); + if (arguments.has_value()) + bound_arguments.extend(*arguments); + + MarkedValueList with_bound_arguments { heap() }; + with_bound_arguments.extend(bound_function.bound_arguments()); + if (arguments.has_value()) + with_bound_arguments.extend(*arguments); + + return call_internal(bound_function.target_function(), bound_function.bound_this(), move(with_bound_arguments)); + } + ExecutionContext callee_context; prepare_for_ordinary_call(function, callee_context, js_undefined()); if (exception()) diff --git a/Userland/Libraries/LibJS/Tests/this-value-strict.js b/Userland/Libraries/LibJS/Tests/this-value-strict.js new file mode 100644 index 0000000000..8d9bf587aa --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/this-value-strict.js @@ -0,0 +1,424 @@ +"use strict"; + +// Note the globalThisValue and globalObject do not need to be the same. +const globalThisValue = this; +const globalObject = (0, eval)("this"); + +// These tests are done in global state to ensure that is possible +const globalArrow = () => { + expect(this).toBe(globalThisValue); + return this; +}; + +function globalFunction() { + expect(this).toBe(undefined); + + expect(globalArrow()).toBe(globalThisValue); + + const arrowInGlobalFunction = () => this; + expect(arrowInGlobalFunction()).toBe(undefined); + + return arrowInGlobalFunction; +} + +expect(globalArrow()).toBe(globalThisValue); +expect(globalFunction()()).toBe(undefined); + +const arrowFromGlobalFunction = globalFunction(); + +const customThisValue = { + isCustomThis: true, + variant: 0, +}; + +const otherCustomThisValue = { + isCustomThis: true, + variant: 1, +}; + +describe("describe with arrow function", () => { + expect(this).toBe(globalThisValue); + + test("nested test with normal function should get global object", function () { + expect(this).toBe(undefined); + }); + + test("nested test with arrow function should get same this value as enclosing function", () => { + expect(this).toBe(globalThisValue); + }); +}); + +describe("describe with normal function", function () { + expect(this).toBe(undefined); + test("nested test with normal function should get global object", function () { + expect(this).toBe(undefined); + }); + + test("nested test with arrow function should get same this value as enclosing function", () => { + expect(this).toBe(undefined); + }); +}); + +describe("basic behavior", () => { + expect(this).toBe(globalThisValue); + + expect(arrowFromGlobalFunction()).toBeUndefined(); + + expect(customThisValue).not.toBe(otherCustomThisValue); + + test("binding arrow function does not influence this value", () => { + const boundGlobalArrow = globalArrow.bind({ shouldNotBeHere: true }); + + expect(boundGlobalArrow()).toBe(globalThisValue); + }); + + function functionInArrow() { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return this; + } + + function functionWithArrow() { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return () => { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return this; + }; + } + + function strictFunction() { + "use strict"; + return this; + } + + test("functions get globalObject as this value", () => { + expect(functionInArrow()).toBeUndefined(); + expect(functionWithArrow()()).toBeUndefined(); + }); + + test("strict functions get undefined as this value", () => { + expect(strictFunction()).toBeUndefined(); + }); + + test("bound function gets overwritten this value", () => { + const boundFunction = functionInArrow.bind(customThisValue); + expect(boundFunction()).toBe(customThisValue); + + const boundFunctionWithArrow = functionWithArrow.bind(customThisValue); + expect(boundFunctionWithArrow()()).toBe(customThisValue); + + // However we cannot bind the arrow function itself + const failingArrowBound = boundFunctionWithArrow().bind(otherCustomThisValue); + expect(failingArrowBound()).toBe(customThisValue); + + const boundStrictFunction = strictFunction.bind(customThisValue); + expect(boundStrictFunction()).toBe(customThisValue); + }); +}); + +describe("functions on created objects", () => { + const obj = { + func: function () { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return this; + }, + + funcWithArrow: function () { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return () => this; + }, + + arrow: () => { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return this; + }, + otherProperty: "yes", + }; + + test("function get this value of associated object", () => { + expect(obj.func()).toBe(obj); + }); + + test("arrow function on object get above this value", () => { + expect(obj.arrow()).toBe(globalThisValue); + }); + + test("arrow function from normal function from object has object as this value", () => { + expect(obj.funcWithArrow()()).toBe(obj); + }); + + test("bound overwrites value of normal object function", () => { + const boundFunction = obj.func.bind(customThisValue); + expect(boundFunction()).toBe(customThisValue); + + const boundFunctionWithArrow = obj.funcWithArrow.bind(customThisValue); + expect(boundFunctionWithArrow()()).toBe(customThisValue); + + const boundArrowFunction = obj.arrow.bind(customThisValue); + expect(boundArrowFunction()).toBe(globalThisValue); + }); + + test("also works for object defined in function", () => { + (function () { + expect(arrowFromGlobalFunction()).toBeUndefined(); + + // It is bound below + expect(this).toBe(customThisValue); + + const obj2 = { + func: function () { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return this; + }, + + arrow: () => { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return this; + }, + otherProperty: "also", + }; + + expect(obj2.func()).toBe(obj2); + expect(obj2.arrow()).toBe(customThisValue); + }.bind(customThisValue)()); + }); +}); + +describe("behavior with classes", () => { + class Basic { + constructor(value) { + expect(this).toBeInstanceOf(Basic); + this.arrowFunctionInClass = () => { + return this; + }; + + this.value = value; + + expect(arrowFromGlobalFunction()).toBeUndefined(); + } + + func() { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return this; + } + } + + const basic = new Basic(14); + const basic2 = new Basic(457); + + expect(basic).not.toBe(basic2); + + test("calling functions on class should give instance as this value", () => { + expect(basic.func()).toBe(basic); + expect(basic2.func()).toBe(basic2); + }); + + test("calling arrow function created in constructor should give instance as this value", () => { + expect(basic.arrowFunctionInClass()).toBe(basic); + expect(basic2.arrowFunctionInClass()).toBe(basic2); + }); + + test("can bind function in class", () => { + const boundFunction = basic.func.bind(customThisValue); + expect(boundFunction()).toBe(customThisValue); + + const boundFunction2 = basic2.func.bind(otherCustomThisValue); + expect(boundFunction2()).toBe(otherCustomThisValue); + }); +}); + +describe("derived classes behavior", () => { + class Base { + baseFunction() { + expect(arrowFromGlobalFunction()).toBeUndefined(); + + return this; + } + } + + class Derived extends Base { + constructor(value) { + expect(arrowFromGlobalFunction()).toBeUndefined(); + const arrowMadeBeforeSuper = () => { + expect(this).toBeInstanceOf(Derived); + return this; + }; + super(); + expect(arrowMadeBeforeSuper()).toBe(this); + + this.arrowMadeBeforeSuper = arrowMadeBeforeSuper; + this.arrowMadeAfterSuper = () => { + expect(this).toBeInstanceOf(Derived); + return this; + }; + this.value = value; + } + + derivedFunction() { + expect(arrowFromGlobalFunction()).toBeUndefined(); + return this; + } + } + + test("can create derived with arrow functions using this before super", () => { + const testDerived = new Derived(-89); + expect(testDerived.arrowMadeBeforeSuper()).toBe(testDerived); + expect(testDerived.arrowMadeAfterSuper()).toBe(testDerived); + }); + + test("base and derived functions get correct this values", () => { + const derived = new Derived(12); + + expect(derived.derivedFunction()).toBe(derived); + expect(derived.baseFunction()).toBe(derived); + }); + + test("can bind derived and base functions", () => { + const derived = new Derived(846); + + const boundDerivedFunction = derived.derivedFunction.bind(customThisValue); + expect(boundDerivedFunction()).toBe(customThisValue); + + const boundBaseFunction = derived.baseFunction.bind(otherCustomThisValue); + expect(boundBaseFunction()).toBe(otherCustomThisValue); + }); +}); + +describe("proxy behavior", () => { + test("with no handler it makes no difference", () => { + const globalArrowProxyNoHandler = new Proxy(globalArrow, {}); + expect(globalArrowProxyNoHandler()).toBe(globalThisValue); + }); + + test("proxy around global arrow still gives correct this value", () => { + let lastThisArg = null; + + const handler = { + apply(target, thisArg, argArray) { + expect(target).toBe(globalArrow); + lastThisArg = thisArg; + expect(this).toBe(handler); + + return target(...argArray); + }, + }; + + const globalArrowProxy = new Proxy(globalArrow, handler); + expect(globalArrowProxy()).toBe(globalThisValue); + expect(lastThisArg).toBeUndefined(); + + const boundProxy = globalArrowProxy.bind(customThisValue); + expect(boundProxy()).toBe(globalThisValue); + expect(lastThisArg).toBe(customThisValue); + + expect(globalArrowProxy.call(15)).toBe(globalThisValue); + expect(lastThisArg).toBe(15); + }); +}); + +describe("derived classes which access this before super should fail", () => { + class Base {} + + test("direct access of this should throw reference error", () => { + class IncorrectConstructor extends Base { + constructor() { + this.something = "this will fail"; + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); + + test("access of this via a arrow function", () => { + class IncorrectConstructor extends Base { + constructor() { + const arrow = () => this; + arrow(); + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); + + test("access of this via a eval", () => { + class IncorrectConstructor extends Base { + constructor() { + eval("this.foo = 'bar'"); + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); + + test.skip("access of this via a eval in arrow function", () => { + class IncorrectConstructor extends Base { + constructor() { + const arrow = () => eval("() => this")(); + arrow(); + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); + + test("access of this via arrow function even if bound with something else", () => { + class IncorrectConstructor extends Base { + constructor() { + const arrow = () => this; + const boundArrow = arrow.bind(customThisValue); + boundArrow(); + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); +}); + +describe("in strict mode primitive this values are not converted to objects", () => { + const array = [true, false]; + + // Technically the comma is implementation defined here. (Also for tests below.) + expect(array.toLocaleString()).toBe("true,false"); + + test("directly overwriting toString", () => { + let count = 0; + Boolean.prototype.toString = function () { + count++; + return typeof this; + }; + + expect(array.toLocaleString()).toBe("boolean,boolean"); + expect(count).toBe(2); + }); + + test("overwriting toString with a getter", () => { + let count = 0; + + Object.defineProperty(Boolean.prototype, "toString", { + get() { + count++; + const that = typeof this; + return function () { + return that; + }; + }, + }); + + expect(array.toLocaleString()).toBe("boolean,boolean"); + expect(count).toBe(2); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/this-value.js b/Userland/Libraries/LibJS/Tests/this-value.js new file mode 100644 index 0000000000..73571e9e84 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/this-value.js @@ -0,0 +1,446 @@ +// Note the globalThisValue and globalObject do not need to be the same. +const globalThisValue = this; +const globalObject = (0, eval)("this"); + +// These tests are done in global state to ensure that is possible +const globalArrow = () => { + expect(this).toBe(globalThisValue); + return this; +}; + +function globalFunction() { + expect(this).toBe(globalObject); + + expect(globalArrow()).toBe(globalThisValue); + + const arrowInGlobalFunction = () => this; + expect(arrowInGlobalFunction()).toBe(globalObject); + + return arrowInGlobalFunction; +} + +expect(globalArrow()).toBe(globalThisValue); +expect(globalFunction()()).toBe(globalObject); + +const arrowFromGlobalFunction = globalFunction(); + +const customThisValue = { + isCustomThis: true, + variant: 0, +}; + +const otherCustomThisValue = { + isCustomThis: true, + variant: 1, +}; + +describe("describe with arrow function", () => { + expect(this).toBe(globalThisValue); + + test("nested test with normal function should get global object", function () { + expect(this).toBe(globalObject); + }); + + test("nested test with arrow function should get same this value as enclosing function", () => { + expect(this).toBe(globalThisValue); + }); +}); + +describe("describe with normal function", function () { + expect(this).toBe(globalObject); + test("nested test with normal function should get global object", function () { + expect(this).toBe(globalObject); + }); + + test("nested test with arrow function should get same this value as enclosing function", () => { + expect(this).toBe(globalObject); + }); +}); + +describe("basic behavior", () => { + expect(this).toBe(globalThisValue); + + expect(arrowFromGlobalFunction()).toBe(globalObject); + + expect(customThisValue).not.toBe(otherCustomThisValue); + + test("binding arrow function does not influence this value", () => { + const boundGlobalArrow = globalArrow.bind({ shouldNotBeHere: true }); + + expect(boundGlobalArrow()).toBe(globalThisValue); + }); + + function functionInArrow() { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return this; + } + + function functionWithArrow() { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return () => { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return this; + }; + } + + function strictFunction() { + "use strict"; + return this; + } + + test("functions get globalObject as this value", () => { + expect(functionInArrow()).toBe(globalObject); + expect(functionWithArrow()()).toBe(globalObject); + }); + + test("strict functions get undefined as this value", () => { + expect(strictFunction()).toBeUndefined(); + }); + + test("bound function gets overwritten this value", () => { + const boundFunction = functionInArrow.bind(customThisValue); + expect(boundFunction()).toBe(customThisValue); + + const boundFunctionWithArrow = functionWithArrow.bind(customThisValue); + expect(boundFunctionWithArrow()()).toBe(customThisValue); + + // However we cannot bind the arrow function itself + const failingArrowBound = boundFunctionWithArrow().bind(otherCustomThisValue); + expect(failingArrowBound()).toBe(customThisValue); + + const boundStrictFunction = strictFunction.bind(customThisValue); + expect(boundStrictFunction()).toBe(customThisValue); + }); +}); + +describe("functions on created objects", () => { + const obj = { + func: function () { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return this; + }, + + funcWithArrow: function () { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return () => this; + }, + + arrow: () => { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return this; + }, + otherProperty: "yes", + }; + + test("function get this value of associated object", () => { + expect(obj.func()).toBe(obj); + }); + + test("arrow function on object get above this value", () => { + expect(obj.arrow()).toBe(globalThisValue); + }); + + test("arrow function from normal function from object has object as this value", () => { + expect(obj.funcWithArrow()()).toBe(obj); + }); + + test("bound overwrites value of normal object function", () => { + const boundFunction = obj.func.bind(customThisValue); + expect(boundFunction()).toBe(customThisValue); + + const boundFunctionWithArrow = obj.funcWithArrow.bind(customThisValue); + expect(boundFunctionWithArrow()()).toBe(customThisValue); + + const boundArrowFunction = obj.arrow.bind(customThisValue); + expect(boundArrowFunction()).toBe(globalThisValue); + }); + + test("also works for object defined in function", () => { + (function () { + expect(arrowFromGlobalFunction()).toBe(globalObject); + + // It is bound below + expect(this).toBe(customThisValue); + + const obj2 = { + func: function () { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return this; + }, + + arrow: () => { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return this; + }, + otherProperty: "also", + }; + + expect(obj2.func()).toBe(obj2); + expect(obj2.arrow()).toBe(customThisValue); + }.bind(customThisValue)()); + }); +}); + +describe("behavior with classes", () => { + class Basic { + constructor(value) { + expect(this).toBeInstanceOf(Basic); + this.arrowFunctionInClass = () => { + return this; + }; + + this.value = value; + + expect(arrowFromGlobalFunction()).toBe(globalObject); + } + + func() { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return this; + } + } + + const basic = new Basic(14); + const basic2 = new Basic(457); + + expect(basic).not.toBe(basic2); + + test("calling functions on class should give instance as this value", () => { + expect(basic.func()).toBe(basic); + expect(basic2.func()).toBe(basic2); + }); + + test("calling arrow function created in constructor should give instance as this value", () => { + expect(basic.arrowFunctionInClass()).toBe(basic); + expect(basic2.arrowFunctionInClass()).toBe(basic2); + }); + + test("can bind function in class", () => { + const boundFunction = basic.func.bind(customThisValue); + expect(boundFunction()).toBe(customThisValue); + + const boundFunction2 = basic2.func.bind(otherCustomThisValue); + expect(boundFunction2()).toBe(otherCustomThisValue); + }); +}); + +describe("derived classes behavior", () => { + class Base { + baseFunction() { + expect(arrowFromGlobalFunction()).toBe(globalObject); + + return this; + } + } + + class Derived extends Base { + constructor(value) { + expect(arrowFromGlobalFunction()).toBe(globalObject); + const arrowMadeBeforeSuper = () => { + expect(this).toBeInstanceOf(Derived); + return this; + }; + super(); + expect(arrowMadeBeforeSuper()).toBe(this); + + this.arrowMadeBeforeSuper = arrowMadeBeforeSuper; + this.arrowMadeAfterSuper = () => { + expect(this).toBeInstanceOf(Derived); + return this; + }; + this.value = value; + } + + derivedFunction() { + expect(arrowFromGlobalFunction()).toBe(globalObject); + return this; + } + } + + test("can create derived with arrow functions using this before super", () => { + const testDerived = new Derived(-89); + expect(testDerived.arrowMadeBeforeSuper()).toBe(testDerived); + expect(testDerived.arrowMadeAfterSuper()).toBe(testDerived); + }); + + test("base and derived functions get correct this values", () => { + const derived = new Derived(12); + + expect(derived.derivedFunction()).toBe(derived); + expect(derived.baseFunction()).toBe(derived); + }); + + test("can bind derived and base functions", () => { + const derived = new Derived(846); + + const boundDerivedFunction = derived.derivedFunction.bind(customThisValue); + expect(boundDerivedFunction()).toBe(customThisValue); + + const boundBaseFunction = derived.baseFunction.bind(otherCustomThisValue); + expect(boundBaseFunction()).toBe(otherCustomThisValue); + }); +}); + +describe("proxy behavior", () => { + test("with no handler it makes no difference", () => { + const globalArrowProxyNoHandler = new Proxy(globalArrow, {}); + expect(globalArrowProxyNoHandler()).toBe(globalThisValue); + }); + + test("proxy around global arrow still gives correct this value", () => { + let lastThisArg = null; + + const handler = { + apply(target, thisArg, argArray) { + expect(target).toBe(globalArrow); + lastThisArg = thisArg; + expect(this).toBe(handler); + + return target(...argArray); + }, + }; + + const globalArrowProxy = new Proxy(globalArrow, handler); + expect(globalArrowProxy()).toBe(globalThisValue); + expect(lastThisArg).toBeUndefined(); + + const boundProxy = globalArrowProxy.bind(customThisValue); + expect(boundProxy()).toBe(globalThisValue); + expect(lastThisArg).toBe(customThisValue); + + expect(globalArrowProxy.call(15)).toBe(globalThisValue); + expect(lastThisArg).toBe(15); + }); +}); + +describe("derived classes which access this before super should fail", () => { + class Base {} + + test("direct access of this should throw reference error", () => { + class IncorrectConstructor extends Base { + constructor() { + this.something = "this will fail"; + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); + + test("access of this via a arrow function", () => { + class IncorrectConstructor extends Base { + constructor() { + const arrow = () => this; + arrow(); + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); + + test("access of this via a eval", () => { + class IncorrectConstructor extends Base { + constructor() { + eval("this.foo = 'bar'"); + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); + + test.skip("access of this via a eval in arrow function", () => { + class IncorrectConstructor extends Base { + constructor() { + const arrow = () => eval("() => this")(); + arrow(); + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); + + test("access of this via arrow function even if bound with something else", () => { + class IncorrectConstructor extends Base { + constructor() { + const arrow = () => this; + const boundArrow = arrow.bind(customThisValue); + boundArrow(); + super(); + } + } + + expect(() => { + new IncorrectConstructor(); + }).toThrowWithMessage(ReferenceError, "|this| has not been initialized"); + }); +}); + +describe("with statements", () => { + test("this value is still the global object", () => { + const obj = { haveValue: true, hello: "friends" }; + with (obj) { + expect(this).toBe(globalThisValue); + expect(hello).toBe("friends"); + } + }); + + test("with gets this value form outer scope", () => { + const obj = { haveValue: true, hello: "friends" }; + + function callme() { + with (obj) { + expect(this).toBe(customThisValue); + expect(hello).toBe("friends"); + } + } + + const boundMe = callme.bind(customThisValue); + boundMe(); + }); +}); + +describe("in non strict mode primitive this values are converted to objects", () => { + const array = [true, false]; + + // Technically the comma is implementation defined here. (Also for tests below.) + expect(array.toLocaleString()).toBe("true,false"); + + test("directly overwriting toString", () => { + let count = 0; + Boolean.prototype.toString = function () { + count++; + return typeof this; + }; + + expect(array.toLocaleString()).toBe("object,object"); + expect(count).toBe(2); + }); + + test("overwriting toString with a getter", () => { + let count = 0; + + Object.defineProperty(Boolean.prototype, "toString", { + get() { + count++; + const that = typeof this; + return function () { + return that; + }; + }, + }); + + expect(array.toLocaleString()).toBe("object,object"); + expect(count).toBe(2); + }); +}); |