boa_engine/builtins/regexp/mod.rs
1//! Boa's implementation of ECMAScript's global `RegExp` object.
2//!
3//! The `RegExp` object is used for matching text with a pattern.
4//!
5//! More information:
6//! - [ECMAScript reference][spec]
7//! - [MDN documentation][mdn]
8//!
9//! [spec]: https://tc39.es/ecma262/#sec-regexp-constructor
10//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
11
12use crate::{
13 Context, JsArgs, JsData, JsResult, JsString,
14 builtins::{BuiltInObject, array::Array, string},
15 context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
16 error::JsNativeError,
17 js_string,
18 native_function::NativeFunctionObject,
19 object::{CONSTRUCTOR, JsObject, internal_methods::get_prototype_from_constructor},
20 property::Attribute,
21 realm::Realm,
22 string::{CodePoint, CommonJsStringBuilder, JsStrVariant, StaticJsStrings},
23 symbol::JsSymbol,
24 value::JsValue,
25};
26use arrayvec::ArrayVec;
27use boa_gc::{Finalize, Trace};
28use boa_macros::{js_str, utf16};
29use boa_parser::lexer::regex::RegExpFlags;
30use regress::{Flags, Range, Regex};
31use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
32
33thread_local! {
34 /// Compiled matchers by source and flags, so a regular expression literal evaluated in a
35 /// loop compiles its pattern once.
36 static MATCHERS: RefCell<HashMap<(JsString, u8), Rc<Regex>>> = RefCell::default();
37}
38
39use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject};
40
41mod regexp_string_iterator;
42pub(crate) use regexp_string_iterator::RegExpStringIterator;
43#[cfg(test)]
44mod tests;
45
46/// The internal representation of a `RegExp` object.
47#[derive(Debug, Clone, Trace, Finalize, JsData)]
48// Safety: `RegExp` does not contain any objects which needs to be traced, so this is safe.
49#[boa_gc(unsafe_empty_trace)]
50pub struct RegExp {
51 /// Regex matcher.
52 matcher: Rc<Regex>,
53 flags: RegExpFlags,
54 original_source: JsString,
55 original_flags: JsString,
56}
57
58impl RegExp {
59 /// Returns the original source string of the regex (e.g. `"regex-test"`).
60 pub(crate) fn original_source(&self) -> &JsString {
61 &self.original_source
62 }
63
64 /// Returns the original flags string of the regex (e.g. `"gi"`).
65 pub(crate) fn original_flags(&self) -> &JsString {
66 &self.original_flags
67 }
68}
69
70impl IntrinsicObject for RegExp {
71 fn init(realm: &Realm) {
72 let get_species = BuiltInBuilder::callable(realm, Self::get_species)
73 .name(js_string!("get [Symbol.species]"))
74 .build();
75
76 let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE;
77
78 let get_has_indices = BuiltInBuilder::callable(realm, Self::get_has_indices)
79 .name(js_string!("get hasIndices"))
80 .build();
81 let get_global = BuiltInBuilder::callable(realm, Self::get_global)
82 .name(js_string!("get global"))
83 .build();
84 let get_ignore_case = BuiltInBuilder::callable(realm, Self::get_ignore_case)
85 .name(js_string!("get ignoreCase"))
86 .build();
87 let get_multiline = BuiltInBuilder::callable(realm, Self::get_multiline)
88 .name(js_string!("get multiline"))
89 .build();
90 let get_dot_all = BuiltInBuilder::callable(realm, Self::get_dot_all)
91 .name(js_string!("get dotAll"))
92 .build();
93 let get_unicode = BuiltInBuilder::callable(realm, Self::get_unicode)
94 .name(js_string!("get unicode"))
95 .build();
96 let get_unicode_sets = BuiltInBuilder::callable(realm, Self::get_unicode_sets)
97 .name(js_string!("get unicodeSets"))
98 .build();
99 let get_sticky = BuiltInBuilder::callable(realm, Self::get_sticky)
100 .name(js_string!("get sticky"))
101 .build();
102 let get_flags = BuiltInBuilder::callable(realm, Self::get_flags)
103 .name(js_string!("get flags"))
104 .build();
105 let get_source = BuiltInBuilder::callable(realm, Self::get_source)
106 .name(js_string!("get source"))
107 .build();
108 let regexp = BuiltInBuilder::from_standard_constructor::<Self>(realm)
109 .static_method(Self::escape, js_string!("escape"), 1)
110 .static_accessor(
111 JsSymbol::species(),
112 Some(get_species),
113 None,
114 Attribute::CONFIGURABLE,
115 )
116 .property(js_string!("lastIndex"), 0, Attribute::all())
117 .method(Self::test, js_string!("test"), 1)
118 .method(Self::exec, js_string!("exec"), 1)
119 .method(Self::to_string, js_string!("toString"), 0)
120 .method(Self::r#match, JsSymbol::r#match(), 1)
121 .method(Self::match_all, JsSymbol::match_all(), 1)
122 .method(Self::replace, JsSymbol::replace(), 2)
123 .method(Self::search, JsSymbol::search(), 1)
124 .method(Self::split, JsSymbol::split(), 2)
125 .accessor(
126 js_string!("hasIndices"),
127 Some(get_has_indices),
128 None,
129 flag_attributes,
130 )
131 .accessor(
132 js_string!("global"),
133 Some(get_global),
134 None,
135 flag_attributes,
136 )
137 .accessor(
138 js_string!("ignoreCase"),
139 Some(get_ignore_case),
140 None,
141 flag_attributes,
142 )
143 .accessor(
144 js_string!("multiline"),
145 Some(get_multiline),
146 None,
147 flag_attributes,
148 )
149 .accessor(
150 js_string!("dotAll"),
151 Some(get_dot_all),
152 None,
153 flag_attributes,
154 )
155 .accessor(
156 js_string!("unicode"),
157 Some(get_unicode),
158 None,
159 flag_attributes,
160 )
161 .accessor(
162 js_string!("unicodeSets"),
163 Some(get_unicode_sets),
164 None,
165 flag_attributes,
166 )
167 .accessor(
168 js_string!("sticky"),
169 Some(get_sticky),
170 None,
171 flag_attributes,
172 )
173 .accessor(js_string!("flags"), Some(get_flags), None, flag_attributes)
174 .accessor(
175 js_string!("source"),
176 Some(get_source),
177 None,
178 flag_attributes,
179 );
180
181 #[cfg(feature = "annex-b")]
182 let regexp = regexp.method(Self::compile, js_string!("compile"), 2);
183
184 regexp.build();
185 }
186
187 fn get(intrinsics: &Intrinsics) -> JsObject {
188 Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
189 }
190}
191
192impl BuiltInObject for RegExp {
193 const NAME: JsString = StaticJsStrings::REG_EXP;
194}
195
196impl BuiltInConstructor for RegExp {
197 const CONSTRUCTOR_ARGUMENTS: usize = 2;
198 const PROTOTYPE_STORAGE_SLOTS: usize = 30;
199 const CONSTRUCTOR_STORAGE_SLOTS: usize = 3;
200
201 const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
202 StandardConstructors::regexp;
203
204 /// `22.2.3.1 RegExp ( pattern, flags )`
205 ///
206 /// More information:
207 /// - [ECMAScript reference][spec]
208 ///
209 /// [spec]: https://tc39.es/ecma262/#sec-regexp-pattern-flags
210 fn constructor(
211 new_target: &JsValue,
212 args: &[JsValue],
213 context: &mut Context,
214 ) -> JsResult<JsValue> {
215 let pattern = args.get_or_undefined(0);
216 let flags = args.get_or_undefined(1);
217
218 // 1. Let patternIsRegExp be ? IsRegExp(pattern).
219 let pattern_is_regexp = Self::is_reg_exp(pattern, context)?;
220
221 // 2. If NewTarget is undefined, then
222 // 3. Else, let newTarget be NewTarget.
223 if new_target.is_undefined() {
224 // a. Let newTarget be the active function object.
225 let new_target = context
226 .active_function_object()
227 .map_or(JsValue::undefined(), JsValue::new);
228
229 // b. If patternIsRegExp is true and flags is undefined, then
230 if let Some(pattern) = &pattern_is_regexp
231 && flags.is_undefined()
232 {
233 // i. Let patternConstructor be ? Get(pattern, "constructor").
234 let pattern_constructor = pattern.get(CONSTRUCTOR, context)?;
235
236 // ii. If SameValue(newTarget, patternConstructor) is true, return pattern.
237 if JsValue::same_value(&new_target, &pattern_constructor) {
238 return Ok(pattern.clone().into());
239 }
240 }
241 }
242
243 // 4. If pattern is an Object and pattern has a [[RegExpMatcher]] internal slot, then
244 let object = pattern.clone().as_object();
245 let (p, f) =
246 if let Some(pattern) = object.as_ref().and_then(JsObject::downcast_ref::<RegExp>) {
247 // a. Let P be pattern.[[OriginalSource]].
248 let p = pattern.original_source.clone().into();
249
250 // b. If flags is undefined, let F be pattern.[[OriginalFlags]].
251 let f = if flags.is_undefined() {
252 pattern.original_flags.clone().into()
253 // c. Else, let F be flags.
254 } else {
255 flags.clone()
256 };
257
258 (p, f)
259 } else if let Some(pattern) = &pattern_is_regexp {
260 // a. Let P be ? Get(pattern, "source").
261 let p = pattern.get(js_string!("source"), context)?;
262
263 // b. If flags is undefined, then
264 let f = if flags.is_undefined() {
265 // i. Let F be ? Get(pattern, "flags").
266 pattern.get(js_string!("flags"), context)?
267 // c. Else,
268 } else {
269 // i. Let F be flags.
270 flags.clone()
271 };
272
273 (p, f)
274 // 6. Else,
275 } else {
276 // a. Let P be pattern.
277 // b. Let F be flags.
278 (pattern.clone(), flags.clone())
279 };
280
281 // 7. Let O be ? RegExpAlloc(newTarget).
282 let proto =
283 get_prototype_from_constructor(new_target, StandardConstructors::regexp, context)?;
284
285 // 8.Return ? RegExpInitialize(O, P, F).
286 Self::initialize(Some(proto), &p, &f, context)
287 }
288}
289
290impl RegExp {
291 /// `7.2.8 IsRegExp ( argument )`
292 ///
293 /// This modified to return the object if it's `true`, [`None`] otherwise.
294 ///
295 /// More information:
296 /// - [ECMAScript reference][spec]
297 ///
298 /// [spec]: https://tc39.es/ecma262/#sec-isregexp
299 pub(crate) fn is_reg_exp(
300 argument: &JsValue,
301 context: &mut Context,
302 ) -> JsResult<Option<JsObject>> {
303 // 1. If argument is not an Object, return false.
304 let Some(argument) = argument.as_object() else {
305 return Ok(None);
306 };
307
308 // 2. Let matcher be ? Get(argument, @@match).
309 let matcher = argument.get(JsSymbol::r#match(), context)?;
310
311 // 3. If matcher is not undefined, return ToBoolean(matcher).
312 if !matcher.is_undefined() {
313 return Ok(matcher.to_boolean().then_some(argument));
314 }
315
316 // 4. If argument has a [[RegExpMatcher]] internal slot, return true.
317 if argument.is::<RegExp>() {
318 return Ok(Some(argument));
319 }
320
321 // 5. Return false.
322 Ok(None)
323 }
324
325 /// Compiles a `RegExp` from the provided pattern and flags.
326 ///
327 /// Equivalent to the beginning of [`RegExpInitialize ( obj, pattern, flags )`][spec]
328 ///
329 /// [spec]: https://tc39.es/ecma262/#sec-regexpinitialize
330 fn compile_native_regexp(
331 pattern: &JsValue,
332 flags: &JsValue,
333 context: &mut Context,
334 ) -> JsResult<RegExp> {
335 // 1. If pattern is undefined, let P be the empty String.
336 // 2. Else, let P be ? ToString(pattern).
337 let p = if pattern.is_undefined() {
338 js_string!()
339 } else {
340 pattern.to_string(context)?
341 };
342
343 // 3. If flags is undefined, let F be the empty String.
344 // 4. Else, let F be ? ToString(flags).
345 let f = if flags.is_undefined() {
346 js_string!()
347 } else {
348 flags.to_string(context)?
349 };
350
351 // 5. If F contains any code unit other than "g", "i", "m", "s", "u", "v", or "y"
352 // or if it contains the same code unit more than once, throw a SyntaxError exception.
353 // TODO: Should directly parse the JsString instead of converting to String
354 let flags = match RegExpFlags::from_str(&f.to_std_string_escaped()) {
355 Err(msg) => return Err(JsNativeError::syntax().with_message(msg).into()),
356 Ok(result) => result,
357 };
358
359 // 13. Let parseResult be ParsePattern(patternText, u, v).
360 // 14. If parseResult is a non-empty List of SyntaxError objects, throw a SyntaxError exception.
361
362 if let Some(matcher) =
363 MATCHERS.with(|matchers| matchers.borrow().get(&(p.clone(), flags.bits())).cloned())
364 {
365 return Ok(RegExp {
366 matcher,
367 flags,
368 original_source: p,
369 original_flags: f,
370 });
371 }
372
373 // If u or v flag is set, fullUnicode is true — compile as full codepoints.
374 let full_unicode =
375 flags.contains(RegExpFlags::UNICODE) || flags.contains(RegExpFlags::UNICODE_SETS);
376
377 let matcher = if full_unicode {
378 // Unicode mode (u/v flag) OR pattern has named groups:
379 // compile as full Unicode codepoints.
380 Regex::from_unicode(p.code_points().map(CodePoint::as_u32), Flags::from(flags))
381 .map_err(|error| {
382 JsNativeError::syntax()
383 .with_message(format!("failed to create matcher: {}", error.text))
384 })?
385 } else {
386 // Non-Unicode mode with no named groups:
387 // compile as raw UTF-16 code units so that surrogate pairs
388 // (e.g. 𠮷 = [0xD842, 0xDFB7]) are matched correctly by find_from_ucs2.
389 let utf16_units = p.code_points().flat_map(|cp| {
390 let mut buf = [0u16; 2];
391 match cp {
392 CodePoint::Unicode(c) => c
393 .encode_utf16(&mut buf)
394 .iter()
395 .map(|&u| u32::from(u))
396 .collect::<Vec<_>>(),
397 CodePoint::UnpairedSurrogate(s) => vec![u32::from(s)],
398 }
399 });
400 Regex::from_unicode(utf16_units, Flags::from(flags)).map_err(|error| {
401 JsNativeError::syntax()
402 .with_message(format!("failed to create matcher: {}", error.text))
403 })?
404 };
405
406 // 15. Assert: parseResult is a Pattern Parse Node.
407 // 16. Set obj.[[OriginalSource]] to P.
408 // 17. Set obj.[[OriginalFlags]] to F.
409 // 18. Let capturingGroupsCount be CountLeftCapturingParensWithin(parseResult).
410 // 19. Let rer be the RegExp Record { [[IgnoreCase]]: i, [[Multiline]]: m,
411 // [[DotAll]]: s, [[Unicode]]: u, [[UnicodeSets]]: v,
412 // [[CapturingGroupsCount]]: capturingGroupsCount }.
413 // 20. Set obj.[[RegExpRecord]] to rer.
414 // 21. Set obj.[[RegExpMatcher]] to CompilePattern of parseResult with argument rer.
415 let matcher = Rc::new(matcher);
416 MATCHERS.with(|matchers| {
417 let mut matchers = matchers.borrow_mut();
418 if matchers.len() >= 1024 {
419 matchers.clear();
420 }
421 matchers.insert((p.clone(), flags.bits()), matcher.clone());
422 });
423 Ok(RegExp {
424 matcher,
425 flags,
426 original_source: p,
427 original_flags: f,
428 })
429 }
430
431 /// `RegExpInitialize ( obj, pattern, flags )`
432 ///
433 /// If prototype is `None`, initializes the prototype to `%RegExp%.prototype`.
434 ///
435 /// More information:
436 /// - [ECMAScript reference][spec]
437 ///
438 /// [spec]: https://tc39.es/ecma262/#sec-regexpinitialize
439 pub(crate) fn initialize(
440 prototype: Option<JsObject>,
441 pattern: &JsValue,
442 flags: &JsValue,
443 context: &mut Context,
444 ) -> JsResult<JsValue> {
445 // Has the steps of `RegExpInitialize`.
446 let regexp = Self::compile_native_regexp(pattern, flags, context)?;
447
448 // 22. Perform ? Set(obj, "lastIndex", +0𝔽, true).
449 let obj = if let Some(prototype) = prototype {
450 let mut template = context
451 .intrinsics()
452 .templates()
453 .regexp_without_proto()
454 .clone();
455 template.set_prototype(prototype);
456 template.create(regexp, vec![0.into()])
457 } else {
458 context
459 .intrinsics()
460 .templates()
461 .regexp()
462 .create(regexp, vec![0.into()])
463 };
464
465 // 23. Return obj.
466 Ok(obj.into())
467 }
468
469 /// `22.2.3.2.4 RegExpCreate ( P, F )`
470 ///
471 /// More information:
472 /// - [ECMAScript reference][spec]
473 ///
474 /// [spec]: https://tc39.es/ecma262/#sec-regexpcreate
475 pub(crate) fn create(p: &JsValue, f: &JsValue, context: &mut Context) -> JsResult<JsValue> {
476 // 1. Let obj be ? RegExpAlloc(%RegExp%).
477 // 2. Return ? RegExpInitialize(obj, P, F).
478 Self::initialize(None, p, f, context)
479 }
480
481 /// `get RegExp [ @@species ]`
482 ///
483 /// The `RegExp [ @@species ]` accessor property returns the `RegExp` constructor.
484 ///
485 /// More information:
486 /// - [ECMAScript reference][spec]
487 /// - [MDN documentation][mdn]
488 ///
489 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp-@@species
490 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@species
491 #[allow(clippy::unnecessary_wraps)]
492 fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
493 // 1. Return the this value.
494 Ok(this.clone())
495 }
496
497 fn regexp_has_flag(this: &JsValue, flag: u8, context: &mut Context) -> JsResult<JsValue> {
498 if let Some(object) = this.as_object() {
499 if let Some(regexp) = object.downcast_ref::<RegExp>() {
500 return Ok(JsValue::new(match flag {
501 b'd' => regexp.flags.contains(RegExpFlags::HAS_INDICES),
502 b'g' => regexp.flags.contains(RegExpFlags::GLOBAL),
503 b'm' => regexp.flags.contains(RegExpFlags::MULTILINE),
504 b's' => regexp.flags.contains(RegExpFlags::DOT_ALL),
505 b'i' => regexp.flags.contains(RegExpFlags::IGNORE_CASE),
506 b'u' => regexp.flags.contains(RegExpFlags::UNICODE),
507 b'v' => regexp.flags.contains(RegExpFlags::UNICODE_SETS),
508 b'y' => regexp.flags.contains(RegExpFlags::STICKY),
509 _ => unreachable!(),
510 }));
511 }
512
513 if JsObject::equals(
514 &object,
515 &context.intrinsics().constructors().regexp().prototype(),
516 ) {
517 return Ok(JsValue::undefined());
518 }
519 }
520
521 let name = match flag {
522 b'd' => "hasIndices",
523 b'g' => "global",
524 b'm' => "multiline",
525 b's' => "dotAll",
526 b'i' => "ignoreCase",
527 b'u' => "unicode",
528 b'v' => "unicodeSets",
529 b'y' => "sticky",
530 _ => unreachable!(),
531 };
532
533 Err(JsNativeError::typ()
534 .with_message(format!(
535 "RegExp.prototype.{name} getter called on non-RegExp object",
536 ))
537 .into())
538 }
539
540 /// `get RegExp.prototype.hasIndices`
541 ///
542 /// More information:
543 /// - [ECMAScript reference][spec]
544 /// - [MDN documentation][mdn]
545 ///
546 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.hasindices
547 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global
548 pub(crate) fn get_has_indices(
549 this: &JsValue,
550 _: &[JsValue],
551 context: &mut Context,
552 ) -> JsResult<JsValue> {
553 Self::regexp_has_flag(this, b'd', context)
554 }
555
556 /// `get RegExp.prototype.global`
557 ///
558 /// The `global` property indicates whether or not the "`g`" flag is used with the regular expression.
559 ///
560 /// More information:
561 /// - [ECMAScript reference][spec]
562 /// - [MDN documentation][mdn]
563 ///
564 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.global
565 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global
566 pub(crate) fn get_global(
567 this: &JsValue,
568 _: &[JsValue],
569 context: &mut Context,
570 ) -> JsResult<JsValue> {
571 Self::regexp_has_flag(this, b'g', context)
572 }
573
574 /// `get RegExp.prototype.ignoreCase`
575 ///
576 /// The `ignoreCase` property indicates whether or not the "`i`" flag is used with the regular expression.
577 ///
578 /// More information:
579 /// - [ECMAScript reference][spec]
580 /// - [MDN documentation][mdn]
581 ///
582 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.ignorecase
583 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/ignoreCase
584 pub(crate) fn get_ignore_case(
585 this: &JsValue,
586 _: &[JsValue],
587 context: &mut Context,
588 ) -> JsResult<JsValue> {
589 Self::regexp_has_flag(this, b'i', context)
590 }
591
592 /// `get RegExp.prototype.multiline`
593 ///
594 /// The multiline property indicates whether or not the "m" flag is used with the regular expression.
595 ///
596 /// More information:
597 /// - [ECMAScript reference][spec]
598 /// - [MDN documentation][mdn]
599 ///
600 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.multiline
601 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/multiline
602 pub(crate) fn get_multiline(
603 this: &JsValue,
604 _: &[JsValue],
605 context: &mut Context,
606 ) -> JsResult<JsValue> {
607 Self::regexp_has_flag(this, b'm', context)
608 }
609
610 /// `get RegExp.prototype.dotAll`
611 ///
612 /// The `dotAll` property indicates whether or not the "`s`" flag is used with the regular expression.
613 ///
614 /// More information:
615 /// - [ECMAScript reference][spec]
616 /// - [MDN documentation][mdn]
617 ///
618 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.dotAll
619 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/dotAll
620 pub(crate) fn get_dot_all(
621 this: &JsValue,
622 _: &[JsValue],
623 context: &mut Context,
624 ) -> JsResult<JsValue> {
625 Self::regexp_has_flag(this, b's', context)
626 }
627
628 /// `get RegExp.prototype.unicode`
629 ///
630 /// The unicode property indicates whether or not the "`u`" flag is used with a regular expression.
631 /// unicode is a read-only property of an individual regular expression instance.
632 ///
633 /// More information:
634 /// - [ECMAScript reference][spec]
635 /// - [MDN documentation][mdn]
636 ///
637 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.unicode
638 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode
639 pub(crate) fn get_unicode(
640 this: &JsValue,
641 _: &[JsValue],
642 context: &mut Context,
643 ) -> JsResult<JsValue> {
644 Self::regexp_has_flag(this, b'u', context)
645 }
646
647 /// `get RegExp.prototype.unicodeSets`
648 ///
649 /// More information:
650 /// - [ECMAScript reference][spec]
651 /// - [MDN documentation][mdn]
652 ///
653 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.unicodesets
654 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets
655 pub(crate) fn get_unicode_sets(
656 this: &JsValue,
657 _: &[JsValue],
658 context: &mut Context,
659 ) -> JsResult<JsValue> {
660 Self::regexp_has_flag(this, b'v', context)
661 }
662
663 /// `get RegExp.prototype.sticky`
664 ///
665 /// This flag indicates that it matches only from the index indicated by the `lastIndex` property
666 /// of this regular expression in the target string (and does not attempt to match from any later indexes).
667 ///
668 /// More information:
669 /// - [ECMAScript reference][spec]
670 /// - [MDN documentation][mdn]
671 ///
672 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.sticky
673 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky
674 pub(crate) fn get_sticky(
675 this: &JsValue,
676 _: &[JsValue],
677 context: &mut Context,
678 ) -> JsResult<JsValue> {
679 Self::regexp_has_flag(this, b'y', context)
680 }
681
682 /// `get RegExp.prototype.flags`
683 ///
684 /// The `flags` property returns a string consisting of the [`flags`][flags] of the current regular expression object.
685 ///
686 /// More information:
687 /// - [ECMAScript reference][spec]
688 /// - [MDN documentation][mdn]
689 ///
690 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.flags
691 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags
692 /// [flags]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Advanced_searching_with_flags_2
693 pub(crate) fn get_flags(
694 this: &JsValue,
695 _: &[JsValue],
696 context: &mut Context,
697 ) -> JsResult<JsValue> {
698 // 1. Let R be the this value.
699 // 2. If R is not an Object, throw a TypeError exception.
700 let Some(object) = this.as_object() else {
701 return Err(JsNativeError::typ()
702 .with_message("RegExp.prototype.flags getter called on non-object")
703 .into());
704 };
705
706 // 3. Let codeUnits be a new empty List.
707 let mut code_units = String::new();
708
709 // 4. Let hasIndices be ToBoolean(? Get(R, "hasIndices")).
710 // 5. If hasIndices is true, append the code unit 0x0064 (LATIN SMALL LETTER D) to codeUnits.
711 if object.get(js_string!("hasIndices"), context)?.to_boolean() {
712 code_units.push('d');
713 }
714
715 // 6. Let global be ToBoolean(? Get(R, "global")).
716 // 7. If global is true, append the code unit 0x0067 (LATIN SMALL LETTER G) to codeUnits.
717 if object.get(js_string!("global"), context)?.to_boolean() {
718 code_units.push('g');
719 }
720
721 // 8. Let ignoreCase be ToBoolean(? Get(R, "ignoreCase")).
722 // 9. If ignoreCase is true, append the code unit 0x0069 (LATIN SMALL LETTER I) to codeUnits.
723 if object.get(js_string!("ignoreCase"), context)?.to_boolean() {
724 code_units.push('i');
725 }
726
727 // 10. Let multiline be ToBoolean(? Get(R, "multiline")).
728 // 11. If multiline is true, append the code unit 0x006D (LATIN SMALL LETTER M) to codeUnits.
729 if object.get(js_string!("multiline"), context)?.to_boolean() {
730 code_units.push('m');
731 }
732
733 // 12. Let dotAll be ToBoolean(? Get(R, "dotAll")).
734 // 13. If dotAll is true, append the code unit 0x0073 (LATIN SMALL LETTER S) to codeUnits.
735 if object.get(js_string!("dotAll"), context)?.to_boolean() {
736 code_units.push('s');
737 }
738
739 // 14. Let unicode be ToBoolean(? Get(R, "unicode")).
740 // 15. If unicode is true, append the code unit 0x0075 (LATIN SMALL LETTER U) to codeUnits.
741 if object.get(js_string!("unicode"), context)?.to_boolean() {
742 code_units.push('u');
743 }
744
745 // 16. Let unicodeSets be ToBoolean(? Get(R, "unicodeSets")).
746 // 17. If unicodeSets is true, append the code unit 0x0076 (LATIN SMALL LETTER V) to codeUnits.
747 if object.get(js_string!("unicodeSets"), context)?.to_boolean() {
748 code_units.push('v');
749 }
750
751 // 18. Let sticky be ToBoolean(? Get(R, "sticky")).
752 // 19. If sticky is true, append the code unit 0x0079 (LATIN SMALL LETTER Y) to codeUnits.
753 if object.get(js_string!("sticky"), context)?.to_boolean() {
754 code_units.push('y');
755 }
756
757 // 20. Return the String value whose code units are the elements of the List codeUnits.
758 // If codeUnits has no elements, the empty String is returned.
759 Ok(JsString::from(code_units).into())
760 }
761
762 /// `get RegExp.prototype.source`
763 ///
764 /// The `source` property returns a `String` containing the source text of the regexp object,
765 /// and it doesn't contain the two forward slashes on both sides and any flags.
766 ///
767 /// More information:
768 /// - [ECMAScript reference][spec]
769 /// - [MDN documentation][mdn]
770 ///
771 /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.source
772 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source
773 pub(crate) fn get_source(
774 this: &JsValue,
775 _: &[JsValue],
776 context: &mut Context,
777 ) -> JsResult<JsValue> {
778 // 1. Let R be the this value.
779 // 2. If Type(R) is not Object, throw a TypeError exception.
780 let Some(object) = this.as_object() else {
781 return Err(JsNativeError::typ()
782 .with_message("RegExp.prototype.source method called on incompatible value")
783 .into());
784 };
785
786 let casted = object.downcast_ref::<RegExp>();
787 match casted {
788 // 3. If R does not have an [[OriginalSource]] internal slot, then
789 None => {
790 // a. If SameValue(R, %RegExp.prototype%) is true, return "(?:)".
791 // b. Otherwise, throw a TypeError exception.
792 if JsValue::same_value(
793 this,
794 &JsValue::new(context.intrinsics().constructors().regexp().prototype()),
795 ) {
796 Ok(JsValue::new(js_string!("(?:)")))
797 } else {
798 Err(JsNativeError::typ()
799 .with_message("RegExp.prototype.source method called on incompatible value")
800 .into())
801 }
802 }
803 // 4. Assert: R has an [[OriginalFlags]] internal slot.
804 Some(re) => {
805 // 5. Let src be R.[[OriginalSource]].
806 // 6. Let flags be R.[[OriginalFlags]].
807 // 7. Return EscapeRegExpPattern(src, flags).
808 Ok(Self::escape_pattern(
809 &re.original_source,
810 &re.original_flags,
811 ))
812 }
813 }
814 }
815
816 /// `22.2.3.2.5 EscapeRegExpPattern ( P, F )`
817 ///
818 /// More information:
819 /// - [ECMAScript reference][spec]
820 ///
821 /// [spec]: https://tc39.es/ecma262/#sec-escaperegexppattern
822 fn escape_pattern(src: &JsString, _flags: &JsString) -> JsValue {
823 if src.is_empty() {
824 js_string!("(?:)").into()
825 } else {
826 let mut s = Vec::with_capacity(src.len());
827 let mut buf = [0; 2];
828 for c in src.code_points() {
829 match c {
830 CodePoint::Unicode('/') => s.extend_from_slice(utf16!(r"\/")),
831 CodePoint::Unicode('\n') => s.extend_from_slice(utf16!(r"\n")),
832 CodePoint::Unicode('\r') => s.extend_from_slice(utf16!(r"\r")),
833 CodePoint::Unicode('\u{2028}') => s.extend_from_slice(utf16!(r"\u2028")),
834 CodePoint::Unicode('\u{2029}') => s.extend_from_slice(utf16!(r"\u2029")),
835 CodePoint::Unicode(c) => s.extend_from_slice(c.encode_utf16(&mut buf)),
836 CodePoint::UnpairedSurrogate(surr) => s.push(surr),
837 }
838 }
839
840 JsValue::new(js_string!(&s[..]))
841 }
842 }
843
844 /// `RegExp.escape( string )`
845 ///
846 /// The `RegExp.escape()` static method escapes any potential regex syntax characters in a string,
847 /// and returns a new string that can be safely used as a literal pattern for the `RegExp()` constructor.
848 ///
849 /// More information:
850 /// - [ECMAScript reference][spec]
851 /// - [MDN documentation][mdn]
852 ///
853 /// [spec]: https://tc39.es/proposal-regex-escaping/#sec-regexp.escape
854 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape
855 ///
856 /// Helper function to check if a character is a `WhiteSpace` character
857 fn is_whitespace(ch: char) -> bool {
858 matches!(
859 ch,
860 '\u{0009}' | // <TAB>
861 '\u{000B}' | // <VT>
862 '\u{000C}' | // <FF>
863 '\u{0020}' | // <SP>
864 '\u{00A0}' | // <NBSP>
865 '\u{FEFF}' | // <ZWNBSP>
866 '\u{1680}' | // Ogham Space Mark
867 '\u{2000}' | '\u{2001}' | '\u{2002}' | '\u{2003}' | '\u{2004}' |
868 '\u{2005}' | '\u{2006}' | '\u{2007}' | '\u{2008}' | '\u{2009}' |
869 '\u{200A}' | // Various space separators
870 '\u{202F}' | // Narrow No-Break Space
871 '\u{205F}' | // Medium Mathematical Space
872 '\u{3000}' // Ideographic Space
873 )
874 }
875
876 /// Helper function to check if a character is a `LineTerminator` character
877 fn is_line_terminator(ch: char) -> bool {
878 matches!(
879 ch,
880 '\u{000A}' | // <LF>
881 '\u{000D}' | // <CR>
882 '\u{2028}' | // <LS>
883 '\u{2029}' // <PS>
884 )
885 }
886
887 pub(crate) fn escape(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
888 let arg = args.get_or_undefined(0);
889
890 // 1. If S is not a String, throw a TypeError exception.
891 let Some(string) = arg.as_string() else {
892 return Err(JsNativeError::typ()
893 .with_message("RegExp.escape requires a string argument")
894 .into());
895 };
896
897 // 2. Let escaped be the empty String.
898 let mut escaped = CommonJsStringBuilder::new();
899
900 // 3. Let cpList be StringToCodePoints(S).
901 // 4. For each code point c of cpList, do
902 for (index, c) in string.code_points().enumerate() {
903 let code = c.as_u32();
904
905 // 4.a. If escaped is the empty String and c is matched by either DecimalDigit or AsciiLetter, then
906 if index == 0
907 && let CodePoint::Unicode(ch) = c
908 && (ch.is_ascii_digit() || ch.is_ascii_alphabetic())
909 {
910 // 4.a.ii-v. Escape using \xXX format
911 let escape_seq = format!("\\x{code:02x}");
912 escaped.push(escape_seq.as_str());
913 continue;
914 }
915
916 // 4.b. Else, set escaped to the string-concatenation of escaped and EncodeForRegExpEscape(c).
917 match c {
918 CodePoint::Unicode(ch) => {
919 // EncodeForRegExpEscape step 1: SyntaxCharacter or U+002F (SOLIDUS)
920 if matches!(
921 ch,
922 '^' | '$'
923 | '\\'
924 | '.'
925 | '*'
926 | '+'
927 | '?'
928 | '('
929 | ')'
930 | '['
931 | ']'
932 | '{'
933 | '}'
934 | '|'
935 | '/'
936 ) {
937 escaped.push('\\');
938 escaped.push(ch);
939 }
940 // Step 2: ControlEscape characters (Table 64)
941 else if ch == '\x09' {
942 escaped.push("\\t");
943 } else if ch == '\x0A' {
944 escaped.push("\\n");
945 } else if ch == '\x0B' {
946 escaped.push("\\v");
947 } else if ch == '\x0C' {
948 escaped.push("\\f");
949 } else if ch == '\x0D' {
950 escaped.push("\\r");
951 }
952 // Step 3-5: otherPunctuators or WhiteSpace or LineTerminator
953 else if matches!(
954 ch,
955 ',' | '-'
956 | '='
957 | '<'
958 | '>'
959 | '#'
960 | '&'
961 | '!'
962 | '%'
963 | ':'
964 | ';'
965 | '@'
966 | '~'
967 | '\''
968 | '`'
969 | '"'
970 ) || Self::is_whitespace(ch)
971 || Self::is_line_terminator(ch)
972 {
973 let code = ch as u32;
974 if code <= 0xFF {
975 // Use \xXX format
976 let escape_seq = format!("\\x{code:02x}");
977 escaped.push(escape_seq.as_str());
978 } else {
979 // Use \uXXXX format
980 let escape_seq = format!("\\u{code:04x}");
981 escaped.push(escape_seq.as_str());
982 }
983 }
984 // Step 6: All other Unicode characters
985 else {
986 escaped.push(ch);
987 }
988 }
989 CodePoint::UnpairedSurrogate(surr) => {
990 // Escape unpaired surrogates using \uXXXX format
991 let escape_seq = format!("\\u{surr:04x}");
992 escaped.push(escape_seq.as_str());
993 }
994 }
995 }
996
997 // 5. Return escaped.
998 Ok(JsValue::new(escaped.build()))
999 }
1000
1001 /// `RegExp.prototype.test( string )`
1002 ///
1003 /// The `test()` method executes a search for a match between a regular expression and a specified string.
1004 ///
1005 /// Returns `true` or `false`.
1006 ///
1007 /// More information:
1008 /// - [ECMAScript reference][spec]
1009 /// - [MDN documentation][mdn]
1010 ///
1011 /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.test
1012 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
1013 pub(crate) fn test(
1014 this: &JsValue,
1015 args: &[JsValue],
1016 context: &mut Context,
1017 ) -> JsResult<JsValue> {
1018 // 1. Let R be the this value.
1019 // 2. If Type(R) is not Object, throw a TypeError exception.
1020 let this = this.as_object().ok_or_else(|| {
1021 JsNativeError::typ()
1022 .with_message("RegExp.prototype.test method called on incompatible value")
1023 })?;
1024
1025 // 3. Let string be ? ToString(S).
1026 let arg_str = args
1027 .first()
1028 .cloned()
1029 .unwrap_or_default()
1030 .to_string(context)?;
1031
1032 // 4. Let match be ? RegExpExec(R, string).
1033 // With the intrinsic `exec`, only whether it matches is needed, not the result array.
1034 let exec = this.get(js_string!("exec"), context)?;
1035 if let Some(rx) = Self::intrinsic_exec_target(&this, &exec) {
1036 let data = rx.borrow().data().clone();
1037 let matched = Self::builtin_exec_match(&rx.upcast(), &data, &arg_str, context)?;
1038 return Ok(JsValue::new(matched.is_some()));
1039 }
1040 let m = Self::abstract_exec_with(&this, &exec, arg_str, context)?;
1041
1042 // 5. If match is not null, return true; else return false.
1043 if m.is_some() {
1044 Ok(JsValue::new(true))
1045 } else {
1046 Ok(JsValue::new(false))
1047 }
1048 }
1049
1050 /// `RegExp.prototype.exec( string )`
1051 ///
1052 /// The `exec()` method executes a search for a match in a specified string.
1053 ///
1054 /// Returns a result array, or `null`.
1055 ///
1056 /// More information:
1057 /// - [ECMAScript reference][spec]
1058 /// - [MDN documentation][mdn]
1059 ///
1060 /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.exec
1061 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec
1062 pub(crate) fn exec(
1063 this: &JsValue,
1064 args: &[JsValue],
1065 context: &mut Context,
1066 ) -> JsResult<JsValue> {
1067 // 1. Let R be the this value.
1068 // 2. Perform ? RequireInternalSlot(R, [[RegExpMatcher]]).
1069 let this = this.as_object();
1070 let obj = this
1071 .and_then(|o| o.downcast::<RegExp>().ok())
1072 .ok_or_else(|| {
1073 JsNativeError::typ().with_message("RegExp.prototype.exec called with invalid value")
1074 })?;
1075
1076 // 3. Let S be ? ToString(string).
1077 let arg_str = args.get_or_undefined(0).to_string(context)?;
1078
1079 // 4. Return ? RegExpBuiltinExec(R, S).
1080 (Self::abstract_builtin_exec(obj, &arg_str, context)?)
1081 .map_or_else(|| Ok(JsValue::null()), |v| Ok(v.into()))
1082 }
1083
1084 /// `22.2.5.2.1 RegExpExec ( R, S )`
1085 ///
1086 /// More information:
1087 /// - [ECMAScript reference][spec]
1088 ///
1089 /// [spec]: https://tc39.es/ecma262/#sec-regexpexec
1090 pub(crate) fn abstract_exec(
1091 this: &JsObject,
1092 input: JsString,
1093 context: &mut Context,
1094 ) -> JsResult<Option<JsObject>> {
1095 // 1. Assert: Type(R) is Object.
1096 // 2. Assert: Type(S) is String.
1097
1098 // 3. Let exec be ? Get(R, "exec").
1099 let exec = this.get(js_string!("exec"), context)?;
1100
1101 Self::abstract_exec_with(this, &exec, input, context)
1102 }
1103
1104 /// The `RegExp` that `exec` would run `RegExpBuiltinExec` on when `exec` is the intrinsic
1105 /// `RegExp.prototype.exec` and `this` is a `RegExp`, so callers can skip the call.
1106 fn intrinsic_exec_target(this: &JsObject, exec: &JsValue) -> Option<JsObject<RegExp>> {
1107 let exec = exec.as_object()?;
1108 let native = exec.downcast_ref::<NativeFunctionObject>()?;
1109 if !native.f.is_fn_ptr(Self::exec) {
1110 return None;
1111 }
1112 this.clone().downcast::<RegExp>().ok()
1113 }
1114
1115 /// Steps 4 to 6 of `RegExpExec`, with `exec` already read from `this`.
1116 fn abstract_exec_with(
1117 this: &JsObject,
1118 exec: &JsValue,
1119 input: JsString,
1120 context: &mut Context,
1121 ) -> JsResult<Option<JsObject>> {
1122 if let Some(rx) = Self::intrinsic_exec_target(this, exec) {
1123 return Self::abstract_builtin_exec(rx, &input, context);
1124 }
1125
1126 // 4. If IsCallable(exec) is true, then
1127 if let Some(exec) = exec.as_callable() {
1128 // a. Let result be ? Call(exec, R, « S »).
1129 let result = exec.call(&this.clone().into(), &[input.into()], context)?;
1130
1131 // b. If Type(result) is neither Object nor Null, throw a TypeError exception.
1132 if !result.is_object() && !result.is_null() {
1133 return Err(JsNativeError::typ()
1134 .with_message("regexp exec returned neither object nor null")
1135 .into());
1136 }
1137
1138 // c. Return result.
1139 return Ok(result.as_object());
1140 }
1141
1142 // 5. Perform ? RequireInternalSlot(R, [[RegExpMatcher]]).
1143 let Ok(this) = this.clone().downcast::<RegExp>() else {
1144 return Err(JsNativeError::typ()
1145 .with_message("RegExpExec called with invalid value")
1146 .into());
1147 };
1148
1149 // 6. Return ? RegExpBuiltinExec(R, S).
1150 Self::abstract_builtin_exec(this, &input, context)
1151 }
1152
1153 /// `22.2.7.2 RegExpBuiltinExec ( R, S )`
1154 ///
1155 /// More information:
1156 /// - [ECMAScript reference][spec]
1157 ///
1158 /// [spec]: https://tc39.es/ecma262/#sec-regexpbuiltinexec
1159 pub(crate) fn abstract_builtin_exec(
1160 this: JsObject<RegExp>,
1161 input: &JsString,
1162 context: &mut Context,
1163 ) -> JsResult<Option<JsObject>> {
1164 let rx = this.borrow().data().clone();
1165 let this = this.upcast();
1166
1167 let Some((match_value, last_index)) = Self::builtin_exec_match(&this, &rx, input, context)?
1168 else {
1169 return Ok(None);
1170 };
1171
1172 // 6. If flags contains "d", let hasIndices be true; else let hasIndices be false.
1173 let has_indices = rx.flags.contains(RegExpFlags::HAS_INDICES);
1174
1175 // 14. Let e be r's endIndex value.
1176 // 15. If fullUnicode is true, set e to GetStringIndex(S, e).
1177 // NOTE: Step 15 is already taken care of by regress.
1178 let e = match_value.end();
1179
1180 Self::build_exec_result(&match_value, last_index, e, has_indices, input, context).map(Some)
1181 }
1182
1183 /// Steps 1 to 16 of `RegExpBuiltinExec`: runs the matcher from `lastIndex` and updates
1184 /// `lastIndex`, returning the match and its start index, or `None` where the result is null.
1185 /// `RegExp.prototype.test` needs no more than this.
1186 fn builtin_exec_match(
1187 this: &JsObject,
1188 rx: &RegExp,
1189 input: &JsString,
1190 context: &mut Context,
1191 ) -> JsResult<Option<(regress::Match, u64)>> {
1192 // 1. Let length be the length of S.
1193 let length = input.len() as u64;
1194
1195 // 2. Let lastIndex be ℝ(? ToLength(? Get(R, "lastIndex"))).
1196 let mut last_index = this
1197 .get(js_string!("lastIndex"), context)?
1198 .to_length(context)?;
1199
1200 // 3. Let flags be R.[[OriginalFlags]].
1201 let flags = rx.flags;
1202
1203 // 4. If flags contains "g", let global be true; else let global be false.
1204 let global = flags.contains(RegExpFlags::GLOBAL);
1205
1206 // 5. If flags contains "y", let sticky be true; else let sticky be false.
1207 let sticky = flags.contains(RegExpFlags::STICKY);
1208
1209 // 7. If global is false and sticky is false, set lastIndex to 0.
1210 if !global && !sticky {
1211 last_index = 0;
1212 }
1213
1214 // 8. Let matcher be R.[[RegExpMatcher]].
1215 let matcher = &rx.matcher;
1216
1217 // 9. If flags contains "u" or flags contains "v", let fullUnicode be true; else let fullUnicode be false.
1218 let full_unicode =
1219 flags.contains(RegExpFlags::UNICODE) || flags.contains(RegExpFlags::UNICODE_SETS);
1220
1221 // NOTE: The following steps are take care of by regress:
1222 //
1223 // SKIP: 10. Let matchSucceeded be false.
1224 // SKIP: 11. If fullUnicode is true, let input be StringToCodePoints(S). Otherwise, let input be a List whose elements are the code units that are the elements of S.
1225 // SKIP: 12. NOTE: Each element of input is considered to be a character.
1226 // SKIP: 13. Repeat, while matchSucceeded is false,
1227
1228 // 13.a. If lastIndex > length, then
1229 if last_index > length {
1230 // i. If global is true or sticky is true, then
1231 if global || sticky {
1232 // 1. Perform ? Set(R, "lastIndex", +0𝔽, true).
1233 this.set(js_string!("lastIndex"), 0, true, context)?;
1234 }
1235
1236 // ii. Return null.
1237 return Ok(None);
1238 }
1239
1240 // 13.b. Let inputIndex be the index into input of the character that was obtained from element lastIndex of S.
1241 // 13.c. Let r be matcher(input, inputIndex).
1242 let r: Option<regress::Match> = match (full_unicode, input.as_str().variant()) {
1243 (true | false, JsStrVariant::Latin1(bytes)) => {
1244 // TODO: Currently regress does not support latin1 encoding.
1245 // A short input is widened on the stack rather than in a new allocation.
1246 let mut short = ArrayVec::<u16, 256>::new();
1247 let long;
1248 let input: &[u16] = if bytes.len() <= short.capacity() {
1249 short.extend(bytes.iter().map(|&byte| u16::from(byte)));
1250 &short
1251 } else {
1252 long = input.to_vec();
1253 &long
1254 };
1255
1256 // NOTE: We can use the faster ucs2 variant since there will never be two byte unicode.
1257 matcher.find_from_ucs2(input, last_index as usize).next()
1258 }
1259 (true, JsStrVariant::Utf16(input)) => {
1260 matcher.find_from_utf16(input, last_index as usize).next()
1261 }
1262 (false, JsStrVariant::Utf16(input)) => {
1263 matcher.find_from_ucs2(input, last_index as usize).next()
1264 }
1265 };
1266
1267 let Some(match_value) = r else {
1268 // d. If r is failure, then
1269 //
1270 // NOTE: Merged the following steps (since we no longer have a loop):
1271 // 13.d.i. If sticky is true, then
1272 // 13.a.i. If global is true or sticky is true, then
1273 if global || sticky {
1274 // 1. Perform ? Set(R, "lastIndex", +0𝔽, true).
1275 this.set(js_string!("lastIndex"), 0, true, context)?;
1276 }
1277
1278 // MOVE: ii. Set lastIndex to AdvanceStringIndex(S, lastIndex, fullUnicode).
1279 // NOTE: Handled within the regress matches iterator, see below for last_index assignment.
1280
1281 // NOTE: Merged and steps:
1282 // 13.a.ii. Return null.
1283 // 13.d.i.2. Return null.
1284 return Ok(None);
1285 };
1286
1287 // e. Else
1288 // SKIP: i. Assert: r is a MatchState.
1289 // SKIP: ii. Set matchSucceeded to true.
1290
1291 // NOTE: regress currently doesn't support the sticky flag so we have to emulate it.
1292 if sticky && match_value.start() != last_index as usize {
1293 // 1. Perform ? Set(R, "lastIndex", +0𝔽, true).
1294 this.set(js_string!("lastIndex"), 0, true, context)?;
1295
1296 // 2. Return null.
1297 return Ok(None);
1298 }
1299
1300 // 13.d.ii. Set lastIndex to AdvanceStringIndex(S, lastIndex, fullUnicode).
1301 // NOTE: Calculation of last_index is done in regress.
1302 last_index = match_value.start() as u64;
1303
1304 // 16. If global is true or sticky is true, then
1305 if global || sticky {
1306 // a. Perform ? Set(R, "lastIndex", 𝔽(e), true).
1307 this.set(js_string!("lastIndex"), match_value.end(), true, context)?;
1308 }
1309
1310 Ok(Some((match_value, last_index)))
1311 }
1312
1313 /// Steps 17 to 35 of `RegExpBuiltinExec`: the result array of a match. The `indices`
1314 /// arrays are only built when the regular expression has the `d` flag.
1315 fn build_exec_result(
1316 match_value: ®ress::Match,
1317 last_index: u64,
1318 e: usize,
1319 has_indices: bool,
1320 input: &JsString,
1321 context: &mut Context,
1322 ) -> JsResult<JsObject> {
1323 // 17. Let n be the number of elements in r's captures List.
1324 let n = match_value.captures.len() as u64;
1325 // 18. Assert: n = R.[[RegExpRecord]].[[CapturingGroupsCount]].
1326 // 19. Assert: n < 232 - 1.
1327 debug_assert!(n < (1u64 << 32) - 1);
1328
1329 // 20. Let A be ! ArrayCreate(n + 1).
1330 // 21. Assert: The mathematical value of A's "length" property is n + 1.
1331 let a = Array::array_create(n + 1, None, context)?;
1332
1333 // 22. Perform ! CreateDataPropertyOrThrow(A, "index", 𝔽(lastIndex)).
1334 a.create_data_property_or_throw(js_string!("index"), last_index, context)?;
1335
1336 // 23. Perform ! CreateDataPropertyOrThrow(A, "input", S).
1337 a.create_data_property_or_throw(js_string!("input"), input.clone(), context)?;
1338
1339 // 24. Let match be the Match Record { [[StartIndex]]: lastIndex, [[EndIndex]]: e }.
1340 // Immediately convert it to an array according to 22.2.7.7 GetMatchIndexPair(S, match)
1341 // 1. Assert: match.[[StartIndex]] ≤ match.[[EndIndex]] ≤ the length of S.
1342 // 2. Return CreateArrayFromList(« 𝔽(match.[[StartIndex]]), 𝔽(match.[[EndIndex]]) »).
1343 let indices = if has_indices {
1344 let match_record = Array::create_array_from_list(
1345 [match_value.start().into(), match_value.end().into()],
1346 context,
1347 );
1348
1349 // 25. Let indices be a new empty List.
1350 let indices = Array::array_create(n + 1, None, context)?;
1351
1352 // 27. Append match to indices.
1353 indices.create_data_property_or_throw(0, match_record, context)?;
1354 Some(indices)
1355 } else {
1356 None
1357 };
1358
1359 // 28. Let matchedSubstr be GetMatchString(S, match).
1360 let matched_substr = input.get_expect((last_index as usize)..(e));
1361
1362 // 29. Perform ! CreateDataPropertyOrThrow(A, "0", matchedSubstr).
1363 a.create_data_property_or_throw(0, matched_substr, context)?;
1364
1365 let named_groups = match_value
1366 .named_groups()
1367 .collect::<Vec<(&str, Option<Range>)>>();
1368
1369 // Combines:
1370 // 26. Let groupNames be a new empty List.
1371 // 30. If R contains any GroupName, then
1372 // 31. Else,
1373 // 33. For each integer i such that 1 ≤ i ≤ n, in ascending order, do
1374 #[allow(clippy::if_not_else)]
1375 let (groups, group_names) = if !named_groups.clone().is_empty() {
1376 // a. Let groups be OrdinaryObjectCreate(null).
1377 let groups = JsObject::with_null_proto();
1378 let group_names = JsObject::with_null_proto();
1379
1380 // e. If the ith capture of R was defined with a GroupName, then
1381 // i. Let s be the CapturingGroupName of that GroupName.
1382 // ii. Perform ! CreateDataPropertyOrThrow(groups, s, capturedValue).
1383 // iii. Append s to groupNames.
1384 for (name, range) in named_groups {
1385 let name = js_string!(name);
1386 if let Some(range) = range {
1387 let value = input.get_expect(range.clone());
1388
1389 groups.create_data_property_or_throw(name.clone(), value, context)?;
1390
1391 // 22.2.7.8 MakeMatchIndicesIndexPairArray ( S, indices, groupNames, hasGroups )
1392 // a. Let matchIndices be indices[i].
1393 // b. If matchIndices is not undefined, then
1394 // i. Let matchIndexPair be GetMatchIndexPair(S, matchIndices).
1395 // d. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(i)), matchIndexPair).
1396 group_names.create_data_property_or_throw(
1397 name.clone(),
1398 Array::create_array_from_list(
1399 [range.start.into(), range.end.into()],
1400 context,
1401 ),
1402 context,
1403 )?;
1404 } else {
1405 groups.create_data_property_or_throw(
1406 name.clone(),
1407 JsValue::undefined(),
1408 context,
1409 )?;
1410
1411 // 22.2.7.8 MakeMatchIndicesIndexPairArray ( S, indices, groupNames, hasGroups )
1412 // c. Else,
1413 // i. Let matchIndexPair be undefined.
1414 // d. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(i)), matchIndexPair).
1415 group_names.create_data_property_or_throw(
1416 name,
1417 JsValue::undefined(),
1418 context,
1419 )?;
1420 }
1421 }
1422
1423 (groups.into(), group_names.into())
1424 } else {
1425 // a. Let groups be undefined.
1426 (JsValue::undefined(), JsValue::undefined())
1427 };
1428
1429 // 22.2.7.8 MakeMatchIndicesIndexPairArray ( S, indices, groupNames, hasGroups )
1430 // 8. Perform ! CreateDataPropertyOrThrow(A, "groups", groups).
1431 if let Some(indices) = &indices {
1432 indices.create_data_property_or_throw(js_string!("groups"), group_names, context)?;
1433 }
1434
1435 // 32. Perform ! CreateDataPropertyOrThrow(A, "groups", groups).
1436 a.create_data_property_or_throw(js_string!("groups"), groups, context)?;
1437
1438 // 27. For each integer i such that i ≥ 1 and i ≤ n, in ascending order, do
1439 for i in 1..=n {
1440 // a. Let captureI be ith element of r's captures List.
1441 let capture = match_value.group(i as usize);
1442
1443 // b. If captureI is undefined, let capturedValue be undefined.
1444 // c. Else if fullUnicode is true, then
1445 // d. Else,
1446 let captured_value = capture.clone().map_or_else(JsValue::undefined, |range| {
1447 js_string!(input.get_expect(range)).into()
1448 });
1449
1450 // e. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(i)), capturedValue).
1451 a.create_data_property_or_throw(i, captured_value.clone(), context)?;
1452
1453 // 22.2.7.8 MakeMatchIndicesIndexPairArray ( S, indices, groupNames, hasGroups )
1454 if let Some(indices) = &indices {
1455 // b. If matchIndices is not undefined, then
1456 // i. Let matchIndexPair be GetMatchIndexPair(S, matchIndices).
1457 // c. Else,
1458 // i. Let matchIndexPair be undefined.
1459 let indices_range = capture.map_or_else(JsValue::undefined, |range| {
1460 Array::create_array_from_list([range.start.into(), range.end.into()], context)
1461 .into()
1462 });
1463
1464 // d. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(i)), matchIndexPair).
1465 indices.create_data_property_or_throw(i, indices_range, context)?;
1466 }
1467 }
1468
1469 // 34. If hasIndices is true, then
1470 // a. Let indicesArray be MakeMatchIndicesIndexPairArray(S, indices, groupNames, hasGroups).
1471 // b. Perform ! CreateDataPropertyOrThrow(A, "indices", indicesArray).
1472 if let Some(indices) = indices {
1473 a.create_data_property_or_throw(js_string!("indices"), indices, context)?;
1474 }
1475
1476 // 35. Return A.
1477 Ok(a)
1478 }
1479
1480 /// `RegExp.prototype[ @@match ]( string )`
1481 ///
1482 /// This method retrieves the matches when matching a string against a regular expression.
1483 ///
1484 /// More information:
1485 /// - [ECMAScript reference][spec]
1486 /// - [MDN documentation][mdn]
1487 ///
1488 /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@match
1489 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@match
1490 pub(crate) fn r#match(
1491 this: &JsValue,
1492 args: &[JsValue],
1493 context: &mut Context,
1494 ) -> JsResult<JsValue> {
1495 // 1. Let rx be the this value.
1496 // 2. If rx is not an Object, throw a TypeError exception.
1497 let Some(rx) = this.as_object() else {
1498 return Err(JsNativeError::typ()
1499 .with_message("RegExp.prototype.match method called on incompatible value")
1500 .into());
1501 };
1502
1503 // 3. Let S be ? ToString(string).
1504 let arg_str = args.get_or_undefined(0).to_string(context)?;
1505
1506 // 4. Let flags be ? ToString(? Get(rx, "flags")).
1507 let flags = rx.get(js_string!("flags"), context)?.to_string(context)?;
1508
1509 // 5. If flags does not contain "g", then
1510 if !flags.contains(b'g') {
1511 // a. Return ? RegExpExec(rx, S).
1512 return (Self::abstract_exec(&rx, arg_str, context)?)
1513 .map_or_else(|| Ok(JsValue::null()), |v| Ok(v.into()));
1514 }
1515
1516 // 6. Else,
1517
1518 // a. If flags contains "u" or flags contains "v", let fullUnicode be true. Otherwise, let fullUnicode be false.
1519 let full_unicode = flags.contains(b'u') || flags.contains(b'v');
1520
1521 // b. Perform ? Set(rx, "lastIndex", +0𝔽, true).
1522 rx.set(js_string!("lastIndex"), 0, true, context)?;
1523
1524 // c. Let A be ! ArrayCreate(0).
1525 let a = Array::array_create(0, None, context)?;
1526
1527 // d. Let n be 0.
1528 let mut n = 0;
1529
1530 // e. Repeat,
1531 loop {
1532 // i. Let result be ? RegExpExec(rx, S).
1533 let result = Self::abstract_exec(&rx, arg_str.clone(), context)?;
1534
1535 // ii. If result is null, then
1536 // iii. Else,
1537 if let Some(result) = result {
1538 // 1. Let matchStr be ? ToString(? Get(result, "0")).
1539 let match_str = result.get(0, context)?.to_string(context)?;
1540
1541 // 2. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), matchStr).
1542 a.create_data_property_or_throw(n, match_str.clone(), context)?;
1543
1544 // 3. If matchStr is the empty String, then
1545 if match_str.is_empty() {
1546 // a. Let thisIndex be ℝ(? ToLength(? Get(rx, "lastIndex"))).
1547 let this_index = rx
1548 .get(js_string!("lastIndex"), context)?
1549 .to_length(context)?;
1550
1551 // b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode).
1552 let next_index = advance_string_index(&arg_str, this_index, full_unicode);
1553
1554 // c. Perform ? Set(rx, "lastIndex", 𝔽(nextIndex), true).
1555 rx.set(
1556 js_string!("lastIndex"),
1557 JsValue::new(next_index),
1558 true,
1559 context,
1560 )?;
1561 }
1562
1563 // 4. Set n to n + 1.
1564 n += 1;
1565 } else {
1566 // 1. If n = 0, return null.
1567 if n == 0 {
1568 return Ok(JsValue::null());
1569 }
1570 // 2. Return A.
1571 return Ok(a.into());
1572 }
1573 }
1574 }
1575
1576 /// `RegExp.prototype.toString()`
1577 ///
1578 /// Return a string representing the regular expression.
1579 ///
1580 /// More information:
1581 /// - [ECMAScript reference][spec]
1582 /// - [MDN documentation][mdn]
1583 ///
1584 /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.tostring
1585 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/toString
1586 #[allow(clippy::wrong_self_convention)]
1587 pub(crate) fn to_string(
1588 this: &JsValue,
1589 _: &[JsValue],
1590 context: &mut Context,
1591 ) -> JsResult<JsValue> {
1592 // 1. Let R be the this value.
1593 // 2. If R is not an Object, throw a TypeError exception.
1594 let regexp = this.as_object().ok_or_else(|| {
1595 JsNativeError::typ()
1596 .with_message("RegExp.prototype.toString method called on incompatible value")
1597 })?;
1598
1599 // 3. Let pattern be ? ToString(? Get(R, "source")).
1600 let pattern = regexp
1601 .get(js_string!("source"), context)?
1602 .to_string(context)?;
1603
1604 // 4. Let flags be ? ToString(? Get(R, "flags")).
1605 let flags = regexp
1606 .get(js_string!("flags"), context)?
1607 .to_string(context)?;
1608
1609 // 5. Let result be the string-concatenation of "/", pattern, "/", and flags.
1610 // 6. Return result.
1611 Ok(js_string!(js_str!("/"), &pattern, js_str!("/"), &flags).into())
1612 }
1613
1614 /// `RegExp.prototype[ @@matchAll ]( string )`
1615 ///
1616 /// The `[@@matchAll]` method returns all matches of the regular expression against a string.
1617 ///
1618 /// More information:
1619 /// - [ECMAScript reference][spec]
1620 /// - [MDN documentation][mdn]
1621 ///
1622 /// [spec]: https://tc39.es/ecma262/#sec-regexp-prototype-matchall
1623 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@matchAll
1624 pub(crate) fn match_all(
1625 this: &JsValue,
1626 args: &[JsValue],
1627 context: &mut Context,
1628 ) -> JsResult<JsValue> {
1629 // 1. Let R be the this value.
1630 // 2. If Type(R) is not Object, throw a TypeError exception.
1631 let regexp = this.as_object().ok_or_else(|| {
1632 JsNativeError::typ()
1633 .with_message("RegExp.prototype.match_all method called on incompatible value")
1634 })?;
1635
1636 // 3. Let S be ? ToString(string).
1637 let arg_str = args.get_or_undefined(0).to_string(context)?;
1638
1639 // 4. Let C be ? SpeciesConstructor(R, %RegExp%).
1640 let c = regexp.species_constructor(StandardConstructors::regexp, context)?;
1641
1642 // 5. Let flags be ? ToString(? Get(R, "flags")).
1643 let flags = regexp
1644 .get(js_string!("flags"), context)?
1645 .to_string(context)?;
1646
1647 // 6. Let matcher be ? Construct(C, « R, flags »).
1648 let matcher = c.construct(&[this.clone(), flags.clone().into()], Some(&c), context)?;
1649
1650 // 7. Let lastIndex be ? ToLength(? Get(R, "lastIndex")).
1651 let last_index = regexp
1652 .get(js_string!("lastIndex"), context)?
1653 .to_length(context)?;
1654
1655 // 8. Perform ? Set(matcher, "lastIndex", lastIndex, true).
1656 matcher.set(js_string!("lastIndex"), last_index, true, context)?;
1657
1658 // 9. If flags contains "g", let global be true.
1659 // 10. Else, let global be false.
1660 let global = flags.contains(b'g');
1661
1662 // 11. If flags contains "u", let fullUnicode be true.
1663 // 12. Else, let fullUnicode be false.
1664 let unicode = flags.contains(b'u') || flags.contains(b'v');
1665
1666 // 13. Return ! CreateRegExpStringIterator(matcher, S, global, fullUnicode).
1667 Ok(RegExpStringIterator::create_regexp_string_iterator(
1668 matcher.clone(),
1669 arg_str,
1670 global,
1671 unicode,
1672 context,
1673 ))
1674 }
1675
1676 /// `RegExp.prototype [ @@replace ] ( string, replaceValue )`
1677 ///
1678 /// The [@@replace]() method replaces some or all matches of a this pattern in a string by a replacement,
1679 /// and returns the result of the replacement as a new string.
1680 /// The replacement can be a string or a function to be called for each match.
1681 ///
1682 /// More information:
1683 /// - [ECMAScript reference][spec]
1684 /// - [MDN documentation][mdn]
1685 ///
1686 /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@replace
1687 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@replace
1688 pub(crate) fn replace(
1689 this: &JsValue,
1690 args: &[JsValue],
1691 context: &mut Context,
1692 ) -> JsResult<JsValue> {
1693 // Helper enum.
1694 enum CallableOrString {
1695 FunctionalReplace(JsObject),
1696 ReplaceValue(JsString),
1697 }
1698
1699 // 1. Let rx be the this value.
1700 // 2. If rx is not an Object, throw a TypeError exception.
1701 let rx = this.as_object().ok_or_else(|| {
1702 JsNativeError::typ().with_message(
1703 "RegExp.prototype[Symbol.replace] method called on incompatible value",
1704 )
1705 })?;
1706
1707 // 3. Let S be ? ToString(string).
1708 let s = args.get_or_undefined(0).to_string(context)?;
1709
1710 // 4. Let lengthS be the length of S.
1711 let length_s = s.len();
1712
1713 let replace_value = args.get_or_undefined(1);
1714
1715 // 5. Let functionalReplace be IsCallable(replaceValue).
1716 let functional_replace = replace_value.as_callable();
1717
1718 // 6. If functionalReplace is false, then
1719 let replace_value = if let Some(callable) = functional_replace {
1720 CallableOrString::FunctionalReplace(callable)
1721 } else {
1722 // a. Set replaceValue to ? ToString(replaceValue).
1723 CallableOrString::ReplaceValue(replace_value.to_string(context)?)
1724 };
1725
1726 // 7. Let flags be ? ToString(? Get(rx, "flags")).
1727 let flags = rx.get(js_string!("flags"), context)?.to_string(context)?;
1728
1729 // 8. If flags contains "g", let global be true. Otherwise, let global be false.
1730 let global = flags.contains(b'g');
1731
1732 // 9. If global is true, then
1733 let full_unicode = if global {
1734 // a. If flags contains "u", let fullUnicode be true. Otherwise, let fullUnicode be false.
1735 let full_unicode = flags.contains(b'u');
1736
1737 // b. Perform ? Set(rx, "lastIndex", +0𝔽, true).
1738 rx.set(js_string!("lastIndex"), 0, true, context)?;
1739
1740 full_unicode
1741 } else {
1742 false
1743 };
1744
1745 // 10. Let results be a new empty List.
1746 let mut results = Vec::new();
1747
1748 // SKIPPED: 11. Let done be false.
1749 //
1750 // NOTE(HalidOdat): We don't keep track of `done`, we just break when done is true.
1751
1752 // 12. Repeat, while done is false,
1753 loop {
1754 // a. Let result be ? RegExpExec(rx, S).
1755 let result = Self::abstract_exec(&rx, s.clone(), context)?;
1756
1757 // b. If result is null, set done to true.
1758 let Some(result) = result else {
1759 // SKIPPED: 1. Set done to true.
1760 break;
1761 };
1762
1763 // c. Else,
1764 // i. Append result to results.
1765 results.push(result.clone());
1766
1767 // ii. If global is false, then
1768 if !global {
1769 // SKIPPED: 1. Set done to true.
1770 break;
1771 }
1772
1773 // iii. Else,
1774 // 1. Let matchStr be ? ToString(? Get(result, "0")).
1775 let match_str = result.get(0, context)?.to_string(context)?;
1776
1777 // 2. If matchStr is the empty String, then
1778 if match_str.is_empty() {
1779 // a. Let thisIndex be ℝ(? ToLength(? Get(rx, "lastIndex"))).
1780 let this_index = rx
1781 .get(js_string!("lastIndex"), context)?
1782 .to_length(context)?;
1783
1784 // b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode).
1785 let next_index = advance_string_index(&s, this_index, full_unicode);
1786
1787 // c. Perform ? Set(rx, "lastIndex", 𝔽(nextIndex), true).
1788 rx.set(
1789 js_string!("lastIndex"),
1790 JsValue::new(next_index),
1791 true,
1792 context,
1793 )?;
1794 }
1795 }
1796
1797 // 16. If nextSourcePosition ≥ lengthS, return accumulatedResult.
1798 // 17. Return the string-concatenation of accumulatedResult and the substring of S from nextSourcePosition.
1799
1800 // 13. Let accumulatedResult be the empty String.
1801 let mut accumulated_result = vec![];
1802
1803 // 14. Let nextSourcePosition be 0.
1804 let mut next_source_position = 0;
1805
1806 // 15. For each element result of results, do
1807 for result in results {
1808 // a. Let resultLength be ? LengthOfArrayLike(result).
1809 let result_length = result.length_of_array_like(context)? as i64;
1810
1811 // b. Let nCaptures be max(resultLength - 1, 0).
1812 let n_captures = std::cmp::max(result_length - 1, 0);
1813
1814 // c. Let matched be ? ToString(? Get(result, "0")).
1815 let matched = result.get(0, context)?.to_string(context)?;
1816
1817 // d. Let matchLength be the length of matched.
1818 let match_length = matched.len();
1819
1820 // e. Let position be ? ToIntegerOrInfinity(? Get(result, "index")).
1821 let position = result
1822 .get(js_string!("index"), context)?
1823 .to_integer_or_infinity(context)?;
1824
1825 // f. Set position to the result of clamping position between 0 and lengthS.
1826 let position = position.clamp_finite(0, length_s as i64) as usize;
1827
1828 // g. Let captures be a new empty List.
1829 let mut captures = Vec::new();
1830
1831 // h. Let n be 1.
1832 // i. Repeat, while n ≤ nCaptures,
1833 for n in 1..=n_captures {
1834 // i. Let capN be ? Get(result, ! ToString(𝔽(n))).
1835 let mut cap_n = result.get(n, context)?;
1836
1837 // ii. If capN is not undefined, then
1838 if !cap_n.is_undefined() {
1839 // 1. Set capN to ? ToString(capN).
1840 cap_n = cap_n.to_string(context)?.into();
1841 }
1842
1843 // iii. Append capN to captures.
1844 captures.push(cap_n);
1845
1846 // iv. NOTE: When n = 1, the preceding step puts the first element into captures (at index 0).
1847 // More generally, the nth capture (the characters captured by the nth set of capturing parentheses)
1848 // is at captures[n - 1].
1849 //
1850 // v. Set n to n + 1.
1851 }
1852
1853 // j. Let namedCaptures be ? Get(result, "groups").
1854 let mut named_captures = result.get(js_string!("groups"), context)?;
1855
1856 let replacement = match replace_value {
1857 // k. If functionalReplace is true, then
1858 CallableOrString::FunctionalReplace(ref replace_value) => {
1859 // i. Let replacerArgs be the list-concatenation of « matched », captures, and « 𝔽(position), S ».
1860 let mut replacer_args = vec![JsValue::new(matched)];
1861 replacer_args.extend(captures);
1862 replacer_args.push(position.into());
1863 replacer_args.push(s.clone().into());
1864
1865 // ii. If namedCaptures is not undefined, then
1866 if !named_captures.is_undefined() {
1867 // 1. Append namedCaptures to replacerArgs.
1868 replacer_args.push(named_captures);
1869 }
1870
1871 // iii. Let replValue be ? Call(replaceValue, undefined, replacerArgs).
1872 let repl_value =
1873 replace_value.call(&JsValue::undefined(), &replacer_args, context)?;
1874
1875 // iv. Let replacement be ? ToString(replValue).
1876 repl_value.to_string(context)?
1877 }
1878 // l. Else,
1879 CallableOrString::ReplaceValue(ref replace_value) => {
1880 // i. If namedCaptures is not undefined, then
1881 if !named_captures.is_undefined() {
1882 // 1. Set namedCaptures to ? ToObject(namedCaptures).
1883 named_captures = named_captures.to_object(context)?.into();
1884 }
1885
1886 // ii. Let replacement be ? GetSubstitution(matched, S, position, captures, namedCaptures, replaceValue).
1887 string::get_substitution(
1888 &matched,
1889 &s,
1890 position,
1891 &captures,
1892 &named_captures,
1893 replace_value,
1894 context,
1895 )?
1896 }
1897 };
1898
1899 // m. If position ≥ nextSourcePosition, then
1900 if position >= next_source_position {
1901 // i. NOTE: position should not normally move backwards.
1902 // If it does, it is an indication of an ill-behaving RegExp subclass or use of
1903 // an access triggered side-effect to change the global flag or other characteristics of rx.
1904 // In such cases, the corresponding substitution is ignored.
1905
1906 // ii. Set accumulatedResult to the string-concatenation of accumulatedResult, the substring of S from nextSourcePosition to position, and replacement.
1907 accumulated_result.extend(s.get_expect(next_source_position..position).iter());
1908 accumulated_result.extend(replacement.iter());
1909
1910 // iii. Set nextSourcePosition to position + matchLength.
1911 next_source_position = position + match_length;
1912 }
1913 }
1914
1915 // 16. If nextSourcePosition ≥ lengthS, return accumulatedResult.
1916 if next_source_position >= length_s {
1917 return Ok(js_string!(&accumulated_result[..]).into());
1918 }
1919
1920 // 17. Return the string-concatenation of accumulatedResult and the substring of S from nextSourcePosition.
1921 Ok(js_string!(
1922 &JsString::from(&accumulated_result[..]),
1923 &s.get_expect(next_source_position..)
1924 )
1925 .into())
1926 }
1927
1928 /// `RegExp.prototype[ @@search ]( string )`
1929 ///
1930 /// This method executes a search for a match between a this regular expression and a string.
1931 ///
1932 /// More information:
1933 /// - [ECMAScript reference][spec]
1934 /// - [MDN documentation][mdn]
1935 ///
1936 /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@search
1937 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@search
1938 pub(crate) fn search(
1939 this: &JsValue,
1940 args: &[JsValue],
1941 context: &mut Context,
1942 ) -> JsResult<JsValue> {
1943 // 1. Let rx be the this value.
1944 // 2. If Type(rx) is not Object, throw a TypeError exception.
1945 let rx = this.as_object().ok_or_else(|| {
1946 JsNativeError::typ()
1947 .with_message("RegExp.prototype[Symbol.search] method called on incompatible value")
1948 })?;
1949
1950 // 3. Let S be ? ToString(string).
1951 let arg_str = args.get_or_undefined(0).to_string(context)?;
1952
1953 // 4. Let previousLastIndex be ? Get(rx, "lastIndex").
1954 let previous_last_index = rx.get(js_string!("lastIndex"), context)?;
1955
1956 // 5. If SameValue(previousLastIndex, +0𝔽) is false, then
1957 if !JsValue::same_value(&previous_last_index, &JsValue::new(0)) {
1958 // a. Perform ? Set(rx, "lastIndex", +0𝔽, true).
1959 rx.set(js_string!("lastIndex"), 0, true, context)?;
1960 }
1961
1962 // 6. Let result be ? RegExpExec(rx, S).
1963 let result = Self::abstract_exec(&rx, arg_str, context)?;
1964
1965 // 7. Let currentLastIndex be ? Get(rx, "lastIndex").
1966 let current_last_index = rx.get(js_string!("lastIndex"), context)?;
1967
1968 // 8. If SameValue(currentLastIndex, previousLastIndex) is false, then
1969 if !JsValue::same_value(¤t_last_index, &previous_last_index) {
1970 // a. Perform ? Set(rx, "lastIndex", previousLastIndex, true).
1971 rx.set(js_string!("lastIndex"), previous_last_index, true, context)?;
1972 }
1973
1974 // 9. If result is null, return -1𝔽.
1975 // 10. Return ? Get(result, "index").
1976 result.map_or_else(
1977 || Ok(JsValue::new(-1)),
1978 |result| result.get(js_string!("index"), context),
1979 )
1980 }
1981
1982 /// `RegExp.prototype [ @@split ] ( string, limit )`
1983 ///
1984 /// The [@@split]() method splits a String object into an array of strings by separating the string into substrings.
1985 ///
1986 /// More information:
1987 /// - [ECMAScript reference][spec]
1988 /// - [MDN documentation][mdn]
1989 ///
1990 /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@split
1991 /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@split
1992 pub(crate) fn split(
1993 this: &JsValue,
1994 args: &[JsValue],
1995 context: &mut Context,
1996 ) -> JsResult<JsValue> {
1997 // 1. Let rx be the this value.
1998 // 2. If Type(rx) is not Object, throw a TypeError exception.
1999 let rx = this.as_object().ok_or_else(|| {
2000 JsNativeError::typ()
2001 .with_message("RegExp.prototype.split method called on incompatible value")
2002 })?;
2003
2004 // 3. Let S be ? ToString(string).
2005 let arg_str = args.get_or_undefined(0).to_string(context)?;
2006
2007 // 4. Let C be ? SpeciesConstructor(rx, %RegExp%).
2008 let constructor = rx.species_constructor(StandardConstructors::regexp, context)?;
2009
2010 // 5. Let flags be ? ToString(? Get(rx, "flags")).
2011 let flags = rx.get(js_string!("flags"), context)?.to_string(context)?;
2012
2013 // 6. If flags contains "u", let unicodeMatching be true.
2014 // 7. Else, let unicodeMatching be false.
2015 let unicode = flags.contains(b'u');
2016
2017 // 8. If flags contains "y", let newFlags be flags.
2018 // 9. Else, let newFlags be the string-concatenation of flags and "y".
2019 let new_flags = if flags.contains(b'y') {
2020 flags
2021 } else {
2022 js_string!(&flags, js_str!("y"))
2023 };
2024
2025 // 10. Let splitter be ? Construct(C, « rx, newFlags »).
2026 let splitter = constructor.construct(
2027 &[this.clone(), new_flags.into()],
2028 Some(&constructor),
2029 context,
2030 )?;
2031
2032 // 11. Let A be ! ArrayCreate(0).
2033 let a = Array::array_create(0, None, context)?;
2034
2035 // 12. Let lengthA be 0.
2036 let mut length_a = 0;
2037
2038 // 13. If limit is undefined, let lim be 2^32 - 1; else let lim be ℝ(? ToUint32(limit)).
2039 let limit = args.get_or_undefined(1);
2040 let lim = if limit.is_undefined() {
2041 u32::MAX
2042 } else {
2043 limit.to_u32(context)?
2044 };
2045
2046 // 14. If lim is 0, return A.
2047 if lim == 0 {
2048 return Ok(a.into());
2049 }
2050
2051 // 15. Let size be the length of S.
2052 let size = arg_str.len() as u64;
2053
2054 // 16. If size is 0, then
2055 if size == 0 {
2056 // a. Let z be ? RegExpExec(splitter, S).
2057 let result = Self::abstract_exec(&splitter, arg_str.clone(), context)?;
2058
2059 // b. If z is not null, return A.
2060 if result.is_some() {
2061 return Ok(a.into());
2062 }
2063
2064 // c. Perform ! CreateDataPropertyOrThrow(A, "0", S).
2065 a.create_data_property_or_throw(0, arg_str, context)?;
2066
2067 // d. Return A.
2068 return Ok(a.into());
2069 }
2070
2071 // With %RegExp% as C and the intrinsic `exec`, the splitter is a fresh RegExp that no user code
2072 // can reach, so a sticky match tried at each q is one forward search from q.
2073 let regexp = context.intrinsics().constructors().regexp();
2074 if JsObject::equals(&constructor, ®exp.constructor()) {
2075 let exec = regexp
2076 .prototype()
2077 .borrow()
2078 .properties()
2079 .get(&js_string!("exec").into())
2080 .and_then(|desc| desc.value().cloned());
2081 if let Some(splitter) =
2082 exec.and_then(|exec| Self::intrinsic_exec_target(&splitter, &exec))
2083 {
2084 return Self::split_by_search(&splitter, &arg_str, &a, lim, unicode, context);
2085 }
2086 }
2087
2088 // 17. Let p be 0.
2089 // 18. Let q be p.
2090 let mut p = 0;
2091 let mut q = p;
2092
2093 // 19. Repeat, while q < size,
2094 while q < size {
2095 // a. Perform ? Set(splitter, "lastIndex", 𝔽(q), true).
2096 splitter.set(js_string!("lastIndex"), JsValue::new(q), true, context)?;
2097
2098 // b. Let z be ? RegExpExec(splitter, S).
2099 let result = Self::abstract_exec(&splitter, arg_str.clone(), context)?;
2100
2101 // c. If z is null, set q to AdvanceStringIndex(S, q, unicodeMatching).
2102 // d. Else,
2103 if let Some(result) = result {
2104 // i. Let e be ℝ(? ToLength(? Get(splitter, "lastIndex"))).
2105 let mut e = splitter
2106 .get(js_string!("lastIndex"), context)?
2107 .to_length(context)?;
2108
2109 // ii. Set e to min(e, size).
2110 e = std::cmp::min(e, size);
2111
2112 // iii. If e = p, set q to AdvanceStringIndex(S, q, unicodeMatching).
2113 // iv. Else,
2114 if e == p {
2115 q = advance_string_index(&arg_str, q, unicode);
2116 } else {
2117 // 1. Let T be the substring of S from p to q.
2118 let arg_str_substring = arg_str.get_expect(p as usize..q as usize);
2119
2120 // 2. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T).
2121 a.create_data_property_or_throw(length_a, arg_str_substring, context)?;
2122
2123 // 3. Set lengthA to lengthA + 1.
2124 length_a += 1;
2125
2126 // 4. If lengthA = lim, return A.
2127 if length_a == lim {
2128 return Ok(a.into());
2129 }
2130
2131 // 5. Set p to e.
2132 p = e;
2133
2134 // 6. Let numberOfCaptures be ? LengthOfArrayLike(z).
2135 let mut number_of_captures = result.length_of_array_like(context)? as isize;
2136
2137 // 7. Set numberOfCaptures to max(numberOfCaptures - 1, 0).
2138 number_of_captures = std::cmp::max(number_of_captures - 1, 0);
2139
2140 // 8. Let i be 1.
2141 // 9. Repeat, while i ≤ numberOfCaptures,
2142 for i in 1..=number_of_captures {
2143 // a. Let nextCapture be ? Get(z, ! ToString(𝔽(i))).
2144 let next_capture = result.get(i, context)?;
2145
2146 // b. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), nextCapture).
2147 a.create_data_property_or_throw(length_a, next_capture, context)?;
2148
2149 // d. Set lengthA to lengthA + 1.
2150 length_a += 1;
2151
2152 // e. If lengthA = lim, return A.
2153 if length_a == lim {
2154 return Ok(a.into());
2155 }
2156 }
2157
2158 // 10. Set q to p.
2159 q = p;
2160 }
2161 } else {
2162 q = advance_string_index(&arg_str, q, unicode);
2163 }
2164 }
2165
2166 // 20. Let T be the substring of S from p to size.
2167 let arg_str_substring = arg_str.get_expect(p as usize..size as usize);
2168
2169 // 21. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T).
2170 a.create_data_property_or_throw(length_a, arg_str_substring, context)?;
2171
2172 // 22. Return A.
2173 Ok(a.into())
2174 }
2175
2176 /// Steps 17 to 22 of `RegExp.prototype [ %Symbol.split% ]` for a splitter only the engine can
2177 /// observe: each piece is one forward search on the input widened once, where the spec loop
2178 /// runs a sticky match at every code unit.
2179 fn split_by_search(
2180 splitter: &JsObject<RegExp>,
2181 string: &JsString,
2182 array: &JsObject,
2183 lim: u32,
2184 unicode: bool,
2185 context: &mut Context,
2186 ) -> JsResult<JsValue> {
2187 let rx = splitter.borrow().data().clone();
2188 let full_unicode =
2189 rx.flags.contains(RegExpFlags::UNICODE) || rx.flags.contains(RegExpFlags::UNICODE_SETS);
2190 let widened;
2191 let (input, utf16): (&[u16], bool) = match string.as_str().variant() {
2192 JsStrVariant::Latin1(bytes) => {
2193 widened = bytes
2194 .iter()
2195 .map(|&byte| u16::from(byte))
2196 .collect::<Vec<_>>();
2197 (&widened, false)
2198 }
2199 JsStrVariant::Utf16(units) => (units, full_unicode),
2200 };
2201 let size = input.len();
2202 let mut length_a = 0;
2203 let mut p = 0;
2204 let mut q = 0;
2205 while q < size {
2206 let found = if utf16 {
2207 rx.matcher.find_from_utf16(input, q).next()
2208 } else {
2209 rx.matcher.find_from_ucs2(input, q).next()
2210 };
2211 // The first q at which a sticky match succeeds, if any is below size.
2212 let Some(found) = found.filter(|found| found.start() < size) else {
2213 break;
2214 };
2215 let end = found.end().min(size);
2216 if end == p {
2217 q = advance_string_index(string, found.start() as u64, unicode) as usize;
2218 continue;
2219 }
2220 array.create_data_property_or_throw(
2221 length_a,
2222 string.get_expect(p..found.start()),
2223 context,
2224 )?;
2225 length_a += 1;
2226 if length_a == lim {
2227 return Ok(array.clone().into());
2228 }
2229 p = end;
2230 for capture in &found.captures {
2231 let value = capture
2232 .clone()
2233 .map_or_else(JsValue::undefined, |range| string.get_expect(range).into());
2234 array.create_data_property_or_throw(length_a, value, context)?;
2235 length_a += 1;
2236 if length_a == lim {
2237 return Ok(array.clone().into());
2238 }
2239 }
2240 q = p;
2241 }
2242 array.create_data_property_or_throw(length_a, string.get_expect(p..size), context)?;
2243 Ok(array.clone().into())
2244 }
2245
2246 /// [`RegExp.prototype.compile ( pattern, flags )`][spec]
2247 ///
2248 /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.compile
2249 #[cfg(feature = "annex-b")]
2250 fn compile(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2251 // 1. Let O be the this value.
2252 // 2. Perform ? RequireInternalSlot(O, [[RegExpMatcher]]).
2253
2254 let this = this
2255 .as_object()
2256 .filter(|o| o.is::<RegExp>())
2257 .ok_or_else(|| {
2258 JsNativeError::typ()
2259 .with_message("`RegExp.prototype.compile` cannot be called for a non-object")
2260 })?;
2261 let pattern = args.get_or_undefined(0);
2262 let flags = args.get_or_undefined(1);
2263 // 3. If pattern is an Object and pattern has a [[RegExpMatcher]] internal slot, then
2264 let (pattern, flags) = if let Some((p, f)) = pattern.as_object().and_then(|o| {
2265 o.downcast_ref::<RegExp>()
2266 .map(|rx| (rx.original_source.clone(), rx.original_flags.clone()))
2267 }) {
2268 // a. If flags is not undefined, throw a TypeError exception.
2269 if !flags.is_undefined() {
2270 return Err(JsNativeError::typ()
2271 .with_message(
2272 "`RegExp.prototype.compile` cannot be \
2273 called with both a RegExp initializer and new flags",
2274 )
2275 .into());
2276 }
2277 // b. Let P be pattern.[[OriginalSource]].
2278 // c. Let F be pattern.[[OriginalFlags]].
2279 (p.into(), f.into())
2280 } else {
2281 // 4. Else,
2282 // a. Let P be pattern.
2283 // b. Let F be flags.
2284 (pattern.clone(), flags.clone())
2285 };
2286
2287 let regexp = Self::compile_native_regexp(&pattern, &flags, context)?;
2288
2289 // 5. Return ? RegExpInitialize(O, P, F).
2290 {
2291 *this
2292 .downcast_mut::<RegExp>()
2293 .expect("already checked that the object was a RegExp") = regexp;
2294 }
2295
2296 this.set(js_string!("lastIndex"), 0, true, context)?;
2297
2298 Ok(this.into())
2299 }
2300}
2301
2302/// `22.2.5.2.3 AdvanceStringIndex ( S, index, unicode )`
2303///
2304/// More information:
2305/// - [ECMAScript reference][spec]
2306///
2307/// [spec]: https://tc39.es/ecma262/#sec-advancestringindex
2308fn advance_string_index(s: &JsString, index: u64, unicode: bool) -> u64 {
2309 // Regress only works with utf8, so this function differs from the spec.
2310
2311 // 1. Assert: index ≤ 2^53 - 1.
2312
2313 // 2. If unicode is false, return index + 1.
2314 if !unicode {
2315 return index + 1;
2316 }
2317
2318 // 3. Let length be the number of code units in S.
2319 let length = s.len() as u64;
2320
2321 // 4. If index + 1 ≥ length, return index + 1.
2322 if index + 1 > length {
2323 return index + 1;
2324 }
2325
2326 // 5. Let cp be ! CodePointAt(S, index).
2327 let code_point = s.code_point_at(index as usize);
2328
2329 index + code_point.code_unit_count() as u64
2330}