mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
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/;
|
||||
@@ -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%)
|
||||
@@ -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%)
|
||||
@@ -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%)
|
||||
@@ -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.
|
||||
|
||||
@@ -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%)
|
||||
Reference in new issue
Block a user