mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(regex): preserve escape values and boundaries (#28296)
Control and octal escapes could produce incorrect values or consume extra digits, and leading-zero escapes could become backreferences. Decode their values and boundaries according to the grammar and print canonical control letters. AI-assisted implementation.
This commit is contained in:
1 parent
fc8acbc6ed
commit
3dc2cb0ae2
11 files changed
+60
-44
No files matched your search
@@ -325,13 +325,11 @@ fn character_to_string(
|
||||
let result = match this.kind {
|
||||
// Not a surrogate, like BMP, or all units in unicode mode
|
||||
CharacterKind::Symbol => Cow::Owned(ch.to_string()),
|
||||
CharacterKind::ControlLetter => match ch {
|
||||
'\n' => Cow::Borrowed(r"\cJ"),
|
||||
'\r' => Cow::Borrowed(r"\cM"),
|
||||
'\t' => Cow::Borrowed(r"\cI"),
|
||||
'\u{0019}' => Cow::Borrowed(r"\cY"),
|
||||
_ => Cow::Owned(format!(r"\c{ch}")),
|
||||
},
|
||||
CharacterKind::ControlLetter => {
|
||||
// Store the decoded value, and print its canonical ASCII control letter.
|
||||
let letter = char::from_u32(cp + 0x40).expect("Invalid control letter `Character`!");
|
||||
Cow::Owned(format!(r"\c{letter}"))
|
||||
}
|
||||
CharacterKind::Identifier => Cow::Owned(format!(r"\{ch}")),
|
||||
CharacterKind::SingleEscape => match ch {
|
||||
'\n' => Cow::Borrowed(r"\n"),
|
||||
@@ -513,12 +511,12 @@ mod test {
|
||||
(r"/(.)(.)(.)(.)(.)(.)(.)(.)\8\8/", None),
|
||||
// Class escape from: <https://github.com/tc39/test262/blob/d62fa93c8f9ce5e687c0bbaa5d2b59670ab2ff60/test/annexB/language/literals/regexp/class-escape.js>
|
||||
(r"/\c0/", None),
|
||||
(r"/[\c0]/", None),
|
||||
(r"/[\c0]/", Some(r"/[\cP]/")),
|
||||
(r"/\c1/", None),
|
||||
(r"/[\c10]+/", None),
|
||||
(r"/[\c10]+/", Some(r"/[\cQ0]+/")),
|
||||
(r"/\c8/", None),
|
||||
(r"/[\c8]/", None),
|
||||
(r"/[\c80]+/", None),
|
||||
(r"/[\c8]/", Some(r"/[\cX]/")),
|
||||
(r"/[\c80]+/", Some(r"/[\cX0]+/")),
|
||||
(r"/\c_/", None),
|
||||
// Capitalize hex unicodes --
|
||||
(r"/^|\udf06/gu", Some(r"/^|\uDF06/gu")),
|
||||
|
||||
@@ -996,7 +996,14 @@ impl<'a> PatternParser<'a> {
|
||||
}
|
||||
|
||||
if self.reader.eat('\\') {
|
||||
if self.reader.peek().as_ref().is_some_and(|&cp| cp == 'c' as u32) {
|
||||
if let Some(class_escape) = self.parse_class_escape(span_start)? {
|
||||
return Ok(Some(class_escape));
|
||||
}
|
||||
|
||||
// Annex B alternatives are tried in order, and do not extend Unicode patterns.
|
||||
if !self.state.unicode_mode
|
||||
&& self.reader.peek().as_ref().is_some_and(|&cp| cp == 'c' as u32)
|
||||
{
|
||||
return Ok(Some(ast::CharacterClassContents::Character(ArenaBox::new_in(
|
||||
ast::Character {
|
||||
span: self.span_factory.create(span_start, self.reader.offset()),
|
||||
@@ -1007,10 +1014,6 @@ impl<'a> PatternParser<'a> {
|
||||
))));
|
||||
}
|
||||
|
||||
if let Some(class_escape) = self.parse_class_escape(span_start)? {
|
||||
return Ok(Some(class_escape));
|
||||
}
|
||||
|
||||
return Err(diagnostics::invalid_class_atom(
|
||||
self.span_factory.create(span_start, self.reader.offset()),
|
||||
));
|
||||
@@ -1068,7 +1071,7 @@ impl<'a> PatternParser<'a> {
|
||||
if let Some(cp) = self
|
||||
.reader
|
||||
.peek()
|
||||
.filter(|&cp| character::is_decimal_digit(cp) || cp == '-' as u32)
|
||||
.filter(|&cp| character::is_decimal_digit(cp) || cp == '_' as u32)
|
||||
{
|
||||
self.reader.advance();
|
||||
|
||||
@@ -1076,7 +1079,7 @@ impl<'a> PatternParser<'a> {
|
||||
ast::Character {
|
||||
span: self.span_factory.create(span_start, self.reader.offset()),
|
||||
kind: ast::CharacterKind::ControlLetter,
|
||||
value: cp,
|
||||
value: cp % 0x20,
|
||||
},
|
||||
self,
|
||||
))));
|
||||
@@ -1798,7 +1801,11 @@ impl<'a> PatternParser<'a> {
|
||||
// NonZeroDigit DecimalDigits[~Sep][opt] [lookahead ∉ DecimalDigit]
|
||||
// ```
|
||||
fn consume_decimal_escape(&mut self) -> Option<u64> {
|
||||
let checkpoint = self.reader.checkpoint();
|
||||
// DecimalEscape starts with NonZeroDigit; leading zero belongs to CharacterEscape.
|
||||
if !self.reader.peek().is_some_and(|cp| character::is_decimal_digit(cp) && cp != '0' as u32)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut index = 0_u64;
|
||||
while let Some(cp) = self.reader.peek().filter(|&cp| character::is_decimal_digit(cp)) {
|
||||
@@ -1812,13 +1819,7 @@ impl<'a> PatternParser<'a> {
|
||||
self.reader.advance();
|
||||
}
|
||||
|
||||
// \0 is CharacterEscape, not DecimalEscape
|
||||
if index != 0 {
|
||||
return Some(index);
|
||||
}
|
||||
|
||||
self.reader.rewind(checkpoint);
|
||||
None
|
||||
Some(index)
|
||||
}
|
||||
|
||||
// ```
|
||||
@@ -2200,11 +2201,11 @@ impl<'a> PatternParser<'a> {
|
||||
}
|
||||
|
||||
if let Some(second) = self.consume_octal_digit() {
|
||||
if let Some(third) = self.consume_octal_digit() {
|
||||
// ZeroToThree OctalDigit OctalDigit
|
||||
if first <= 3 {
|
||||
return Some(first * 64 + second * 8 + third);
|
||||
}
|
||||
// Only ZeroToThree permits a third digit; FourToSeven consumes two.
|
||||
if first <= 3
|
||||
&& let Some(third) = self.consume_octal_digit()
|
||||
{
|
||||
return Some(first * 64 + second * 8 + third);
|
||||
}
|
||||
|
||||
// ZeroToThree OctalDigit [lookahead ∉ OctalDigit]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/[\c1]/u;
|
||||
@@ -0,0 +1 @@
|
||||
/()\01/u;
|
||||
@@ -0,0 +1 @@
|
||||
const controls = [/\cA/u, /[\cA]/u, /[\cz]/u, /[\cA-\cZ]/u, /()\01/];
|
||||
@@ -1,3 +1,3 @@
|
||||
codegen_misc Summary:
|
||||
AST Parsed : 120/120 (100.00%)
|
||||
Positive Passed: 120/120 (100.00%)
|
||||
AST Parsed : 121/121 (100.00%)
|
||||
Positive Passed: 121/121 (100.00%)
|
||||
@@ -1,3 +1,3 @@
|
||||
formatter_misc Summary:
|
||||
AST Parsed : 120/120 (100.00%)
|
||||
Positive Passed: 120/120 (100.00%)
|
||||
AST Parsed : 121/121 (100.00%)
|
||||
Positive Passed: 121/121 (100.00%)
|
||||
@@ -1,3 +1,3 @@
|
||||
lexer_misc Summary:
|
||||
AST Parsed : 377/377 (100.00%)
|
||||
Positive Passed: 377/377 (100.00%)
|
||||
AST Parsed : 380/380 (100.00%)
|
||||
Positive Passed: 380/380 (100.00%)
|
||||
@@ -1,7 +1,7 @@
|
||||
parser_misc Summary:
|
||||
AST Parsed : 120/120 (100.00%)
|
||||
Positive Passed: 120/120 (100.00%)
|
||||
Negative Passed: 259/259 (100.00%)
|
||||
AST Parsed : 121/121 (100.00%)
|
||||
Positive Passed: 121/121 (100.00%)
|
||||
Negative Passed: 261/261 (100.00%)
|
||||
|
||||
× TS(1275): 'accessor' modifier can only appear on a property declaration.
|
||||
╭─[misc/fail/accessor-modifier-abstract-method.ts:1:29]
|
||||
@@ -5109,6 +5109,20 @@ Negative Passed: 259/259 (100.00%)
|
||||
· ────────
|
||||
╰────
|
||||
|
||||
× Invalid regular expression: Invalid class atom
|
||||
╭─[misc/fail/regexp-unicode-class-control-digit.js:1:3]
|
||||
1 │ /[\c1]/u;
|
||||
· ─
|
||||
╰────
|
||||
help: This character or escape sequence is not valid inside a character class
|
||||
|
||||
× Invalid regular expression: Could not parse the entire pattern
|
||||
╭─[misc/fail/regexp-unicode-leading-zero-reference.js:1:5]
|
||||
1 │ /()\01/u;
|
||||
· ▲
|
||||
╰────
|
||||
help: Check for unbalanced brackets or invalid escape sequences
|
||||
|
||||
× Invalid rest element target in destructuring assignment
|
||||
╭─[misc/fail/rest-assignment-target.js:1:6]
|
||||
1 │ ({...[]} = {});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
semantic_misc Summary:
|
||||
AST Parsed : 120/120 (100.00%)
|
||||
Positive Passed: 114/120 (95.00%)
|
||||
AST Parsed : 121/121 (100.00%)
|
||||
Positive Passed: 115/121 (95.04%)
|
||||
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 : 120/120 (100.00%)
|
||||
Positive Passed: 120/120 (100.00%)
|
||||
AST Parsed : 121/121 (100.00%)
|
||||
Positive Passed: 121/121 (100.00%)
|
||||
Reference in new issue
Block a user