fix(regular_expression): allow incomplete oversized legacy quantifiers (#28449)

Defer oversized decimal diagnostics until a braced quantifier is
complete, allowing Annex B literal fallback for incomplete legacy
patterns.

Validated with scoped Rust checks, full conformance, parser bindings,
the linter suite.

AI-assisted (Codex).
This commit is contained in:
Boshen authored and GitHub committed 2026-10-30 17:17:31 +08:00
1 parent 04b20c0d4b
commit b03c7b8493
10 files changed
+62 -32

No files matched your search

@@ -46,6 +46,37 @@ mod test {
}
}
#[test]
fn oversized_incomplete_quantifiers() {
let allocator = Allocator::default();
for pattern in [
"a{18446744073709551616",
"a{18446744073709551616,",
"a{1,18446744073709551616",
"a{18446744073709551616x}",
"{18446744073709551616",
] {
let parsed = LiteralParser::new(&allocator, pattern, None, Options::default())
.parse()
.unwrap_or_else(|error| panic!("/{pattern}/: {error}"));
assert_eq!(parsed.body.body[0].body.len(), pattern.len());
for (term, byte) in parsed.body.body[0].body.iter().zip(pattern.bytes()) {
let Term::Character(character) = term else {
panic!("expected literal character in /{pattern}/");
};
assert_eq!(character.value, u32::from(byte));
}
for flags in ["u", "v"] {
assert!(
LiteralParser::new(&allocator, pattern, Some(flags), Options::default())
.parse()
.is_err(),
"/{pattern}/{flags}"
);
}
}
}
#[test]
fn escaped_group_names() {
let allocator = Allocator::default();
@@ -1746,7 +1746,7 @@ impl<'a> PatternParser<'a> {
let span_start = self.reader.offset();
let checkpoint = self.reader.checkpoint();
if self.reader.eat('{') {
if let Some(min) = self.consume_decimal_digits()? {
if let Some(min) = self.consume_decimal_digits() {
if self.reader.eat('}') {
if MAX_QUANTIFIER < min {
return Err(diagnostics::too_large_number_in_braced_quantifier(
@@ -1768,7 +1768,7 @@ impl<'a> PatternParser<'a> {
return Ok(Some(((min, None), is_greedy(&mut self.reader))));
}
if let Some(max) = self.consume_decimal_digits()?
if let Some(max) = self.consume_decimal_digits()
&& self.reader.eat('}')
{
if max < min {
@@ -1828,8 +1828,7 @@ impl<'a> PatternParser<'a> {
// [+Sep] DecimalDigits[+Sep] NumericLiteralSeparator DecimalDigit
// ```
// ([Sep] is disabled for `QuantifierPrefix` and `DecimalEscape`, skip it)
fn consume_decimal_digits(&mut self) -> Result<Option<u64>> {
let span_start = self.reader.offset();
fn consume_decimal_digits(&mut self) -> Option<u64> {
let checkpoint = self.reader.checkpoint();
let mut value: u64 = 0;
@@ -1838,23 +1837,18 @@ impl<'a> PatternParser<'a> {
#[expect(clippy::cast_lossless)]
let d = (cp - '0' as u32) as u64;
// To prevent panic on overflow cases like `\999999999999999999999`, `a{999999999999999999999}`
if let Some(v) = value.checked_mul(10).and_then(|v| v.checked_add(d)) {
value = v;
self.reader.advance();
} else {
return Err(diagnostics::too_large_number_digits(
self.span_factory.create(span_start, self.reader.offset()),
"decimal",
));
}
// A large digit sequence may be literal text in Annex B when no complete
// quantifier follows. Defer the size diagnostic until its syntax is known.
// Saturation preserves the comparison against MAX_QUANTIFIER.
value = value.saturating_mul(10).saturating_add(d);
self.reader.advance();
}
if self.reader.checkpoint() != checkpoint {
return Ok(Some(value));
return Some(value);
}
Ok(None)
None
}
// ```
@@ -76,12 +76,12 @@ expression: snapshot
# too_large_number_in_braced_quantifier
× Invalid regular expression: Number is too large in decimal digits
╭─[/x{99999999999999999999999999999999999999999999999999}/:1:4]
× Invalid regular expression: Number is too large in braced quantifier
╭─[/x{99999999999999999999999999999999999999999999999999}/:1:3]
1 │ /x{99999999999999999999999999999999999999999999999999}/
· ───────────────────
· ────────────────────────────────────────────────────
╰────
help: The number exceeds the maximum allowed value
help: Use a smaller number in the quantifier
# braced_quantifier_out_of_order
@@ -0,0 +1,5 @@
/a{18446744073709551616/;
/a{18446744073709551616,/;
/a{1,18446744073709551616/;
/a{18446744073709551616x}/;
/{18446744073709551616/;
+2 -2
View File
@@ -1,3 +1,3 @@
codegen_misc Summary:
AST Parsed : 131/131 (100.00%)
Positive Passed: 131/131 (100.00%)
AST Parsed : 132/132 (100.00%)
Positive Passed: 132/132 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
formatter_misc Summary:
AST Parsed : 131/131 (100.00%)
Positive Passed: 131/131 (100.00%)
AST Parsed : 132/132 (100.00%)
Positive Passed: 132/132 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
lexer_misc Summary:
AST Parsed : 399/399 (100.00%)
Positive Passed: 399/399 (100.00%)
AST Parsed : 400/400 (100.00%)
Positive Passed: 400/400 (100.00%)
+2 -2
View File
@@ -1,6 +1,6 @@
parser_misc Summary:
AST Parsed : 131/131 (100.00%)
Positive Passed: 131/131 (100.00%)
AST Parsed : 132/132 (100.00%)
Positive Passed: 132/132 (100.00%)
Negative Passed: 270/270 (100.00%)
× TS(1275): 'accessor' modifier can only appear on a property declaration.
+2 -2
View File
@@ -1,6 +1,6 @@
semantic_misc Summary:
AST Parsed : 131/131 (100.00%)
Positive Passed: 125/131 (95.42%)
AST Parsed : 132/132 (100.00%)
Positive Passed: 126/132 (95.45%)
semantic Error: tasks/coverage/misc/pass/declare-let-private.ts
Bindings mismatch:
after transform: ScopeId(0): ["private"]
@@ -1,3 +1,3 @@
transformer_misc Summary:
AST Parsed : 131/131 (100.00%)
Positive Passed: 131/131 (100.00%)
AST Parsed : 132/132 (100.00%)
Positive Passed: 132/132 (100.00%)