perf(regular_expression): parse class set character prefixes once (#28512)

Share the ClassSetCharacter prefix between range and operand parsing
while preserving malformed-range fallback order.

AI-assisted (Codex).
This commit is contained in:
Boshen authored and GitHub committed 2026-10-30 17:17:35 +08:00
1 parent 3708934efb
commit 5395a40e36
2 files changed
+83 -32

No files matched your search

@@ -1135,12 +1135,11 @@ impl<'a> PatternParser<'a> {
&mut self,
) -> Result<(ast::CharacterClassContentsKind, ArenaVec<'a, ast::CharacterClassContents<'a>>)>
{
// ClassUnion :: ClassSetRange ClassUnion[opt]
if let Some(class_set_range) = self.parse_class_set_range()? {
return self.parse_class_set_union(class_set_range);
}
if let Some(class_set_operand) = self.parse_class_set_operand()? {
if let Some(class_set_operand) = self.parse_class_set_range_or_operand()? {
// A range can only start a union, not an intersection or subtraction.
if matches!(class_set_operand, ast::CharacterClassContents::CharacterClassRange(_)) {
return self.parse_class_set_union(class_set_operand);
}
// ClassIntersection
if self.reader.peek().as_ref().is_some_and(|&cp| cp == '&' as u32)
&& self.reader.peek2().as_ref().is_some_and(|&cp| cp == '&' as u32)
@@ -1178,11 +1177,7 @@ impl<'a> PatternParser<'a> {
body.push(class_set_range_or_class_set_operand);
loop {
if let Some(class_set_range) = self.parse_class_set_range()? {
body.push(class_set_range);
continue;
}
if let Some(class_set_operand) = self.parse_class_set_operand()? {
if let Some(class_set_operand) = self.parse_class_set_range_or_operand()? {
body.push(class_set_operand);
continue;
}
@@ -1274,34 +1269,45 @@ impl<'a> PatternParser<'a> {
// ClassSetRange ::
// ClassSetCharacter - ClassSetCharacter
// ```
fn parse_class_set_range(&mut self) -> Result<Option<ast::CharacterClassContents<'a>>> {
fn parse_class_set_range_or_operand(
&mut self,
) -> Result<Option<ast::CharacterClassContents<'a>>> {
let checkpoint = self.reader.checkpoint();
if let Some(class_set_character) = self.parse_class_set_character()?
&& self.reader.eat('-')
&& let Some(class_set_character_to) = self.parse_class_set_character()?
{
// [SS:EE] ClassSetRange :: ClassSetCharacter - ClassSetCharacter
// It is a Syntax Error if the CharacterValue of the first ClassSetCharacter is strictly greater than the CharacterValue of the second ClassSetCharacter.
if class_set_character_to.value < class_set_character.value {
return Err(diagnostics::character_class_range_out_of_order(
class_set_character.span.merge(class_set_character_to.span),
"class set",
));
if let Some(class_set_character) = self.parse_class_set_character()? {
if !self.reader.eat('-') {
return Ok(Some(ast::CharacterClassContents::Character(ArenaBox::new_in(
class_set_character,
self,
))));
}
return Ok(Some(ast::CharacterClassContents::CharacterClassRange(ArenaBox::new_in(
ast::CharacterClassRange {
span: class_set_character.span.merge(class_set_character_to.span),
min: class_set_character,
max: class_set_character_to,
},
self,
))));
if let Some(class_set_character_to) = self.parse_class_set_character()? {
// [SS:EE] ClassSetRange :: ClassSetCharacter - ClassSetCharacter
// It is a Syntax Error if the CharacterValue of the first ClassSetCharacter is strictly greater than the CharacterValue of the second ClassSetCharacter.
if class_set_character_to.value < class_set_character.value {
return Err(diagnostics::character_class_range_out_of_order(
class_set_character.span.merge(class_set_character_to.span),
"class set",
));
}
return Ok(Some(ast::CharacterClassContents::CharacterClassRange(
ArenaBox::new_in(
ast::CharacterClassRange {
span: class_set_character.span.merge(class_set_character_to.span),
min: class_set_character,
max: class_set_character_to,
},
self,
),
)));
}
}
// Failed ranges retain the original operand parse and diagnostic order.
self.reader.rewind(checkpoint);
Ok(None)
self.parse_class_set_operand()
}
// ```
@@ -0,0 +1,45 @@
use oxc_allocator::Allocator;
use oxc_regular_expression::{
LiteralParser, Options,
ast::{CharacterClassContents, Term},
};
use oxc_span::Span;
#[test]
fn class_set_character_values_and_ranges() {
let allocator = Allocator::default();
let pattern = r"[\u{61}\x62c-e]";
let parsed =
LiteralParser::new(&allocator, pattern, Some("v"), Options::default()).parse().unwrap();
let Term::CharacterClass(class) = &parsed.body.body[0].body[0] else {
panic!("expected character class");
};
assert_eq!(class.body.len(), 3);
for (entry, value, span) in
[(&class.body[0], 'a', Span::new(1, 7)), (&class.body[1], 'b', Span::new(7, 11))]
{
let CharacterClassContents::Character(character) = entry else {
panic!("expected class character");
};
assert_eq!(character.value, u32::from(value));
assert_eq!(character.span, span);
}
let CharacterClassContents::CharacterClassRange(range) = &class.body[2] else {
panic!("expected class range");
};
assert_eq!((range.min.value, range.max.value), (u32::from('c'), u32::from('e')));
assert_eq!(range.span, Span::new(11, 14));
for (pattern, flags, message, span) in [
("[a-]", "v", "Unterminated character class", Span::new(0, 2)),
("[a-b&&c]", "v", "Unterminated character class", Span::new(0, 4)),
("[z-a]", "v", "Character class set range out of order", Span::new(1, 4)),
] {
let error = LiteralParser::new(&allocator, pattern, Some(flags), Options::default())
.parse()
.unwrap_err();
assert!(error.to_string().contains(message), "/{pattern}/{flags}: {error}");
assert_eq!(error.labels[0].offset(), span.start);
assert_eq!(error.labels[0].len(), span.size());
}
}