mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-04-22 04:25:13 +00:00
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.
This commit is contained in:
parent
1ee7e97e24
commit
7d521b7c7c
Notes:
sideshowbarker
2024-07-17 20:38:06 +09:00
Author: https://github.com/linusg Commit: https://github.com/SerenityOS/serenity/commit/7d521b7c7c7 Pull-request: https://github.com/SerenityOS/serenity/pull/12002
3 changed files with 166 additions and 59 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2021, Linus Groh <linusg@serenityos.org>
|
||||
* Copyright (c) 2020-2022, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* 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<TypeError>(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<ECMAScriptFunctionObject>(this_object)) {
|
||||
auto& function = static_cast<ECMAScriptFunctionObject&>(*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<FlyString>()) {
|
||||
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<ECMAScriptFunctionObject*>(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<NativeFunction>(this_object))
|
||||
function_name = static_cast<NativeFunction&>(*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<TypeError>(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<ECMAScriptFunctionObject>(function)) {
|
||||
// a. Return ! CodePointsToString(func.[[SourceText]]).
|
||||
return js_string(vm, static_cast<ECMAScriptFunctionObject&>(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<NativeFunction>(function))
|
||||
return js_string(vm, String::formatted("function {}() {{ [native code] }}", static_cast<NativeFunction&>(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
|
||||
|
|
|
@ -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}");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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] }");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue