fix(regex): decode escaped capturing group names (#27156)

## Summary

- Decode RegExp Unicode escapes in capture and named-reference names so equivalent spellings match, such as `/(?<Ꭰ>)\k<\u13A0>/`.
- Share Unicode escape decoding with the capture pre-scan, and leave malformed names to the main parser so they retain their original diagnostics.
- Update the TypeScript conformance snapshot to remove false invalid-reference diagnostics.
This commit is contained in:
camc314 committed 2026-09-29 15:00:40 +00:00
1 parent 7a953767dd
commit 8f61129126
8 files changed
+338 -287

No files matched your search

@@ -10,7 +10,93 @@ pub use parser_impl::{ConstructorParser, LiteralParser};
mod test {
use oxc_allocator::Allocator;
use crate::{ConstructorParser, LiteralParser, Options};
use crate::{ConstructorParser, LiteralParser, Options, ast::Term};
#[test]
fn escaped_group_names() {
let allocator = Allocator::default();
for (pattern, flags, expected_name) in [
(r"(?<Ꭰ>)\k<\u13A0>", "", "Ꭰ"),
(r"(?<\u13A0>)\k<Ꭰ>", "", "Ꭰ"),
(r"(?<\u{13A0}>)\k<\u13A0>", "u", "Ꭰ"),
(r"(?<𝒜>)\k<\uD835\uDC9C>", "u", "𝒜"),
] {
let parsed = LiteralParser::new(&allocator, pattern, Some(flags), Options::default())
.parse()
.unwrap_or_else(|error| panic!("/{pattern}/{flags}: {error}"));
let terms = &parsed.body.body[0].body;
let Term::CapturingGroup(group) = &terms[0] else {
panic!("expected capturing group in /{pattern}/{flags}");
};
let Term::NamedReference(reference) = &terms[1] else {
panic!("expected named reference in /{pattern}/{flags}");
};
assert_eq!(group.name.as_ref().unwrap().as_str(), expected_name);
assert_eq!(reference.name.as_str(), expected_name);
}
for pattern in [r"(?<Ꭰ>)(?<\u13A0>)", r"(?<\u13A0>)(?<Ꭰ>)"] {
assert!(
LiteralParser::new(&allocator, pattern, Some(""), Options::default())
.parse()
.is_err(),
"/{pattern}/ should reject duplicate capture names"
);
}
let alternative_names = r"(?<Ꭰ>)|(?<\u13A0>)";
assert!(
LiteralParser::new(&allocator, alternative_names, Some(""), Options::default())
.parse()
.is_ok(),
"duplicate capture names in separate alternatives are valid"
);
for (pattern, expected) in [
(r"(?<⽇>)(?<\u2F47>)", "Unterminated capturing group name"),
(r"(?<_⽇>)(?<_\u2F47>)", "Unterminated capturing group name"),
(r"(?<🌚>)(?<\u{1F31A}>)", "Invalid surrogate pair"),
(r"(?<_🌚>)(?<_\u{1F31A}>)", "Invalid surrogate pair"),
] {
let error = LiteralParser::new(&allocator, pattern, Some(""), Options::default())
.parse()
.unwrap_err();
assert!(error.to_string().contains(expected), "/{pattern}/: {error}");
}
}
#[test]
fn escaped_group_names_in_constructor() {
let allocator = Allocator::default();
for (source, expected_name) in [
(r#""(?<\\u13A0>)\\k<\\u13A0>""#, "Ꭰ"),
(r#""(?<\\u13A0>)\\k<Ꭰ>""#, "Ꭰ"),
(r#""(?<\x61>)\\k<a>""#, "a"),
] {
let parsed = ConstructorParser::new(&allocator, source, None, Options::default())
.parse()
.unwrap_or_else(|error| panic!("{source}: {error}"));
let terms = &parsed.body.body[0].body;
let Term::CapturingGroup(group) = &terms[0] else {
panic!("expected capturing group in {source}");
};
let Term::NamedReference(reference) = &terms[1] else {
panic!("expected named reference in {source}");
};
assert_eq!(group.name.as_ref().unwrap().as_str(), expected_name);
assert_eq!(reference.name.as_str(), expected_name);
}
let duplicate = r#""(?<\\u13A0>)(?<Ꭰ>)""#;
assert!(
ConstructorParser::new(&allocator, duplicate, None, Options::default())
.parse()
.is_err(),
"{duplicate} should reject duplicate capture names"
);
}
#[test]
fn should_pass() {
@@ -0,0 +1,73 @@
use oxc_allocator::{Allocator, ArenaStringBuilder};
use oxc_str::Str;
use crate::parser::{
pattern_parser::{
character,
unicode_escape::{UnicodeEscape, decode},
},
reader::CodePoint,
};
use crate::surrogate_pair;
/// Decode a group name found by the capture pre-scan. `units` have already been decoded from
/// a string or template literal, but may still contain RegExp Unicode escapes.
/// Names without source escapes keep borrowing the source text. Malformed names return `None` so
/// the pre-scan cannot report a duplicate before the main parser diagnoses the invalid name.
/// <https://tc39.es/ecma262/multipage/text-processing.html#sec-static-semantics-capturinggroupname>
pub(super) fn capturing_group_name<'a>(
raw: &'a str,
units: &[CodePoint],
unicode_mode: bool,
allocator: &'a Allocator,
) -> Option<Str<'a>> {
if units.is_empty() {
return None;
}
let mut name =
raw.contains('\\').then(|| ArenaStringBuilder::with_capacity_in(raw.len(), allocator));
let mut index = 0;
while index < units.len() {
let (mut value, mut next) = decode_code_point(units, index)?;
// In non-Unicode mode, RegExpIdentifierCodePoint combines raw surrogate pairs.
// https://tc39.es/ecma262/multipage/text-processing.html#sec-static-semantics-regexpidentifiercodepoint
if !unicode_mode
&& units[index].value != '\\' as u32
&& surrogate_pair::is_lead_surrogate(value)
&& let Some(trail) = units.get(next).map(|unit| unit.value)
&& surrogate_pair::is_trail_surrogate(trail)
{
value = surrogate_pair::combine_surrogate_pair(value, trail);
next += 1;
}
let valid = if index == 0 {
character::is_identifier_start_char(value)
} else {
character::is_identifier_part_char(value)
};
if !valid {
return None;
}
if let Some(name) = &mut name {
name.push(char::from_u32(value)?);
}
index = next;
}
Some(name.map_or_else(|| Str::from(raw), |name| Str::from(name.into_str())))
}
fn decode_code_point(units: &[CodePoint], start: usize) -> Option<(u32, usize)> {
let value = units.get(start)?.value;
if value != '\\' as u32 {
return Some((value, start + 1));
}
match decode(&units[start + 1..], true) {
UnicodeEscape::Value { code_point, length } => Some((code_point, start + 1 + length)),
UnicodeEscape::Invalid | UnicodeEscape::Overflow { .. } => None,
}
}
@@ -1,6 +1,8 @@
mod character;
mod group_name;
mod pattern_parser_impl;
mod state;
mod unicode_escape;
mod unicode_property;
pub use pattern_parser_impl::PatternParser;
@@ -1,11 +1,16 @@
use oxc_allocator::{Allocator, ArenaBox, ArenaVec, GetAllocator};
use oxc_allocator::{Allocator, ArenaBox, ArenaStringBuilder, ArenaVec, GetAllocator};
use oxc_diagnostics::Result;
use oxc_str::Str;
use crate::{
ast, diagnostics,
parser::{
pattern_parser::{character, state::State, unicode_property},
pattern_parser::{
character,
state::State,
unicode_escape::{UnicodeEscape, decode},
unicode_property,
},
reader::{EscapeKind, Reader},
span_factory::SpanFactory,
},
@@ -58,11 +63,16 @@ impl<'a> PatternParser<'a> {
// [SS:EE] Pattern :: Disjunction
// It is a Syntax Error if Pattern contains two or more GroupSpecifiers for which the CapturingGroupName of GroupSpecifier is the same.
self.state.initialize_with_parsing(&mut self.reader).map_err(|offsets| {
diagnostics::duplicated_capturing_group_names(
offsets.iter().map(|&(start, end)| self.span_factory.create(start, end)).collect(),
)
})?;
self.state.initialize_with_parsing(&mut self.reader, self.allocator).map_err(
|offsets| {
diagnostics::duplicated_capturing_group_names(
offsets
.iter()
.map(|&(start, end)| self.span_factory.create(start, end))
.collect(),
)
},
)?;
// [SS:EE] Pattern :: Disjunction
// It is a Syntax Error if CountLeftCapturingParensWithin(Pattern) ≥ 2**32 - 1.
@@ -540,6 +550,7 @@ impl<'a> PatternParser<'a> {
if let Some(name) = self.consume_group_name()? {
// [SS:EE] AtomEscape :: k GroupName
// It is a Syntax Error if GroupSpecifiersThatMatch(GroupName) is empty.
// https://tc39.es/ecma262/multipage/text-processing.html#sec-static-semantics-groupspecifiersthatmatch
if !self.state.capturing_group_names.contains(name.as_str()) {
let names: Vec<&str> =
self.state.capturing_group_names.iter().map(Str::as_str).collect();
@@ -1975,12 +1986,36 @@ impl<'a> PatternParser<'a> {
fn consume_reg_exp_idenfigier_name(&mut self) -> Result<Option<Str<'a>>> {
let span_start = self.reader.offset();
if self.consume_reg_exp_idenfigier_start()?.is_some() {
while self.consume_reg_exp_idenfigier_part()?.is_some() {}
return Ok(Some(self.reader.str(span_start, self.reader.offset())));
let Some(mut cp) = self.consume_reg_exp_idenfigier_start()? else {
return Ok(None);
};
let mut decoded = None;
let mut copied_until = span_start;
let mut part_start = span_start;
loop {
let part_end = self.reader.offset();
if self.reader.str(part_start, part_end).as_str().starts_with('\\') {
let name =
decoded.get_or_insert_with(|| ArenaStringBuilder::new_in(self.allocator));
name.push_str(self.reader.str(copied_until, part_start).as_str());
name.push(char::from_u32(cp).expect("validated group-name code point"));
copied_until = part_end;
}
part_start = part_end;
let Some(next_cp) = self.consume_reg_exp_idenfigier_part()? else {
break;
};
cp = next_cp;
}
Ok(None)
if let Some(mut name) = decoded {
name.push_str(self.reader.str(copied_until, self.reader.offset()).as_str());
Ok(Some(Str::from(name.into_str())))
} else {
Ok(Some(self.reader.str(span_start, self.reader.offset())))
}
}
// ```
@@ -2113,76 +2148,35 @@ impl<'a> PatternParser<'a> {
unicode_mode: bool,
) -> Result<Option<u32>> {
let span_start = self.reader.offset();
let checkpoint = self.reader.checkpoint();
if self.reader.eat('u') {
if unicode_mode {
let checkpoint = self.reader.checkpoint();
// HexLeadSurrogate + HexTrailSurrogate
if let Some(lead_surrogate) = self
.consume_fixed_hex_digits(4)
.filter(|&cp| surrogate_pair::is_lead_surrogate(cp))
&& self.reader.eat2('\\', 'u')
&& let Some(trail_surrogate) = self
.consume_fixed_hex_digits(4)
.filter(|&cp| surrogate_pair::is_trail_surrogate(cp))
{
return Ok(Some(surrogate_pair::combine_surrogate_pair(
lead_surrogate,
trail_surrogate,
)));
match decode(self.reader.remaining_code_points(), unicode_mode) {
UnicodeEscape::Value { code_point, length } => {
for _ in 0..length {
self.reader.advance();
}
self.reader.rewind(checkpoint);
// HexLeadSurrogate
if let Some(lead_surrogate) = self
.consume_fixed_hex_digits(4)
.filter(|&cp| surrogate_pair::is_lead_surrogate(cp))
{
return Ok(Some(lead_surrogate));
Ok(Some(code_point))
}
UnicodeEscape::Overflow { digits } => {
self.reader.advance(); // u
self.reader.advance(); // {
let digits_start = self.reader.offset();
for _ in 0..digits {
self.reader.advance();
}
self.reader.rewind(checkpoint);
// HexTrailSurrogate
if let Some(trail_surrogate) = self
.consume_fixed_hex_digits(4)
.filter(|&cp| surrogate_pair::is_trail_surrogate(cp))
{
return Ok(Some(trail_surrogate));
Err(diagnostics::too_large_number_digits(
self.span_factory.create(digits_start, self.reader.offset()),
"hex",
))
}
UnicodeEscape::Invalid => {
if self.state.unicode_mode && self.reader.eat('u') {
Err(diagnostics::invalid_unicode_escape_sequence(
self.span_factory.create(span_start, self.reader.offset()),
))
} else {
Ok(None)
}
self.reader.rewind(checkpoint);
}
// HexNonSurrogate and Hex4Digits are the same
if let Some(hex_digits) = self.consume_fixed_hex_digits(4) {
return Ok(Some(hex_digits));
}
// {CodePoint}
if unicode_mode {
let checkpoint = self.reader.checkpoint();
if self.reader.eat('{')
&& let Some(hex_digits) =
self.consume_hex_digits()?.filter(|&cp| character::is_valid_unicode(cp))
&& self.reader.eat('}')
{
return Ok(Some(hex_digits));
}
self.reader.rewind(checkpoint);
}
if self.state.unicode_mode {
return Err(diagnostics::invalid_unicode_escape_sequence(
self.span_factory.create(span_start, self.reader.offset()),
));
}
self.reader.rewind(checkpoint);
}
Ok(None)
}
// ```
@@ -2302,31 +2296,6 @@ impl<'a> PatternParser<'a> {
Some(cp)
}
fn consume_hex_digits(&mut self) -> Result<Option<u32>> {
let span_start = self.reader.offset();
let checkpoint = self.reader.checkpoint();
let mut value: u32 = 0;
while let Some(hex) = self.reader.peek().and_then(character::map_hex_digit) {
// To prevent panic on overflow cases like `\u{FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF}`
if let Some(v) = value.checked_mul(16).and_then(|v| v.checked_add(hex)) {
value = v;
self.reader.advance();
} else {
return Err(diagnostics::too_large_number_digits(
self.span_factory.create(span_start, self.reader.offset()),
"hex",
));
}
}
if self.reader.checkpoint() != checkpoint {
return Ok(Some(value));
}
Ok(None)
}
fn consume_fixed_hex_digits(&mut self, len: usize) -> Option<u32> {
let checkpoint = self.reader.checkpoint();
@@ -1,7 +1,8 @@
use oxc_allocator::Allocator;
use oxc_str::Str;
use rustc_hash::FxHashSet;
use crate::parser::reader::Reader;
use crate::parser::{pattern_parser::group_name::capturing_group_name, reader::Reader};
/// NOTE: Currently all of properties are read-only from outside of this module.
/// Even inside of this module, it is not changed after initialized.
@@ -32,6 +33,7 @@ impl<'a> State<'a> {
pub fn initialize_with_parsing(
&mut self,
reader: &mut Reader<'a>,
allocator: &'a Allocator,
) -> Result<(), DuplicatedNamedCapturingGroupOffsets> {
// PERF: Fast path, no `(` in the decoded units.
// Provably no capturing group (named or unnamed) and no duplicate name.
@@ -48,7 +50,8 @@ impl<'a> State<'a> {
}
let checkpoint = reader.checkpoint();
let (num_of_left_capturing_parens, capturing_group_names) = parse_capturing_groups(reader)?;
let (num_of_left_capturing_parens, capturing_group_names) =
parse_capturing_groups(reader, self.unicode_mode, allocator)?;
reader.rewind(checkpoint);
// In Annex B, this is `false` by default.
@@ -70,6 +73,8 @@ impl<'a> State<'a> {
/// Returns: Result<(num_of_left_parens, capturing_group_names), duplicated_named_capturing_group_offsets>
fn parse_capturing_groups<'a>(
reader: &mut Reader<'a>,
unicode_mode: bool,
allocator: &'a Allocator,
) -> Result<(u32, FxHashSet<Str<'a>>), DuplicatedNamedCapturingGroupOffsets> {
// Count only normal CapturingGroup(named, unnamed)
// (?<name>...), (...)
@@ -122,6 +127,7 @@ fn parse_capturing_groups<'a>(
// Collect capturing group names
if reader.eat2('?', '<') {
let span_start = reader.offset();
let name_start = reader.checkpoint();
while let Some(ch) = reader.peek() {
if ch == '>' as u32 {
break;
@@ -130,8 +136,17 @@ fn parse_capturing_groups<'a>(
}
let span_end = reader.offset();
if reader.eat('>') {
let group_name = reader.str(span_start, span_end);
if reader.peek() == Some('>' as u32) {
let group_name = capturing_group_name(
reader.str(span_start, span_end).as_str(),
reader.code_points_since(name_start),
unicode_mode,
allocator,
);
reader.advance();
let Some(group_name) = group_name else {
continue;
};
let alternative_path = tracker.get_alternative_path();
// Check for duplicates with existing groups
@@ -260,8 +275,9 @@ mod tests {
] {
let mut reader = Reader::initialize(source_text, true, false).unwrap();
let allocator = Allocator::default();
let (num_of_left_capturing_parens, capturing_group_names) =
parse_capturing_groups(&mut reader).unwrap();
parse_capturing_groups(&mut reader, true, &allocator).unwrap();
let actual = (num_of_left_capturing_parens, capturing_group_names.len());
assert_eq!(expected, actual, "{source_text}");
@@ -281,7 +297,11 @@ mod tests {
] {
let mut reader = Reader::initialize(source_text, true, false).unwrap();
assert!(parse_capturing_groups(&mut reader).is_err(), "{source_text}");
let allocator = Allocator::default();
assert!(
parse_capturing_groups(&mut reader, true, &allocator).is_err(),
"{source_text}"
);
}
}
}
@@ -0,0 +1,71 @@
use crate::{
parser::{pattern_parser::character, reader::CodePoint},
surrogate_pair,
};
pub(super) enum UnicodeEscape {
Value {
code_point: u32,
length: usize,
},
Invalid,
/// Number of valid digits consumed after `u{` before the overflowing digit.
Overflow {
digits: usize,
},
}
/// Decode RegExpUnicodeEscapeSequence from cooked code points, beginning with `u`.
/// <https://tc39.es/ecma262/multipage/text-processing.html#prod-RegExpUnicodeEscapeSequence>
pub(super) fn decode(units: &[CodePoint], unicode_mode: bool) -> UnicodeEscape {
if units.first().is_none_or(|unit| unit.value != 'u' as u32) {
return UnicodeEscape::Invalid;
}
if let Some(value) = fixed_hex_digits(units, 1) {
if unicode_mode
&& surrogate_pair::is_lead_surrogate(value)
&& units.get(5).is_some_and(|unit| unit.value == '\\' as u32)
&& units.get(6).is_some_and(|unit| unit.value == 'u' as u32)
&& let Some(trail) = fixed_hex_digits(units, 7)
&& surrogate_pair::is_trail_surrogate(trail)
{
return UnicodeEscape::Value {
code_point: surrogate_pair::combine_surrogate_pair(value, trail),
length: 11,
};
}
return UnicodeEscape::Value { code_point: value, length: 5 };
}
if unicode_mode && units.get(1).is_some_and(|unit| unit.value == '{' as u32) {
let mut index = 2;
let mut value = 0_u32;
while let Some(digit) =
units.get(index).and_then(|unit| character::map_hex_digit(unit.value))
{
let Some(next) = value.checked_mul(16).and_then(|value| value.checked_add(digit))
else {
return UnicodeEscape::Overflow { digits: index - 2 };
};
value = next;
index += 1;
}
if index > 2
&& units.get(index).is_some_and(|unit| unit.value == '}' as u32)
&& character::is_valid_unicode(value)
{
return UnicodeEscape::Value { code_point: value, length: index + 1 };
}
}
UnicodeEscape::Invalid
}
fn fixed_hex_digits(units: &[CodePoint], start: usize) -> Option<u32> {
let mut value = 0_u32;
for unit in units.get(start..start + 4)? {
value = value * 16 + character::map_hex_digit(unit.value)?;
}
Some(value)
}
@@ -75,6 +75,15 @@ impl<'a> Reader<'a> {
(self.index, self.offset)
}
/// Return the cooked code points consumed since `checkpoint`.
pub fn code_points_since(&self, checkpoint: (usize, u32)) -> &[CodePoint] {
&self.units[checkpoint.0..self.index]
}
pub fn remaining_code_points(&self) -> &[CodePoint] {
&self.units[self.index..]
}
pub fn rewind(&mut self, checkpoint: (usize, u32)) {
self.index = checkpoint.0;
self.offset = checkpoint.1;
@@ -12388,60 +12388,6 @@ Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/
╰────
help: Valid group names are: foo
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:2:17]
1 │ // U+13A0 CHEROKEE LETTER A: character inside BMP with both the Unicode properties ID_Start and ID_Continue
2 │ /(?<Ꭰ>)\k<Ꭰ>\k<\u13A0>\k<\u{13A0}>/;
· ──────────
3 │ /(?<\u13A0>)\k<Ꭰ>\k<\u13A0>\k<\u{13A0}>/;
╰────
help: Valid group names are: Ꭰ
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:3:13]
2 │ /(?<Ꭰ>)\k<Ꭰ>\k<\u13A0>\k<\u{13A0}>/;
3 │ /(?<\u13A0>)\k<Ꭰ>\k<\u13A0>\k<\u{13A0}>/;
· ─────
4 │ /(?<\u{13A0}>)\k<Ꭰ>\k<\u13A0>\k<\u{13A0}>/;
╰────
help: Valid group names are: \u13A0
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:4:15]
3 │ /(?<\u13A0>)\k<Ꭰ>\k<\u13A0>\k<\u{13A0}>/;
4 │ /(?<\u{13A0}>)\k<Ꭰ>\k<\u13A0>\k<\u{13A0}>/;
· ─────
5 │
╰────
help: Valid group names are: \u{13A0}
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:6:19]
5 │
6 │ /(?<_Ꭰ>)\k<_Ꭰ>\k<_\u13A0>\k<_\u{13A0}>/;
· ───────────
7 │ /(?<_\u13A0>)\k<_Ꭰ>\k<_\u13A0>\k<_\u{13A0}>/;
╰────
help: Valid group names are: _Ꭰ
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:7:14]
6 │ /(?<_Ꭰ>)\k<_Ꭰ>\k<_\u13A0>\k<_\u{13A0}>/;
7 │ /(?<_\u13A0>)\k<_Ꭰ>\k<_\u13A0>\k<_\u{13A0}>/;
· ──────
8 │ /(?<_\u{13A0}>)\k<_Ꭰ>\k<_\u13A0>\k<_\u{13A0}>/;
╰────
help: Valid group names are: _\u13A0
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:8:16]
7 │ /(?<_\u13A0>)\k<_Ꭰ>\k<_\u13A0>\k<_\u{13A0}>/;
8 │ /(?<_\u{13A0}>)\k<_Ꭰ>\k<_\u13A0>\k<_\u{13A0}>/;
· ──────
9 │
╰────
help: Valid group names are: _\u{13A0}
× Invalid regular expression: Unterminated capturing group name
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:11:4]
10 │ // U+19D4 NEW TAI LUE DIGIT FOUR: character inside BMP with only ID_Continue
@@ -12469,87 +12415,6 @@ Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/
╰────
help: Use `\uXXXX` or `\u{XXXXX}` format for unicode escapes
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:15:19]
14 │
15 │ /(?<_᧔>)\k<_᧔>\k<_\u19D4>\k<_\u{19D4}>/;
· ───────────
16 │ /(?<_\u19D4>)\k<_᧔>\k<_\u19D4>\k<_\u{19D4}>/;
╰────
help: Valid group names are: _᧔
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:16:14]
15 │ /(?<_᧔>)\k<_᧔>\k<_\u19D4>\k<_\u{19D4}>/;
16 │ /(?<_\u19D4>)\k<_᧔>\k<_\u19D4>\k<_\u{19D4}>/;
· ──────
17 │ /(?<_\u{19D4}>)\k<_᧔>\k<_\u19D4>\k<_\u{19D4}>/;
╰────
help: Valid group names are: _\u19D4
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:17:16]
16 │ /(?<_\u19D4>)\k<_᧔>\k<_\u19D4>\k<_\u{19D4}>/;
17 │ /(?<_\u{19D4}>)\k<_᧔>\k<_\u19D4>\k<_\u{19D4}>/;
· ──────
18 │
╰────
help: Valid group names are: _\u{19D4}
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:20:19]
19 │ // U+102A7 CARIAN LETTER A2: character outside BMP with both ID_Start and ID_Continue
20 │ /(?<𐊧>)\k<𐊧>\k<\u{102A7}>\k<\uD800\uDEA7>/;
· ─────────────
21 │ /(?<\u{102A7}>)\k<𐊧>\k<\u{102A7}>\k<\uD800\uDEA7>/;
╰────
help: Valid group names are: 𐊧
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:21:16]
20 │ /(?<𐊧>)\k<𐊧>\k<\u{102A7}>\k<\uD800\uDEA7>/;
21 │ /(?<\u{102A7}>)\k<𐊧>\k<\u{102A7}>\k<\uD800\uDEA7>/;
· ─────
22 │ /(?<\uD800\uDEA7>)\k<𐊧>\k<\u{102A7}>\k<\uD800\uDEA7>/;
╰────
help: Valid group names are: \u{102A7}
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:22:19]
21 │ /(?<\u{102A7}>)\k<𐊧>\k<\u{102A7}>\k<\uD800\uDEA7>/;
22 │ /(?<\uD800\uDEA7>)\k<𐊧>\k<\u{102A7}>\k<\uD800\uDEA7>/;
· ─────
23 │
╰────
help: Valid group names are: \uD800\uDEA7
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:24:21]
23 │
24 │ /(?<_𐊧>)\k<_𐊧>\k<_\u{102A7}>\k<_\uD800\uDEA7>/;
· ──────────────
25 │ /(?<_\u{102A7}>)\k<_𐊧>\k<_\u{102A7}>\k<_\uD800\uDEA7>/;
╰────
help: Valid group names are: _𐊧
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:25:17]
24 │ /(?<_𐊧>)\k<_𐊧>\k<_\u{102A7}>\k<_\uD800\uDEA7>/;
25 │ /(?<_\u{102A7}>)\k<_𐊧>\k<_\u{102A7}>\k<_\uD800\uDEA7>/;
· ──────
26 │ /(?<_\uD800\uDEA7>)\k<_𐊧>\k<_\u{102A7}>\k<_\uD800\uDEA7>/;
╰────
help: Valid group names are: _\u{102A7}
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:26:20]
25 │ /(?<_\u{102A7}>)\k<_𐊧>\k<_\u{102A7}>\k<_\uD800\uDEA7>/;
26 │ /(?<_\uD800\uDEA7>)\k<_𐊧>\k<_\u{102A7}>\k<_\uD800\uDEA7>/;
· ──────
27 │
╰────
help: Valid group names are: _\uD800\uDEA7
× Invalid regular expression: Invalid surrogate pair
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:29:5]
28 │ // U+1113D CHAKMA DIGIT SEVEN: character outside BMP with only ID_Continue
@@ -12577,33 +12442,6 @@ Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/
╰────
help: Use `\uXXXX` or `\u{XXXXX}` format for unicode escapes
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:33:21]
32 │
33 │ /(?<_𑄽>)\k<_𑄽>\k<_\u{1113D}>\k<_\uD804\uDD3D>/;
· ──────────────
34 │ /(?<_\u{1113D}>)\k<_𑄽>\k<_\u{1113D}>\k<_\uD804\uDD3D>/;
╰────
help: Valid group names are: _𑄽
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:34:17]
33 │ /(?<_𑄽>)\k<_𑄽>\k<_\u{1113D}>\k<_\uD804\uDD3D>/;
34 │ /(?<_\u{1113D}>)\k<_𑄽>\k<_\u{1113D}>\k<_\uD804\uDD3D>/;
· ──────
35 │ /(?<_\uD804\uDD3D>)\k<_𑄽>\k<_\u{1113D}>\k<_\uD804\uDD3D>/;
╰────
help: Valid group names are: _\u{1113D}
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:35:20]
34 │ /(?<_\u{1113D}>)\k<_𑄽>\k<_\u{1113D}>\k<_\uD804\uDD3D>/;
35 │ /(?<_\uD804\uDD3D>)\k<_𑄽>\k<_\u{1113D}>\k<_\uD804\uDD3D>/;
· ──────
36 │
╰────
help: Valid group names are: _\uD804\uDD3D
× Invalid regular expression: Unterminated capturing group name
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:40:4]
39 │ // U+2F47 KANGXI RADICAL SUN: character inside BMP without both properties
@@ -12694,23 +12532,6 @@ Expect to Parse: tasks/coverage/typescript/tsc/testdata/tests/cases/conformance/
╰────
help: Use `\uXXXX` or `\u{XXXXX}` format for unicode escapes
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:60:23]
59 │ // Cases with Unicode escapes and extended Unicode escapes mixed
60 │ /(?<RegExp>)\k<RegExp>\k<\u{52}\u0065gE\u{78}p>\k<\u0052e\u0067\u{45}x\u{70}>/;
· ─────────────────────────
61 │ /(?<_RegExp>)\k<_RegExp>\k<_\u{52}\u0065gE\u{78}p>\k<_\u0052e\u0067\u{45}x\u{70}>/;
╰────
help: Valid group names are: RegExp
× Invalid regular expression: Invalid named reference
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionGroupNameUnicodeEscapes.ts:61:25]
60 │ /(?<RegExp>)\k<RegExp>\k<\u{52}\u0065gE\u{78}p>\k<\u0052e\u0067\u{45}x\u{70}>/;
61 │ /(?<_RegExp>)\k<_RegExp>\k<_\u{52}\u0065gE\u{78}p>\k<_\u0052e\u0067\u{45}x\u{70}>/;
· ──────────────────────────
╰────
help: Valid group names are: _RegExp
× Invalid regular expression: Number is too large in braced quantifier
╭─[typescript/tsc/testdata/tests/cases/compiler/regularExpressionQuantifierBounds1.ts:3:4]
2 │ /a{7,8}/,