From 149c1ee20a9438e4b9fac0d5f2186637f703fe3c Mon Sep 17 00:00:00 2001 From: RageKnify Date: Mon, 5 Oct 2020 22:55:34 +0100 Subject: [PATCH 1/3] Feat: Implement Function.prototype.call and test case closes boa-dev/boa#797 --- boa/src/builtins/function/mod.rs | 20 ++++++++++++++++++++ boa/src/builtins/function/tests.rs | 12 ++++++++++++ 2 files changed, 32 insertions(+) diff --git a/boa/src/builtins/function/mod.rs b/boa/src/builtins/function/mod.rs index f7e26bdca8b..4b2ebbc5d38 100644 --- a/boa/src/builtins/function/mod.rs +++ b/boa/src/builtins/function/mod.rs @@ -312,6 +312,25 @@ impl BuiltInFunctionObject { fn prototype(_: &Value, _: &[Value], _: &mut Context) -> Result { Ok(Value::undefined()) } + + /// `Function.prototype.call` + /// + /// The call() method invokes self with the first argument as the `this` value. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-function.prototype.call + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call + fn call(this: &Value, args: &[Value], context: &mut Context) -> Result { + let func = this; + if !func.is_function() { + return context.throw_type_error("TODO: this is not a function"); + } + let this_arg: Value = args.get(0).map_or_else(Value::default, Value::clone); + context.call(func, &this_arg, &args[1..]) + } } impl BuiltIn for BuiltInFunctionObject { @@ -339,6 +358,7 @@ impl BuiltIn for BuiltInFunctionObject { ) .name(Self::NAME) .length(Self::LENGTH) + .method(Self::call, "call", 1) .build(); (Self::NAME, function_object.into(), Self::attribute()) diff --git a/boa/src/builtins/function/tests.rs b/boa/src/builtins/function/tests.rs index 1bcc7f417c3..c28c5cba6ff 100644 --- a/boa/src/builtins/function/tests.rs +++ b/boa/src/builtins/function/tests.rs @@ -114,3 +114,15 @@ fn function_prototype_length() { assert!(value.is_number()); assert_eq!(value.as_number().unwrap(), 0.0); } + +#[test] +fn function_prototype_call() { + let mut engine = Context::new(); + let func = r#" + let e = new Error() + Object.prototype.toString.call(e) + "#; + let value = forward_val(&mut engine, func).unwrap(); + assert!(value.is_string()); + assert_eq!(value.as_string().unwrap(), "[object Error]"); +} From 6e72fadb6de276e28f229301d063a03e7ec6f175 Mon Sep 17 00:00:00 2001 From: RageKnify Date: Mon, 5 Oct 2020 23:58:14 +0100 Subject: [PATCH 2/3] Feat: Finish Function.prototype.call, cleanup and tests --- boa/src/builtins/function/mod.rs | 11 ++++++----- boa/src/builtins/function/tests.rs | 9 +++++++++ boa/src/builtins/object/tests.rs | 17 ++++++++--------- boa/src/builtins/regexp/mod.rs | 7 +++++-- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/boa/src/builtins/function/mod.rs b/boa/src/builtins/function/mod.rs index 4b2ebbc5d38..365b0ffb8a7 100644 --- a/boa/src/builtins/function/mod.rs +++ b/boa/src/builtins/function/mod.rs @@ -324,12 +324,13 @@ impl BuiltInFunctionObject { /// [spec]: https://tc39.es/ecma262/#sec-function.prototype.call /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call fn call(this: &Value, args: &[Value], context: &mut Context) -> Result { - let func = this; - if !func.is_function() { - return context.throw_type_error("TODO: this is not a function"); + if !this.is_function() { + return context.throw_type_error(format!("{} is not a function", this.display())); } - let this_arg: Value = args.get(0).map_or_else(Value::default, Value::clone); - context.call(func, &this_arg, &args[1..]) + let this_arg: Value = args.get(0).cloned().unwrap_or_default(); + // TODO?: 3. Perform PrepareForTailCall + let start = if !args.is_empty() { 1 } else { 0 }; + context.call(this, &this_arg, &args[start..]) } } diff --git a/boa/src/builtins/function/tests.rs b/boa/src/builtins/function/tests.rs index c28c5cba6ff..c8fed271601 100644 --- a/boa/src/builtins/function/tests.rs +++ b/boa/src/builtins/function/tests.rs @@ -125,4 +125,13 @@ fn function_prototype_call() { let value = forward_val(&mut engine, func).unwrap(); assert!(value.is_string()); assert_eq!(value.as_string().unwrap(), "[object Error]"); + + let throw = r#" + let call = Function.prototype.call; + call(call) + "#; + let value = forward_val(&mut engine, throw).unwrap_err(); + assert!(value.is_object()); + let string = value.to_string(&mut engine).unwrap(); + assert!(string.starts_with("TypeError")) } diff --git a/boa/src/builtins/object/tests.rs b/boa/src/builtins/object/tests.rs index 9491c08db7b..bb39add9afc 100644 --- a/boa/src/builtins/object/tests.rs +++ b/boa/src/builtins/object/tests.rs @@ -161,15 +161,14 @@ fn object_to_string() { let o = Object(); "#; eprintln!("{}", forward(&mut ctx, init)); - // TODO: need Function.prototype.call to be implemented - // assert_eq!( - // forward(&mut ctx, "Object.prototype.toString.call(u)"), - // "\"[object Undefined]\"" - // ); - // assert_eq!( - // forward(&mut ctx, "Object.prototype.toString.call(n)"), - // "\"[object Null]\"" - // ); + assert_eq!( + forward(&mut ctx, "Object.prototype.toString.call(u)"), + "\"[object Undefined]\"" + ); + assert_eq!( + forward(&mut ctx, "Object.prototype.toString.call(n)"), + "\"[object Null]\"" + ); assert_eq!(forward(&mut ctx, "a.toString()"), "\"[object Array]\""); assert_eq!(forward(&mut ctx, "f.toString()"), "\"[object Function]\""); assert_eq!(forward(&mut ctx, "e.toString()"), "\"[object Error]\""); diff --git a/boa/src/builtins/regexp/mod.rs b/boa/src/builtins/regexp/mod.rs index 327c0997aea..b9c6c30becf 100644 --- a/boa/src/builtins/regexp/mod.rs +++ b/boa/src/builtins/regexp/mod.rs @@ -438,12 +438,15 @@ impl RegExp { /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.tostring /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/toString #[allow(clippy::wrong_self_convention)] - pub(crate) fn to_string(this: &Value, _: &[Value], _: &mut Context) -> Result { + pub(crate) fn to_string(this: &Value, _: &[Value], context: &mut Context) -> Result { let (body, flags) = if let Some(object) = this.as_object() { let regex = object.as_regexp().unwrap(); (regex.original_source.clone(), regex.flags.clone()) } else { - panic!("object is not an object") + return context.throw_type_error(format!( + "Method RegExp.prototype.toString called on incompatible receiver {}", + this.display() + )); }; Ok(Value::from(format!("/{}/{}", body, flags))) } From 2646951fdb0a3df52c81aed1e5c0abf6ed58d558 Mon Sep 17 00:00:00 2001 From: RageKnify Date: Tue, 6 Oct 2020 00:24:27 +0100 Subject: [PATCH 3/3] Test: Improve testing of Function.prototype.call --- boa/src/builtins/function/tests.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/boa/src/builtins/function/tests.rs b/boa/src/builtins/function/tests.rs index c8fed271601..df8b09efdf7 100644 --- a/boa/src/builtins/function/tests.rs +++ b/boa/src/builtins/function/tests.rs @@ -125,7 +125,11 @@ fn function_prototype_call() { let value = forward_val(&mut engine, func).unwrap(); assert!(value.is_string()); assert_eq!(value.as_string().unwrap(), "[object Error]"); +} +#[test] +fn function_prototype_call_throw() { + let mut engine = Context::new(); let throw = r#" let call = Function.prototype.call; call(call) @@ -135,3 +139,27 @@ fn function_prototype_call() { let string = value.to_string(&mut engine).unwrap(); assert!(string.starts_with("TypeError")) } + +#[test] +fn function_prototype_call_multiple_args() { + let mut engine = Context::new(); + let init = r#" + function f(a, b) { + this.a = a; + this.b = b; + } + let o = {a: 0, b: 0}; + f.call(o, 1, 2); + "#; + forward_val(&mut engine, init).unwrap(); + let boolean = forward_val(&mut engine, "o.a == 1") + .unwrap() + .as_boolean() + .unwrap(); + assert!(boolean); + let boolean = forward_val(&mut engine, "o.b == 2") + .unwrap() + .as_boolean() + .unwrap(); + assert!(boolean); +}