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.
This commit is contained in:
Boshen committed 2026-09-25 13:20:11 +00:00
1 parent d7d70cbabb
commit 52ae63f4fb
5 files changed
+117 -1

No files matched your search

+1
View File
@@ -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
+8 -1
View File
@@ -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.
@@ -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]
@@ -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())
@@ -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
};