From 52ae63f4fbb98f2fa66937b487f8ea397cbccf73 Mon Sep 17 00:00:00 2001 From: Boshen Date: Fri, 25 Sep 2026 13:20:11 +0000 Subject: [PATCH] feat(regular_expression): support buffer boundary assertions (#27048) Port https://github.com/jviereck/regjsparser/pull/166 with spec-conforming buffer boundaries and all 90 upstream cases. AI-assisted. --- crates/oxc_regular_expression/README.md | 1 + crates/oxc_regular_expression/src/ast.rs | 9 +- .../src/ast_impl/display.rs | 7 ++ .../oxc_regular_expression/src/parser/mod.rs | 92 +++++++++++++++++++ .../pattern_parser/pattern_parser_impl.rs | 9 ++ 5 files changed, 117 insertions(+), 1 deletion(-) diff --git a/crates/oxc_regular_expression/README.md b/crates/oxc_regular_expression/README.md index d154516dbd..20f50fc3b4 100644 --- a/crates/oxc_regular_expression/README.md +++ b/crates/oxc_regular_expression/README.md @@ -10,3 +10,4 @@ And, Stage 4 proposals - https://github.com/tc39/proposal-duplicate-named-capturing-groups - https://github.com/tc39/proposal-regexp-modifiers +- https://github.com/tc39/proposal-regexp-buffer-boundaries diff --git a/crates/oxc_regular_expression/src/ast.rs b/crates/oxc_regular_expression/src/ast.rs index 5c9fbf8757..fcb43cf4c5 100644 --- a/crates/oxc_regular_expression/src/ast.rs +++ b/crates/oxc_regular_expression/src/ast.rs @@ -55,7 +55,7 @@ pub enum Term<'a> { } /// Simple form of assertion. -/// e.g. `^`, `$`, `\b`, `\B` +/// e.g. `^`, `$`, `\b`, `\B`, `\A`, `\z`, `\Z` #[ast] #[derive(Debug)] #[generate_derive(CloneIn, ContentEq)] @@ -72,6 +72,13 @@ pub enum BoundaryAssertionKind { End = 1, Boundary = 2, NegativeBoundary = 3, + /// `\A`: start of input, regardless of multiline mode. Unicode mode only. + StartBuffer = 4, + /// `\z`: end of input, regardless of multiline mode. Unicode mode only. + EndBuffer = 5, + /// `\Z`: end of input or before a final line terminator or CRLF sequence, + /// regardless of multiline mode. Unicode mode only. + EndBufferOptionalNewline = 6, } /// Lookaround assertion. diff --git a/crates/oxc_regular_expression/src/ast_impl/display.rs b/crates/oxc_regular_expression/src/ast_impl/display.rs index 3d26beb74a..a603da035f 100644 --- a/crates/oxc_regular_expression/src/ast_impl/display.rs +++ b/crates/oxc_regular_expression/src/ast_impl/display.rs @@ -74,6 +74,9 @@ impl Display for BoundaryAssertionKind { Self::End => "$", Self::Boundary => r"\b", Self::NegativeBoundary => r"\B", + Self::StartBuffer => r"\A", + Self::EndBuffer => r"\z", + Self::EndBufferOptionalNewline => r"\Z", }; f.write_str(s) } @@ -533,6 +536,10 @@ mod test { (r"/(?im-s:.)/u", None), (r"/(?m-is:.)/v", None), (r"/(?smi:.)/v", Some(r"/(?ims:.)/v")), + // Buffer boundaries + (r"/\A\z\Z/", None), + (r"/\A\z\Z/u", None), + (r"/\A\z\Z/v", None), ]; #[test] diff --git a/crates/oxc_regular_expression/src/parser/mod.rs b/crates/oxc_regular_expression/src/parser/mod.rs index f97e394bf2..101dabddb6 100644 --- a/crates/oxc_regular_expression/src/parser/mod.rs +++ b/crates/oxc_regular_expression/src/parser/mod.rs @@ -123,6 +123,83 @@ mod test { (r"(?si-m:.)", ""), (r"(?im-s:.)", "v"), (r"(?ims-:.)", ""), + // Buffer boundaries + (r"\A", ""), + (r"\A", "u"), + (r"\A", "v"), + (r"\z", ""), + (r"\z", "u"), + (r"\z", "v"), + (r"\Z", ""), + (r"\Z", "u"), + (r"\Z", "v"), + (r"\A\z", ""), + (r"\A\z", "u"), + (r"\A\z", "v"), + (r"\A\Z", ""), + (r"\A\Z", "u"), + (r"\A\Z", "v"), + (r"\A\Z\z", ""), + (r"\A\Z\z", "u"), + (r"\A\Z\z", "v"), + (r"\Afoo\z", ""), + (r"\Afoo\z", "u"), + (r"\Afoo\z", "v"), + (r"\Afoo\Z", ""), + (r"\Afoo\Z", "u"), + (r"\Afoo\Z", "v"), + (r"\A|\z", ""), + (r"\A|\z", "u"), + (r"\A|\z", "v"), + (r"(\A)", ""), + (r"(\A)", "u"), + (r"(\A)", "v"), + (r"(?=\A)", ""), + (r"(?=\A)", "u"), + (r"(?=\A)", "v"), + (r"\A*", ""), + (r"\z+", ""), + (r"\Z{2}", ""), + (r"[\A]", ""), + (r"[\z\Z]", ""), + (r"\\A", ""), + (r"\\A", "u"), + (r"\\A", "v"), + (r"a\Zb", ""), + (r"a\Zb", "u"), + (r"a\Zb", "v"), + (r"(?i:\A)", ""), + (r"(?i:\A)", "u"), + (r"(?i:\A)", "v"), + (r"(?m:\z)", ""), + (r"(?m:\z)", "u"), + (r"(?m:\z)", "v"), + (r"(?s:\Z)", ""), + (r"(?s:\Z)", "u"), + (r"(?s:\Z)", "v"), + (r"(?-i:\A)", ""), + (r"(?-i:\A)", "u"), + (r"(?-i:\A)", "v"), + (r"(?-m:\Z)", ""), + (r"(?-m:\Z)", "u"), + (r"(?-m:\Z)", "v"), + (r"(?m-s:\Afoo\z)", ""), + (r"(?m-s:\Afoo\z)", "u"), + (r"(?m-s:\Afoo\z)", "v"), + (r"(?ims:a\Zb)", ""), + (r"(?ims:a\Zb)", "u"), + (r"(?ims:a\Zb)", "v"), + (r"(?i:\A|\z)", ""), + (r"(?i:\A|\z)", "u"), + (r"(?i:\A|\z)", "v"), + (r"(?m:\Z)+", ""), + (r"(?m:\Z)+", "u"), + (r"(?m:\Z)+", "v"), + (r"(?m:\A*)", ""), + (r"(?i:[\z])", ""), + (r"(?m:(?-m:\A)\z)", ""), + (r"(?m:(?-m:\A)\z)", "u"), + (r"(?m:(?-m:\A)\z)", "v"), ] { let res = LiteralParser::new(&allocator, pattern_text, Some(flags_text), Options::default()) @@ -218,6 +295,21 @@ mod test { (r"(?i", ""), (r"(?i-", ""), (r"(?i-s", ""), + // Buffer boundaries + (r"\A*", "u"), + (r"\A*", "v"), + (r"\z+", "u"), + (r"\z+", "v"), + (r"\Z{2}", "u"), + (r"\Z{2}", "v"), + (r"[\A]", "u"), + (r"[\A]", "v"), + (r"[\z\Z]", "u"), + (r"[\z\Z]", "v"), + (r"(?m:\A*)", "u"), + (r"(?m:\A*)", "v"), + (r"(?i:[\z])", "u"), + (r"(?i:[\z])", "v"), ] { assert!( LiteralParser::new(&allocator, pattern_text, Some(flags_text), Options::default()) diff --git a/crates/oxc_regular_expression/src/parser/pattern_parser/pattern_parser_impl.rs b/crates/oxc_regular_expression/src/parser/pattern_parser/pattern_parser_impl.rs index 1691e2b01a..727f4e3043 100644 --- a/crates/oxc_regular_expression/src/parser/pattern_parser/pattern_parser_impl.rs +++ b/crates/oxc_regular_expression/src/parser/pattern_parser/pattern_parser_impl.rs @@ -231,6 +231,9 @@ impl<'a> PatternParser<'a> { // Assertion[UnicodeMode, UnicodeSetsMode, NamedCaptureGroups] :: // ^ // $ + // [+UnicodeMode] \A + // [+UnicodeMode] \z + // [+UnicodeMode] \Z // \b // \B // [+UnicodeMode] (?= Disjunction[+UnicodeMode, ?UnicodeSetsMode, ?NamedCaptureGroups] ) @@ -255,6 +258,12 @@ impl<'a> PatternParser<'a> { Some(ast::BoundaryAssertionKind::Boundary) } else if self.reader.eat2('\\', 'B') { Some(ast::BoundaryAssertionKind::NegativeBoundary) + } else if self.state.unicode_mode && self.reader.eat2('\\', 'A') { + Some(ast::BoundaryAssertionKind::StartBuffer) + } else if self.state.unicode_mode && self.reader.eat2('\\', 'z') { + Some(ast::BoundaryAssertionKind::EndBuffer) + } else if self.state.unicode_mode && self.reader.eat2('\\', 'Z') { + Some(ast::BoundaryAssertionKind::EndBufferOptionalNewline) } else { None };