From f87ef83be33b994a6a3ab00b4115010ad3e4b09a Mon Sep 17 00:00:00 2001 From: Kevin Castro Date: Sat, 19 Sep 2026 13:35:22 -0700 Subject: [PATCH 1/2] test(regexp): cover RegExp.prototype.source escaping of `/` and line terminators `/a\/b/.source` currently returns `a\\/b` instead of `a\/b`, so these tests fail until the following commit. --- core/engine/src/builtins/regexp/tests.rs | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/core/engine/src/builtins/regexp/tests.rs b/core/engine/src/builtins/regexp/tests.rs index 0897b682720..0a537884b26 100644 --- a/core/engine/src/builtins/regexp/tests.rs +++ b/core/engine/src/builtins/regexp/tests.rs @@ -175,6 +175,55 @@ fn to_string() { TestAction::assert_eq(r"/,\;/.toString()", js_string!(r"/,\;/")), ]); } +#[test] +fn source_escapes_slash() { + run_test_actions([ + TestAction::assert_eq(r#"new RegExp("/").source"#, js_string!(r"\/")), + TestAction::assert_eq(r"/a\/b/.source", js_string!(r"a\/b")), + TestAction::assert_eq(r#"new RegExp("\\/").source"#, js_string!(r"\/")), + // The backslash is escaped, the `/` is not. + TestAction::assert_eq(r#"new RegExp("\\\\/").source"#, js_string!(r"\\\/")), + ]); +} + +#[test] +fn source_escapes_line_terminators() { + run_test_actions([ + TestAction::assert_eq(r#"new RegExp("\n").source"#, js_string!(r"\n")), + TestAction::assert_eq(r#"new RegExp("\\\n").source"#, js_string!(r"\n")), + TestAction::assert_eq(r#"new RegExp("\\\r").source"#, js_string!(r"\r")), + TestAction::assert_eq(r#"new RegExp("\\\u2028").source"#, js_string!(r"\u2028")), + TestAction::assert_eq(r#"new RegExp("\\\u2029").source"#, js_string!(r"\u2029")), + // The backslash is escaped, the line terminator is not. + TestAction::assert_eq(r#"new RegExp("\\\\\n").source"#, js_string!(r"\\\n")), + ]); +} + +#[test] +fn source_round_trips() { + run_test_actions([ + TestAction::run(indoc! {r#" + function roundTrips(re, inputs) { + var copies = [ + new RegExp(re.source, re.flags), + eval("/" + re.source + "/" + re.flags), + ]; + return copies.every(function (copy) { + return inputs.every(function (input) { + return copy.test(input) === re.test(input); + }); + }); + } + "#}), + TestAction::assert(r#"roundTrips(/a\/b/, ["a/b", "a\\/b"])"#), + TestAction::assert(r#"roundTrips(new RegExp("/"), ["/", "\\/"])"#), + TestAction::assert(r#"roundTrips(new RegExp("\\\\/"), ["/", "\\/", "\\"])"#), + TestAction::assert(r#"roundTrips(new RegExp("[/]"),["/", "\\"])"#), + TestAction::assert(r#"roundTrips(new RegExp("\\\n"), ["\n", "\\n"])"#), + TestAction::assert(r#"roundTrips(new RegExp("\\\u2028"), ["\u2028", "\\u2028"])"#), + ]); +} + #[test] fn search() { const ERROR: &str = "RegExp.prototype[Symbol.search] method called on incompatible value"; From 318d8b95558ba58704fe515833738b31a44698f7 Mon Sep 17 00:00:00 2001 From: Kevin Castro Date: Sat, 19 Sep 2026 13:35:52 -0700 Subject: [PATCH 2/2] fix(regexp): don't double-escape `/` and line terminators in RegExp.prototype.source `EscapeRegExpPattern` escaped every `/` and line terminator, even when the pattern had already escaped it, so `/a\/b/.source` returned `a\\/b` and `new RegExp(re.source, re.flags)` no longer behaved like `re`. Track whether the current code point is preceded by an unescaped backslash and, if so, don't add a second one. This matches V8. --- core/engine/src/builtins/regexp/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/engine/src/builtins/regexp/mod.rs b/core/engine/src/builtins/regexp/mod.rs index 037eed6eb19..2e6a4eb87e1 100644 --- a/core/engine/src/builtins/regexp/mod.rs +++ b/core/engine/src/builtins/regexp/mod.rs @@ -798,7 +798,17 @@ impl RegExp { } else { let mut s = Vec::with_capacity(src.len()); let mut buf = [0; 2]; + let mut escaped = false; for c in src.code_points() { + // Drop the pattern's own `\` so the code point is not escaped twice. + if escaped + && matches!( + c, + CodePoint::Unicode('/' | '\n' | '\r' | '\u{2028}' | '\u{2029}') + ) + { + s.pop(); + } match c { CodePoint::Unicode('/') => s.extend_from_slice(utf16!(r"\/")), CodePoint::Unicode('\n') => s.extend_from_slice(utf16!(r"\n")), @@ -808,6 +818,7 @@ impl RegExp { CodePoint::Unicode(c) => s.extend_from_slice(c.encode_utf16(&mut buf)), CodePoint::UnpairedSurrogate(surr) => s.push(surr), } + escaped = !escaped && c == CodePoint::Unicode('\\'); } JsValue::new(js_string!(&s[..]))