diff --git a/crates/oxc_regular_expression/src/parser/mod.rs b/crates/oxc_regular_expression/src/parser/mod.rs index 68a700b0c8..c9b15b28a8 100644 --- a/crates/oxc_regular_expression/src/parser/mod.rs +++ b/crates/oxc_regular_expression/src/parser/mod.rs @@ -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(); 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 33106a76f0..a5455c3723 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 @@ -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> { - let span_start = self.reader.offset(); + fn consume_decimal_digits(&mut self) -> Option { 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 } // ``` diff --git a/crates/oxc_regular_expression/tests/snapshots/diagnostics__test.snap b/crates/oxc_regular_expression/tests/snapshots/diagnostics__test.snap index 23cecc944d..c6c8fb4cfc 100644 --- a/crates/oxc_regular_expression/tests/snapshots/diagnostics__test.snap +++ b/crates/oxc_regular_expression/tests/snapshots/diagnostics__test.snap @@ -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 diff --git a/tasks/coverage/misc/pass/regexp-incomplete-large-quantifier.js b/tasks/coverage/misc/pass/regexp-incomplete-large-quantifier.js new file mode 100644 index 0000000000..9b00b864de --- /dev/null +++ b/tasks/coverage/misc/pass/regexp-incomplete-large-quantifier.js @@ -0,0 +1,5 @@ +/a{18446744073709551616/; +/a{18446744073709551616,/; +/a{1,18446744073709551616/; +/a{18446744073709551616x}/; +/{18446744073709551616/; diff --git a/tasks/coverage/snapshots/codegen_misc.snap b/tasks/coverage/snapshots/codegen_misc.snap index 63055b737d..9a8afa1041 100644 --- a/tasks/coverage/snapshots/codegen_misc.snap +++ b/tasks/coverage/snapshots/codegen_misc.snap @@ -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%) diff --git a/tasks/coverage/snapshots/formatter_misc.snap b/tasks/coverage/snapshots/formatter_misc.snap index c60494f27d..a3c3ed4857 100644 --- a/tasks/coverage/snapshots/formatter_misc.snap +++ b/tasks/coverage/snapshots/formatter_misc.snap @@ -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%) diff --git a/tasks/coverage/snapshots/lexer_misc.snap b/tasks/coverage/snapshots/lexer_misc.snap index 2ce05e0c87..5a722d5615 100644 --- a/tasks/coverage/snapshots/lexer_misc.snap +++ b/tasks/coverage/snapshots/lexer_misc.snap @@ -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%) diff --git a/tasks/coverage/snapshots/parser_misc.snap b/tasks/coverage/snapshots/parser_misc.snap index 23fce65842..27fa904d79 100644 --- a/tasks/coverage/snapshots/parser_misc.snap +++ b/tasks/coverage/snapshots/parser_misc.snap @@ -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. diff --git a/tasks/coverage/snapshots/semantic_misc.snap b/tasks/coverage/snapshots/semantic_misc.snap index 77d272308c..37aaa57012 100644 --- a/tasks/coverage/snapshots/semantic_misc.snap +++ b/tasks/coverage/snapshots/semantic_misc.snap @@ -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"] diff --git a/tasks/coverage/snapshots/transformer_misc.snap b/tasks/coverage/snapshots/transformer_misc.snap index 0281124fbf..725842f999 100644 --- a/tasks/coverage/snapshots/transformer_misc.snap +++ b/tasks/coverage/snapshots/transformer_misc.snap @@ -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%)