diff --git a/boa/src/builtins/regexp/mod.rs b/boa/src/builtins/regexp/mod.rs index c4ac8b7cc0..4466143cfc 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,6 +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, + (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) @@ -714,4 +720,62 @@ 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 { + // 1. Let rx be the this value. + // 2. If Type(rx) is not Object, throw a TypeError exception. + if !this.is_object() { + return context.throw_type_error( + "RegExp.prototype[Symbol.search] method called on incompatible value", + ); + } + + // 3. Let S be ? ToString(string). + let arg_str = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; + + // 4. Let previousLastIndex be ? Get(rx, "lastIndex"). + let previous_last_index = this.get_field("lastIndex", context)?.to_length(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)?; + } + + // 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`")) + } + } } diff --git a/boa/src/builtins/regexp/tests.rs b/boa/src/builtins/regexp/tests.rs index a9d014bb87..b4f2c341d4 100644 --- a/boa/src/builtins/regexp/tests.rs +++ b/boa/src/builtins/regexp/tests.rs @@ -107,3 +107,89 @@ 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(); + + // 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"); + 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/mod.rs b/boa/src/builtins/string/mod.rs index 3b6c15271a..aa757d225a 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,51 @@ impl String { RegExp::match_all(&re, this.to_string(context)?.to_string(), context) } + /// `String.prototype.search( 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 { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(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); + } + } + + // 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 { StringIterator::create_string_iterator(context, this.clone()) } diff --git a/boa/src/builtins/string/tests.rs b/boa/src/builtins/string/tests.rs index dea218708f..996283b4f8 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"); +}