From 7d521b7c7c7bce42349cce2ab56991976df99d7b Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Tue, 18 Jan 2022 23:50:53 +0000 Subject: [PATCH] LibJS: Implement Function.prototype.toString() according to the spec That's an old yak :^) No, past me, AST nodes do not need to learn to stringify themselves. This is now massively simplified by using the [[SourceText]] internal slot. Also updates a bunch of tests that are incorrect due to the old implementation not being spec compliant, and add plenty more. --- .../LibJS/Runtime/FunctionPrototype.cpp | 64 +++---- .../LibJS/Tests/builtins/Function/Function.js | 2 +- .../Function/Function.prototype.toString.js | 159 ++++++++++++++++-- 3 files changed, 166 insertions(+), 59 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/FunctionPrototype.cpp b/Userland/Libraries/LibJS/Runtime/FunctionPrototype.cpp index ea37349f92..92ccbf859e 100644 --- a/Userland/Libraries/LibJS/Runtime/FunctionPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/FunctionPrototype.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021, Linus Groh + * Copyright (c) 2020-2022, Linus Groh * * SPDX-License-Identifier: BSD-2-Clause */ @@ -94,50 +94,30 @@ JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::call) // 20.2.3.5 Function.prototype.toString ( ), https://tc39.es/ecma262/#sec-function.prototype.tostring JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::to_string) { - auto* this_object = TRY(vm.this_value(global_object).to_object(global_object)); - if (!this_object->is_function()) - return vm.throw_completion(global_object, ErrorType::NotAnObjectOfType, "Function"); - String function_name; - String function_parameters; - String function_body; + // 1. Let func be the this value. + auto function_value = vm.this_value(global_object); - if (is(this_object)) { - auto& function = static_cast(*this_object); - StringBuilder parameters_builder; - auto first = true; - for (auto& parameter : function.formal_parameters()) { - // FIXME: Also stringify binding patterns. - if (auto* name_ptr = parameter.binding.get_pointer()) { - if (!first) - parameters_builder.append(", "); - first = false; - parameters_builder.append(*name_ptr); - if (parameter.default_value) { - // FIXME: See note below - parameters_builder.append(" = TODO"); - } - } - } - function_name = function.name(); - function_parameters = parameters_builder.build(); - // FIXME: ASTNodes should be able to dump themselves to source strings - something like this: - // auto& body = static_cast(this_object)->body(); - // function_body = body.to_source(); - function_body = " ???"; - } else { - // This is "implementation-defined" - other engines don't include a name for - // ProxyObject and BoundFunction, only NativeFunction - let's do the same here. - if (is(this_object)) - function_name = static_cast(*this_object).name(); - function_body = " [native code]"; + // If func is not a function, let's bail out early. The order of this step is not observable. + if (!function_value.is_function()) { + // 5. Throw a TypeError exception. + return vm.throw_completion(global_object, ErrorType::NotAnObjectOfType, "Function"); } - auto function_source = String::formatted( - "function {}({}) {{\n{}\n}}", - function_name.is_null() ? "" : function_name, - function_parameters.is_null() ? "" : function_parameters, - function_body); - return js_string(vm, function_source); + auto& function = function_value.as_function(); + + // 2. If Type(func) is Object and func has a [[SourceText]] internal slot and func.[[SourceText]] is a sequence of Unicode code points and ! HostHasSourceTextAvailable(func) is true, then + if (is(function)) { + // a. Return ! CodePointsToString(func.[[SourceText]]). + return js_string(vm, static_cast(function).source_text()); + } + + // 3. If func is a built-in function object, return an implementation-defined String source code representation of func. The representation must have the syntax of a NativeFunction. Additionally, if func has an [[InitialName]] internal slot and func.[[InitialName]] is a String, the portion of the returned String that would be matched by NativeFunctionAccessor[opt] PropertyName must be the value of func.[[InitialName]]. + if (is(function)) + return js_string(vm, String::formatted("function {}() {{ [native code] }}", static_cast(function).name())); + + // 4. If Type(func) is Object and IsCallable(func) is true, return an implementation-defined String source code representation of func. The representation must have the syntax of a NativeFunction. + // NOTE: ProxyObject, BoundFunction, WrappedFunction + return js_string(vm, "function () { [native code] }"); } // 20.2.3.6 Function.prototype [ @@hasInstance ] ( V ), https://tc39.es/ecma262/#sec-function.prototype-@@hasinstance diff --git a/Userland/Libraries/LibJS/Tests/builtins/Function/Function.js b/Userland/Libraries/LibJS/Tests/builtins/Function/Function.js index a26e5a53db..2105a633ac 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Function/Function.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Function/Function.js @@ -33,7 +33,7 @@ describe("correct behavior", () => { expect(new Function("-->")()).toBeUndefined(); expect(new Function().name).toBe("anonymous"); - expect(new Function().toString()).toBe("function anonymous() {\n ???\n}"); + expect(new Function().toString()).toBe("function anonymous(\n) {\n\n}"); }); }); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Function/Function.prototype.toString.js b/Userland/Libraries/LibJS/Tests/builtins/Function/Function.prototype.toString.js index 73246ee909..fabeecd5ab 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Function/Function.prototype.toString.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Function/Function.prototype.toString.js @@ -1,17 +1,144 @@ -test("basic functionality", () => { - expect(function () {}.toString()).toBe("function () {\n ???\n}"); - expect(function (foo) {}.toString()).toBe("function (foo) {\n ???\n}"); - expect(function (foo, bar, baz) {}.toString()).toBe("function (foo, bar, baz) {\n ???\n}"); - expect( - function (foo, bar, baz) { - if (foo) { - return baz; - } else if (bar) { - return foo; - } - return bar + 42; - }.toString() - ).toBe("function (foo, bar, baz) {\n ???\n}"); - expect(console.debug.toString()).toBe("function debug() {\n [native code]\n}"); - expect(Function.toString()).toBe("function Function() {\n [native code]\n}"); +describe("correct behavior", () => { + test("length is 0", () => { + expect(Function.prototype.toString).toHaveLength(0); + }); + + test("basic functionality", () => { + expect(function () {}.toString()).toBe("function () {}"); + expect(function (foo) {}.toString()).toBe("function (foo) {}"); + expect(function (foo, bar, baz) {}.toString()).toBe("function (foo, bar, baz) {}"); + // prettier-ignore + expect((/* comment 1 */ function () { /* comment 2 */ } /* comment 3 */).toString()).toBe("function () { /* comment 2 */ }"); + expect(function* () {}.toString()).toBe("function* () {}"); + expect(async function () {}.toString()).toBe("async function () {}"); + expect(async function* () {}.toString()).toBe("async function* () {}"); + expect( + function (foo, bar, baz) { + if (foo) { + return baz; + } else if (bar) { + return foo; + } + return bar + 42; + }.toString() + ).toBe( + `function (foo, bar, baz) { + if (foo) { + return baz; + } else if (bar) { + return foo; + } + return bar + 42; + }` + ); + }); + + test("object method", () => { + expect({ foo() {} }.foo.toString()).toBe("foo() {}"); + expect({ ["foo"]() {} }.foo.toString()).toBe('["foo"]() {}'); + expect({ *foo() {} }.foo.toString()).toBe("*foo() {}"); + expect({ async foo() {} }.foo.toString()).toBe("async foo() {}"); + expect({ async *foo() {} }.foo.toString()).toBe("async *foo() {}"); + expect(Object.getOwnPropertyDescriptor({ get foo() {} }, "foo").get.toString()).toBe( + "get foo() {}" + ); + expect(Object.getOwnPropertyDescriptor({ set foo(x) {} }, "foo").set.toString()).toBe( + "set foo(x) {}" + ); + }); + + test("arrow function", () => { + expect((() => {}).toString()).toBe("() => {}"); + expect((foo => {}).toString()).toBe("foo => {}"); + // prettier-ignore + expect(((foo) => {}).toString()).toBe("(foo) => {}"); + expect(((foo, bar) => {}).toString()).toBe("(foo, bar) => {}"); + expect((() => foo).toString()).toBe("() => foo"); + // prettier-ignore + expect((() => { /* comment */ }).toString()).toBe("() => { /* comment */ }"); + }); + + test("class expression", () => { + expect(class {}.toString()).toBe("class {}"); + expect(class Foo {}.toString()).toBe("class Foo {}"); + // prettier-ignore + expect(class Foo { bar() {} }.toString()).toBe("class Foo { bar() {} }"); + // prettier-ignore + expect((/* comment 1 */ class { /* comment 2 */ } /* comment 3 */).toString()).toBe("class { /* comment 2 */ }"); + + class Bar {} + expect( + class Foo extends Bar { + constructor() { + super(); + } + + a = 1; + #b = 2; + static c = 3; + + /* comment */ + + async *foo() { + return 42; + } + }.toString() + ).toBe( + `class Foo extends Bar { + constructor() { + super(); + } + + a = 1; + #b = 2; + static c = 3; + + /* comment */ + + async *foo() { + return 42; + } + }` + ); + }); + + test("class constructor", () => { + expect(class {}.constructor.toString()).toBe("function Function() { [native code] }"); + // prettier-ignore + expect(class { constructor() {} }.constructor.toString()).toBe("function Function() { [native code] }"); + }); + + // prettier-ignore + test("class method", () => { + expect(new (class { foo() {} })().foo.toString()).toBe("foo() {}"); + expect(new (class { ["foo"]() {} })().foo.toString()).toBe('["foo"]() {}'); + }); + + // prettier-ignore + test("static class method", () => { + expect(class { static foo() {} }.foo.toString()).toBe("foo() {}"); + expect(class { static ["foo"]() {} }.foo.toString()).toBe('["foo"]() {}'); + expect(class { static *foo() {} }.foo.toString()).toBe("*foo() {}"); + expect(class { static async foo() {} }.foo.toString()).toBe("async foo() {}"); + expect(class { static async *foo() {} }.foo.toString()).toBe("async *foo() {}"); + }); + + test("native function", () => { + // Built-in functions + expect(console.debug.toString()).toBe("function debug() { [native code] }"); + expect(Function.toString()).toBe("function Function() { [native code] }"); + + const values = [ + // Callable Proxy + new Proxy(function foo() {}, {}), + // Bound function + function foo() {}.bind(null), + // Wrapped function + new ShadowRealm().evaluate("function foo() {}; foo"), + ]; + for (const fn of values) { + // Inner function name is not exposed + expect(fn.toString()).toBe("function () { [native code] }"); + } + }); });