From 16eb1277343baab15c1c0743debac7dbf82af1f9 Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Thu, 10 Jun 2021 23:47:05 +0200 Subject: [PATCH 1/6] implement RegExp.prototype [ @@search ] ( string ) --- boa/src/builtins/regexp/mod.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/boa/src/builtins/regexp/mod.rs b/boa/src/builtins/regexp/mod.rs index c4ac8b7cc04..69d3c7e1240 100644 --- a/boa/src/builtins/regexp/mod.rs +++ b/boa/src/builtins/regexp/mod.rs @@ -120,6 +120,7 @@ impl BuiltIn for RegExp { .method(Self::test, "test", 1) .method(Self::exec, "exec", 1) .method(Self::to_string, "toString", 0) + .method(Self::search, "search", 1) .accessor("global", Some(get_global), None, flag_attributes) .accessor("ignoreCase", Some(get_ignore_case), None, flag_attributes) .accessor("multiline", Some(get_multiline), None, flag_attributes) @@ -714,4 +715,37 @@ impl RegExp { Ok(result) } + + /// `RegExp.prototype[ @@search ]( string )` + /// + /// This method executes a search for a match between a this regular expression and a string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@search + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@search + pub(crate) fn search(this: &Value, args: &[Value], context: &mut Context) -> Result { + let previous_last_index = this.get_field("lastIndex", context)?.to_length(context)?; + if previous_last_index != 0 { + this.set_field("lastIndex", 0, context)?; + } + + let result = Self::exec(this, args, context)?; + + let current_last_index = this.get_field("lastIndex", context)?.to_length(context)?; + if current_last_index != previous_last_index { + this.set_field("lastIndex", previous_last_index, context)?; + } + + if result.is_null() { + Ok(Value::from(-1)) + } else { + Ok(result + .get_property("index") + .map(|p| p.as_data_descriptor().unwrap().value()) + .ok_or_else(|| context.construct_type_error("Could not find property `index`"))?) + } + } } From 7d2466519d8686268f24cbbdc72bc478dae1ca69 Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Fri, 11 Jun 2021 01:56:02 +0200 Subject: [PATCH 2/6] implement String.prototype.search ( regexp ) --- boa/src/builtins/string/mod.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs index 3b6c15271af..8c525c96f02 100644 --- a/boa/src/builtins/string/mod.rs +++ b/boa/src/builtins/string/mod.rs @@ -133,6 +133,7 @@ impl BuiltIn for String { .method(Self::match_all, "matchAll", 1) .method(Self::replace, "replace", 2) .method(Self::iterator, (symbol_iterator, "[Symbol.iterator]"), 0) + .method(Self::search, "search", 1) .build(); (Self::NAME, string_object.into(), Self::attribute()) @@ -1324,6 +1325,29 @@ impl String { RegExp::match_all(&re, this.to_string(context)?.to_string(), context) } + /// `String.prototype.replace( regexp )` + /// + /// The search() method executes a search for a match between a regular expression and this String object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.search + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search + pub(crate) fn search(this: &Value, args: &[Value], context: &mut Context) -> Result { + let this = this.require_object_coercible(context)?; + + let re_value = args.get(0).cloned().unwrap_or_default(); + if !re_value.is_undefined() && !re_value.is_null() { + let re = RegExp::constructor(&Value::from(Object::default()), &[re_value], context)?; + return RegExp::search(&re, &[this.clone()], context); + } + + let re = RegExp::constructor(&Value::from(Object::default()), &[re_value], context)?; + RegExp::search(&re, &[Value::from(this.to_string(context)?)], context) + } + pub(crate) fn iterator(this: &Value, _: &[Value], context: &mut Context) -> Result { StringIterator::create_string_iterator(context, this.clone()) } From 70415b1d956819e7000422bebb0d126c4b9814fe Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Sat, 12 Jun 2021 02:10:35 +0200 Subject: [PATCH 3/6] fix errors and refactor search implementations: * RegExp.prototype [ @@search ] ( string ) * String.prototype.search ( regexp ) --- boa/src/builtins/regexp/mod.rs | 67 ++++++++++++++++++++++++++-------- boa/src/builtins/string/mod.rs | 39 ++++++++++++++++---- 2 files changed, 83 insertions(+), 23 deletions(-) diff --git a/boa/src/builtins/regexp/mod.rs b/boa/src/builtins/regexp/mod.rs index 69d3c7e1240..e1176bbd978 100644 --- a/boa/src/builtins/regexp/mod.rs +++ b/boa/src/builtins/regexp/mod.rs @@ -14,6 +14,7 @@ use crate::{ gc::{empty_trace, Finalize, Trace}, object::{ConstructorBuilder, FunctionBuilder, GcObject, ObjectData, PROTOTYPE}, property::{Attribute, DataDescriptor}, + symbol::WellKnownSymbols, value::{RcString, Value}, BoaProfiler, Context, Result, }; @@ -120,7 +121,11 @@ impl BuiltIn for RegExp { .method(Self::test, "test", 1) .method(Self::exec, "exec", 1) .method(Self::to_string, "toString", 0) - .method(Self::search, "search", 1) + .method( + Self::search, + (WellKnownSymbols::search(), "[Symbol.search]"), + 1, + ) .accessor("global", Some(get_global), None, flag_attributes) .accessor("ignoreCase", Some(get_ignore_case), None, flag_attributes) .accessor("multiline", Some(get_multiline), None, flag_attributes) @@ -727,25 +732,55 @@ impl RegExp { /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@search /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@search pub(crate) fn search(this: &Value, args: &[Value], context: &mut Context) -> Result { - let previous_last_index = this.get_field("lastIndex", context)?.to_length(context)?; - if previous_last_index != 0 { - this.set_field("lastIndex", 0, context)?; - } + // 1. Let rx be the this value. + // 2. If Type(rx) is not Object, throw a TypeError exception. + if let Some(object) = this.as_object() { + if object.is_regexp() { + // 3. Let S be ? ToString(string). + let arg_str = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; - let result = Self::exec(this, args, context)?; + // 4. Let previousLastIndex be ? Get(rx, "lastIndex"). + let previous_last_index = + this.get_field("lastIndex", context)?.to_length(context)?; - let current_last_index = this.get_field("lastIndex", context)?.to_length(context)?; - if current_last_index != previous_last_index { - this.set_field("lastIndex", previous_last_index, context)?; - } + // 5. If SameValue(previousLastIndex, +0𝔽) is false, then + if previous_last_index != 0 { + // a. Perform ? Set(rx, "lastIndex", +0𝔽, true). + this.set_field("lastIndex", 0, context)?; + } + + // 6. Let result be ? RegExpExec(rx, S). + let result = Self::exec(this, &[Value::from(arg_str)], context)?; + + // 7. Let currentLastIndex be ? Get(rx, "lastIndex"). + let current_last_index = + this.get_field("lastIndex", context)?.to_length(context)?; + + // 8. If SameValue(currentLastIndex, previousLastIndex) is false, then + if current_last_index != previous_last_index { + // a. Perform ? Set(rx, "lastIndex", previousLastIndex, true). + this.set_field("lastIndex", previous_last_index, context)?; + } - if result.is_null() { - Ok(Value::from(-1)) + // 9. If result is null, return -1𝔽. + // 10. Return ? Get(result, "index"). + if result.is_null() { + Ok(Value::from(-1)) + } else { + result.get_field("index", context).map_err(|_| { + context.construct_type_error("Could not find property `index`") + }) + } + } else { + context + .throw_type_error("RegExp.prototype.search method called on incompatible value") + } } else { - Ok(result - .get_property("index") - .map(|p| p.as_data_descriptor().unwrap().value()) - .ok_or_else(|| context.construct_type_error("Could not find property `index`"))?) + context.throw_type_error("RegExp.prototype.search method called on incompatible value") } } } diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs index 8c525c96f02..7f9fac4dea3 100644 --- a/boa/src/builtins/string/mod.rs +++ b/boa/src/builtins/string/mod.rs @@ -1325,7 +1325,7 @@ impl String { RegExp::match_all(&re, this.to_string(context)?.to_string(), context) } - /// `String.prototype.replace( regexp )` + /// `String.prototype.search( regexp )` /// /// The search() method executes a search for a match between a regular expression and this String object. /// @@ -1336,16 +1336,41 @@ impl String { /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.search /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search pub(crate) fn search(this: &Value, args: &[Value], context: &mut Context) -> Result { + // 1. Let O be ? RequireObjectCoercible(this value). let this = this.require_object_coercible(context)?; - let re_value = args.get(0).cloned().unwrap_or_default(); - if !re_value.is_undefined() && !re_value.is_null() { - let re = RegExp::constructor(&Value::from(Object::default()), &[re_value], context)?; - return RegExp::search(&re, &[this.clone()], context); + // 2. If regexp is neither undefined nor null, then + let regexp = args.get(0).cloned().unwrap_or_default(); + if !regexp.is_null_or_undefined() { + // a. Let searcher be ? GetMethod(regexp, @@search). + // b. If searcher is not undefined, then + if let Some(searcher) = regexp + .to_object(context)? + .get_method(context, WellKnownSymbols::search())? + { + // i. Return ? Call(searcher, regexp, « O »). + return searcher.call(®exp, &[this.clone()], context); + } + + //let re = RegExp::constructor(&Value::from(Object::default()), &[regexp], context)?; + //return RegExp::search(&re, &[this.clone()], context); } - let re = RegExp::constructor(&Value::from(Object::default()), &[re_value], context)?; - RegExp::search(&re, &[Value::from(this.to_string(context)?)], context) + // 3. Let string be ? ToString(O). + let s = this.to_string(context)?; + + // 4. Let rx be ? RegExpCreate(regexp, undefined). + let rx = RegExp::constructor(&Value::from(Object::default()), &[regexp], context)?; + + // 5. Return ? Invoke(rx, @@search, « string »). + if let Some(searcher) = rx + .to_object(context)? + .get_method(context, WellKnownSymbols::search())? + { + searcher.call(&rx, &[Value::from(s)], context) + } else { + context.throw_type_error("regexp[Symbol.search] is not a function") + } } pub(crate) fn iterator(this: &Value, _: &[Value], context: &mut Context) -> Result { From 4e94108ddd7bdcd5c567cac3bf5302dcbdccc1c7 Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Sat, 12 Jun 2021 02:11:22 +0200 Subject: [PATCH 4/6] add basic tests for search implementations: * RegExp.prototype [ @@search ] ( string ) * String.prototype.search ( regexp ) --- boa/src/builtins/regexp/tests.rs | 11 +++++++++++ boa/src/builtins/string/tests.rs | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/boa/src/builtins/regexp/tests.rs b/boa/src/builtins/regexp/tests.rs index a9d014bb87b..56f4be43694 100644 --- a/boa/src/builtins/regexp/tests.rs +++ b/boa/src/builtins/regexp/tests.rs @@ -107,3 +107,14 @@ fn no_panic_on_invalid_character_escape() { // The line below should not cause Boa to panic forward(&mut context, r"const a = /,\;/"); } + +#[test] +fn search() { + let mut context = Context::new(); + + assert_eq!(forward(&mut context, "/a/[Symbol.search](\"a\")"), "0"); + assert_eq!(forward(&mut context, "/a/[Symbol.search](\"ba\")"), "1"); + assert_eq!(forward(&mut context, "/a/[Symbol.search](\"bb\")"), "-1"); + assert_eq!(forward(&mut context, "/u/[Symbol.search](null)"), "1"); + assert_eq!(forward(&mut context, "/d/[Symbol.search](undefined)"), "2"); +} diff --git a/boa/src/builtins/string/tests.rs b/boa/src/builtins/string/tests.rs index dea218708fa..996283b4f86 100644 --- a/boa/src/builtins/string/tests.rs +++ b/boa/src/builtins/string/tests.rs @@ -1104,3 +1104,13 @@ fn string_get_property() { assert_eq!(forward(&mut context, "'abc'['foo']"), "undefined"); assert_eq!(forward(&mut context, "'😀'[0]"), "\"\\ud83d\""); } + +#[test] +fn search() { + let mut context = Context::new(); + + assert_eq!(forward(&mut context, "'aa'.search(/b/)"), "-1"); + assert_eq!(forward(&mut context, "'aa'.search(/a/)"), "0"); + assert_eq!(forward(&mut context, "'aa'.search(/a/g)"), "0"); + assert_eq!(forward(&mut context, "'ba'.search(/a/)"), "1"); +} From 60a5a532a94546be898fb5d96a557e748b33da70 Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Sun, 13 Jun 2021 02:57:33 +0200 Subject: [PATCH 5/6] remove leftover comments --- boa/src/builtins/string/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs index 7f9fac4dea3..aa757d225a0 100644 --- a/boa/src/builtins/string/mod.rs +++ b/boa/src/builtins/string/mod.rs @@ -1351,9 +1351,6 @@ impl String { // i. Return ? Call(searcher, regexp, « O »). return searcher.call(®exp, &[this.clone()], context); } - - //let re = RegExp::constructor(&Value::from(Object::default()), &[regexp], context)?; - //return RegExp::search(&re, &[this.clone()], context); } // 3. Let string be ? ToString(O). From 9ac467a8db6c0e35721f4b6fab97a15b174b7e8d Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Mon, 14 Jun 2021 17:00:45 +0200 Subject: [PATCH 6/6] Fix RegExp.prototype.search to accept all objects * Add some more rust tests from the 262 suite --- boa/src/builtins/regexp/mod.rs | 75 +++++++++++++++----------------- boa/src/builtins/regexp/tests.rs | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 40 deletions(-) diff --git a/boa/src/builtins/regexp/mod.rs b/boa/src/builtins/regexp/mod.rs index e1176bbd978..4466143cfc6 100644 --- a/boa/src/builtins/regexp/mod.rs +++ b/boa/src/builtins/regexp/mod.rs @@ -734,53 +734,48 @@ impl RegExp { pub(crate) fn search(this: &Value, args: &[Value], context: &mut Context) -> Result { // 1. Let rx be the this value. // 2. If Type(rx) is not Object, throw a TypeError exception. - if let Some(object) = this.as_object() { - if object.is_regexp() { - // 3. Let S be ? ToString(string). - let arg_str = args - .get(0) - .cloned() - .unwrap_or_default() - .to_string(context)?; + if !this.is_object() { + return context.throw_type_error( + "RegExp.prototype[Symbol.search] method called on incompatible value", + ); + } - // 4. Let previousLastIndex be ? Get(rx, "lastIndex"). - let previous_last_index = - this.get_field("lastIndex", context)?.to_length(context)?; + // 3. Let S be ? ToString(string). + let arg_str = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; - // 5. If SameValue(previousLastIndex, +0𝔽) is false, then - if previous_last_index != 0 { - // a. Perform ? Set(rx, "lastIndex", +0𝔽, true). - this.set_field("lastIndex", 0, context)?; - } + // 4. Let previousLastIndex be ? Get(rx, "lastIndex"). + let previous_last_index = this.get_field("lastIndex", context)?.to_length(context)?; - // 6. Let result be ? RegExpExec(rx, S). - let result = Self::exec(this, &[Value::from(arg_str)], context)?; + // 5. If SameValue(previousLastIndex, +0𝔽) is false, then + if previous_last_index != 0 { + // a. Perform ? Set(rx, "lastIndex", +0𝔽, true). + this.set_field("lastIndex", 0, context)?; + } - // 7. Let currentLastIndex be ? Get(rx, "lastIndex"). - let current_last_index = - this.get_field("lastIndex", context)?.to_length(context)?; + // 6. Let result be ? RegExpExec(rx, S). + let result = Self::exec(this, &[Value::from(arg_str)], context)?; - // 8. If SameValue(currentLastIndex, previousLastIndex) is false, then - if current_last_index != previous_last_index { - // a. Perform ? Set(rx, "lastIndex", previousLastIndex, true). - this.set_field("lastIndex", previous_last_index, context)?; - } + // 7. Let currentLastIndex be ? Get(rx, "lastIndex"). + let current_last_index = this.get_field("lastIndex", context)?.to_length(context)?; - // 9. If result is null, return -1𝔽. - // 10. Return ? Get(result, "index"). - if result.is_null() { - Ok(Value::from(-1)) - } else { - result.get_field("index", context).map_err(|_| { - context.construct_type_error("Could not find property `index`") - }) - } - } else { - context - .throw_type_error("RegExp.prototype.search method called on incompatible value") - } + // 8. If SameValue(currentLastIndex, previousLastIndex) is false, then + if current_last_index != previous_last_index { + // a. Perform ? Set(rx, "lastIndex", previousLastIndex, true). + this.set_field("lastIndex", previous_last_index, context)?; + } + + // 9. If result is null, return -1𝔽. + // 10. Return ? Get(result, "index"). + if result.is_null() { + Ok(Value::from(-1)) } else { - context.throw_type_error("RegExp.prototype.search method called on incompatible value") + result + .get_field("index", context) + .map_err(|_| context.construct_type_error("Could not find property `index`")) } } } diff --git a/boa/src/builtins/regexp/tests.rs b/boa/src/builtins/regexp/tests.rs index 56f4be43694..b4f2c341d4c 100644 --- a/boa/src/builtins/regexp/tests.rs +++ b/boa/src/builtins/regexp/tests.rs @@ -112,6 +112,81 @@ fn no_panic_on_invalid_character_escape() { fn search() { let mut context = Context::new(); + // coerce-string + assert_eq!( + forward( + &mut context, + r#" + var obj = { + toString: function() { + return 'toString value'; + } + }; + /ring/[Symbol.search](obj) + "# + ), + "4" + ); + + // failure-return-val + assert_eq!(forward(&mut context, "/z/[Symbol.search]('a')"), "-1"); + + // length + assert_eq!( + forward(&mut context, "RegExp.prototype[Symbol.search].length"), + "1" + ); + + let init = + "var obj = Object.getOwnPropertyDescriptor(RegExp.prototype[Symbol.search], \"length\")"; + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "obj.enumerable"), "false"); + assert_eq!(forward(&mut context, "obj.writable"), "false"); + assert_eq!(forward(&mut context, "obj.configurable"), "true"); + + // name + assert_eq!( + forward(&mut context, "RegExp.prototype[Symbol.search].name"), + "\"[Symbol.search]\"" + ); + + let init = + "var obj = Object.getOwnPropertyDescriptor(RegExp.prototype[Symbol.search], \"name\")"; + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "obj.enumerable"), "false"); + assert_eq!(forward(&mut context, "obj.writable"), "false"); + assert_eq!(forward(&mut context, "obj.configurable"), "true"); + + // prop-desc + let init = "var obj = Object.getOwnPropertyDescriptor(RegExp.prototype, Symbol.search)"; + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "obj.enumerable"), "false"); + assert_eq!(forward(&mut context, "obj.writable"), "true"); + assert_eq!(forward(&mut context, "obj.configurable"), "true"); + + // success-return-val + assert_eq!(forward(&mut context, "/a/[Symbol.search]('abc')"), "0"); + assert_eq!(forward(&mut context, "/b/[Symbol.search]('abc')"), "1"); + assert_eq!(forward(&mut context, "/c/[Symbol.search]('abc')"), "2"); + + // this-val-non-obj + let error = "Uncaught \"TypeError\": \"RegExp.prototype[Symbol.search] method called on incompatible value\""; + let init = "var search = RegExp.prototype[Symbol.search]"; + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "search.call()"), error); + assert_eq!(forward(&mut context, "search.call(undefined)"), error); + assert_eq!(forward(&mut context, "search.call(null)"), error); + assert_eq!(forward(&mut context, "search.call(true)"), error); + assert_eq!(forward(&mut context, "search.call('string')"), error); + assert_eq!(forward(&mut context, "search.call(Symbol.search)"), error); + assert_eq!(forward(&mut context, "search.call(86)"), error); + + // u-lastindex-advance + assert_eq!( + forward(&mut context, "/\\udf06/u[Symbol.search]('\\ud834\\udf06')"), + "-1" + ); + assert_eq!(forward(&mut context, "/a/[Symbol.search](\"a\")"), "0"); assert_eq!(forward(&mut context, "/a/[Symbol.search](\"ba\")"), "1"); assert_eq!(forward(&mut context, "/a/[Symbol.search](\"bb\")"), "-1");