Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement RegExp.prototype [ @@search ] ( string ) #1314

Merged
merged 6 commits into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 64 additions & 0 deletions boa/src/builtins/regexp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<Value> {
RageKnify marked this conversation as resolved.
Show resolved Hide resolved
// 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`"))
}
}
}
86 changes: 86 additions & 0 deletions boa/src/builtins/regexp/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
46 changes: 46 additions & 0 deletions boa/src/builtins/string/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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<Value> {
// 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(&regexp, &[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<Value> {
StringIterator::create_string_iterator(context, this.clone())
}
Expand Down
10 changes: 10 additions & 0 deletions boa/src/builtins/string/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}