mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(regex): decode constructor patterns and flags consistently (#28298)
RegExp constructor source must decode to the same pattern and flags as JavaScript string/template values. Correct escape values, continuations, surrogate pairing, line endings, and escaped property aliases. AI-assisted implementation.
This commit is contained in:
1 parent
8a90be278d
commit
2e3126c1cf
16 files changed
+172
-110
No files matched your search
@@ -20,3 +20,4 @@ crates/oxc_linter/src/generated/assert_layouts.rs linguist-generated=true
|
||||
# Preserve the line terminators exercised by these parser regression fixtures.
|
||||
tasks/coverage/misc/pass/tagged-template-invalid-escape-cr.js -text whitespace=cr-at-eol
|
||||
tasks/coverage/misc/pass/tagged-template-invalid-escape-crlf.js -text whitespace=cr-at-eol
|
||||
tasks/coverage/misc/pass/regexp-constructor-line-endings.js -text whitespace=cr-at-eol
|
||||
@@ -93,11 +93,7 @@ impl<'a> ConstructorParser<'a> {
|
||||
(false, false)
|
||||
};
|
||||
|
||||
let pattern_text = if matches!(self.pattern_text, r#""""# | "''" | "``") {
|
||||
r#""(?:)""#
|
||||
} else {
|
||||
self.pattern_text
|
||||
};
|
||||
let pattern_text = self.pattern_text;
|
||||
let reader =
|
||||
Reader::initialize(pattern_text, unicode_mode, parse_string_literal).map_err(|_| {
|
||||
let span_start = pattern_span_offset;
|
||||
@@ -106,6 +102,14 @@ impl<'a> ConstructorParser<'a> {
|
||||
diagnostics::invalid_input(Span::new(span_start, span_end))
|
||||
})?;
|
||||
|
||||
// A literal containing only line continuations also has an empty cooked value.
|
||||
// Use the same replacement and synthetic spans as an empty quoted literal.
|
||||
let reader = if reader.peek().is_none() {
|
||||
Reader::initialize(r#""(?:)""#, unicode_mode, parse_string_literal)?
|
||||
} else {
|
||||
reader
|
||||
};
|
||||
|
||||
PatternParser::new(
|
||||
self.allocator,
|
||||
reader,
|
||||
|
||||
@@ -1926,8 +1926,6 @@ impl<'a> PatternParser<'a> {
|
||||
}
|
||||
|
||||
fn consume_unicode_property_name(&mut self) -> Option<Str<'a>> {
|
||||
let span_start = self.reader.offset();
|
||||
|
||||
let checkpoint = self.reader.checkpoint();
|
||||
while character::is_unicode_property_name_character(self.reader.peek()?) {
|
||||
self.reader.advance();
|
||||
@@ -1937,12 +1935,10 @@ impl<'a> PatternParser<'a> {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(self.reader.str(span_start, self.reader.offset()))
|
||||
Some(self.unicode_property_text(checkpoint))
|
||||
}
|
||||
|
||||
fn consume_unicode_property_value(&mut self) -> Option<Str<'a>> {
|
||||
let span_start = self.reader.offset();
|
||||
|
||||
let checkpoint = self.reader.checkpoint();
|
||||
while character::is_unicode_property_value_character(self.reader.peek()?) {
|
||||
self.reader.advance();
|
||||
@@ -1952,7 +1948,23 @@ impl<'a> PatternParser<'a> {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(self.reader.str(span_start, self.reader.offset()))
|
||||
Some(self.unicode_property_text(checkpoint))
|
||||
}
|
||||
|
||||
fn unicode_property_text(&self, checkpoint: (usize, u32)) -> Str<'a> {
|
||||
let raw = self.reader.str(checkpoint.1, self.reader.offset());
|
||||
let units = self.reader.code_points_since(checkpoint);
|
||||
// Property names and values are ASCII. Equal lengths mean no string-source
|
||||
// escapes or line continuations need decoding, so keep borrowing the source.
|
||||
if raw.as_str().len() == units.len() {
|
||||
return raw;
|
||||
}
|
||||
|
||||
let mut decoded = ArenaStringBuilder::with_capacity_in(units.len(), self.allocator);
|
||||
for unit in units {
|
||||
decoded.push(char::from_u32(unit.value).expect("validated Unicode property character"));
|
||||
}
|
||||
Str::from(decoded.into_str())
|
||||
}
|
||||
|
||||
// ```
|
||||
|
||||
@@ -19,7 +19,21 @@ pub fn is_line_terminator(ch: char) -> bool {
|
||||
// ' " \ b f n r t v
|
||||
// ```
|
||||
pub fn is_single_escape_character(ch: char) -> bool {
|
||||
matches!(ch, '\'' | '"' | '\\' | 'b' | 'f' | 'n' | 'r' | 't' | 'v')
|
||||
map_single_escape_character(ch).is_some()
|
||||
}
|
||||
|
||||
/// Code unit value of a `SingleEscapeCharacter` (ECMA-262, Table 33).
|
||||
pub fn map_single_escape_character(ch: char) -> Option<u32> {
|
||||
Some(match ch {
|
||||
'b' => 0x08,
|
||||
't' => 0x09,
|
||||
'n' => 0x0a,
|
||||
'v' => 0x0b,
|
||||
'f' => 0x0c,
|
||||
'r' => 0x0d,
|
||||
'\'' | '"' | '\\' => ch as u32,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
// ```
|
||||
|
||||
+40
-45
@@ -5,10 +5,11 @@ use crate::parser::reader::{
|
||||
CodePoint, Options,
|
||||
ast::EscapeKind,
|
||||
characters::{
|
||||
CR, LF, LS, PS, is_line_terminator, is_non_escape_character, is_single_escape_character,
|
||||
CR, LF, LS, PS, is_line_terminator, is_non_escape_character, map_single_escape_character,
|
||||
},
|
||||
string_literal_parser::{ast, diagnostics},
|
||||
};
|
||||
use crate::surrogate_pair;
|
||||
|
||||
// Internal representation of escape sequence resolved unit in a string literal.
|
||||
// ((start, end), code_point, escape_kind)
|
||||
@@ -58,6 +59,18 @@ impl Parser {
|
||||
) {
|
||||
let span = Span::new(span_offset + offsets.0, span_offset + offsets.1);
|
||||
|
||||
// Unicode patterns decode adjacent UTF-16 surrogate halves into one code point.
|
||||
// Both halves came from Unicode escapes, so retain the leading escape kind.
|
||||
if combine_surrogate_pair
|
||||
&& surrogate_pair::is_trail_surrogate(cp)
|
||||
&& let Some(previous) = body.last_mut()
|
||||
&& surrogate_pair::is_lead_surrogate(previous.value)
|
||||
{
|
||||
previous.value = surrogate_pair::combine_surrogate_pair(previous.value, cp);
|
||||
previous.span = previous.span.merge(span);
|
||||
return;
|
||||
}
|
||||
|
||||
if combine_surrogate_pair || (0..=0xffff).contains(&cp) {
|
||||
// If the code point is in the BMP or if forced, just push it
|
||||
body.push(CodePoint { span, value: cp, escape_kind });
|
||||
@@ -117,7 +130,14 @@ impl Parser {
|
||||
// ```
|
||||
fn parse_string_characters(&mut self, single_or_double_quote: char) -> Result<Vec<CodePoint>> {
|
||||
let mut body = vec![];
|
||||
while let Some(code_point) = self.parse_string_character(single_or_double_quote)? {
|
||||
loop {
|
||||
// A LineContinuation contributes no code units to the string value.
|
||||
if self.parse_line_continuation() {
|
||||
continue;
|
||||
}
|
||||
let Some(code_point) = self.parse_string_character(single_or_double_quote)? else {
|
||||
break;
|
||||
};
|
||||
Parser::handle_code_point(
|
||||
&mut body,
|
||||
code_point,
|
||||
@@ -171,10 +191,6 @@ impl Parser {
|
||||
}
|
||||
self.rewind(checkpoint);
|
||||
}
|
||||
if let Some(cp) = self.parse_line_terminator_sequence() {
|
||||
return Ok(Some(((offset_start, self.offset()), cp, EscapeKind::None)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -239,9 +255,9 @@ impl Parser {
|
||||
// NonEscapeCharacter
|
||||
// ```
|
||||
fn parse_character_escape_sequence(&mut self) -> Option<u32> {
|
||||
if let Some(ch) = self.peek().filter(|&ch| is_single_escape_character(ch)) {
|
||||
if let Some(cp) = self.peek().and_then(map_single_escape_character) {
|
||||
self.advance();
|
||||
return Some(ch as u32);
|
||||
return Some(cp);
|
||||
}
|
||||
if let Some(ch) = self.peek().filter(|&ch| is_non_escape_character(ch)) {
|
||||
self.advance();
|
||||
@@ -271,16 +287,16 @@ impl Parser {
|
||||
fn parse_legacy_octal_escape_sequence(&mut self) -> Option<u32> {
|
||||
if let Some(first) = self.consume_octal_digit() {
|
||||
// 0 [lookahead ∈ { 8, 9 }]
|
||||
if first == 0 && self.peek().as_ref().is_some_and(|&ch| !matches!(ch, '8' | '9')) {
|
||||
if first == 0 && self.peek().as_ref().is_some_and(|&ch| matches!(ch, '8' | '9')) {
|
||||
return Some(first);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
// FourToSeven permits only two digits; leave a third for the next character.
|
||||
if first <= 3
|
||||
&& let Some(third) = self.consume_octal_digit()
|
||||
{
|
||||
return Some(first * 64 + second * 8 + third);
|
||||
}
|
||||
|
||||
// ZeroToThree OctalDigit [lookahead ∉ OctalDigit]
|
||||
@@ -369,38 +385,17 @@ impl Parser {
|
||||
// <PS>
|
||||
// <CR> <LF>
|
||||
// ```
|
||||
fn parse_line_terminator_sequence(&mut self) -> Option<u32> {
|
||||
let checkpoint = self.checkpoint();
|
||||
|
||||
if self.eat('\\') {
|
||||
if self.peek() == Some(LF) {
|
||||
self.advance();
|
||||
return Some(LF as u32);
|
||||
}
|
||||
if self.peek() == Some(CR) && self.peek2() != Some(LF) {
|
||||
self.advance();
|
||||
return Some(CR as u32);
|
||||
}
|
||||
if self.peek() == Some(LS) {
|
||||
self.advance();
|
||||
return Some(LS as u32);
|
||||
}
|
||||
if self.peek() == Some(PS) {
|
||||
self.advance();
|
||||
return Some(PS as u32);
|
||||
}
|
||||
// NOTE: CR+LF can not represent as a single code point.
|
||||
// I don't know the best way to handle this.
|
||||
// To distinguish this from CR and LF, structural change is needed...
|
||||
if self.peek() == Some(CR) && self.peek2() == Some(LF) {
|
||||
self.advance();
|
||||
self.advance();
|
||||
return Some(LF as u32);
|
||||
}
|
||||
fn parse_line_continuation(&mut self) -> bool {
|
||||
if self.peek() != Some('\\') || !self.peek2().is_some_and(is_line_terminator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.rewind(checkpoint);
|
||||
None
|
||||
self.advance(); // backslash
|
||||
let terminator = self.peek();
|
||||
self.advance();
|
||||
if terminator == Some(CR) {
|
||||
self.eat(LF);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
+40
-41
@@ -5,10 +5,11 @@ use crate::parser::reader::{
|
||||
Options,
|
||||
ast::{CodePoint, EscapeKind},
|
||||
characters::{
|
||||
CR, LF, LS, PS, is_line_terminator, is_non_escape_character, is_single_escape_character,
|
||||
CR, LF, LS, PS, is_line_terminator, is_non_escape_character, map_single_escape_character,
|
||||
},
|
||||
template_literal_parser::{ast, diagnostics},
|
||||
};
|
||||
use crate::surrogate_pair;
|
||||
|
||||
// Internal representation of escape sequence resolved unit in a template literal.
|
||||
// ((start, end), code_point, escape_kind)
|
||||
@@ -34,6 +35,18 @@ impl Parser {
|
||||
) {
|
||||
let span = Span::new(span_offset + offsets.0, span_offset + offsets.1);
|
||||
|
||||
// Unicode patterns decode adjacent UTF-16 surrogate halves into one code point.
|
||||
// Both halves came from Unicode escapes, so retain the leading escape kind.
|
||||
if combine_surrogate_pair
|
||||
&& surrogate_pair::is_trail_surrogate(cp)
|
||||
&& let Some(previous) = body.last_mut()
|
||||
&& surrogate_pair::is_lead_surrogate(previous.value)
|
||||
{
|
||||
previous.value = surrogate_pair::combine_surrogate_pair(previous.value, cp);
|
||||
previous.span = previous.span.merge(span);
|
||||
return;
|
||||
}
|
||||
|
||||
if combine_surrogate_pair || (0..=0xffff).contains(&cp) {
|
||||
// If the code point is in the BMP or if forced, just push it
|
||||
body.push(CodePoint { span, value: cp, escape_kind });
|
||||
@@ -101,7 +114,14 @@ impl Parser {
|
||||
// ```
|
||||
fn parse_template_characters(&mut self) -> Result<Vec<CodePoint>> {
|
||||
let mut body = vec![];
|
||||
while let Some(code_point) = self.parse_template_character()? {
|
||||
loop {
|
||||
// A LineContinuation contributes no code units to the cooked value.
|
||||
if self.parse_line_continuation() {
|
||||
continue;
|
||||
}
|
||||
let Some(code_point) = self.parse_template_character()? else {
|
||||
break;
|
||||
};
|
||||
Parser::handle_code_point(
|
||||
&mut body,
|
||||
code_point,
|
||||
@@ -138,9 +158,6 @@ impl Parser {
|
||||
return Ok(Some(((offset_start, self.offset()), cp, EscapeKind::None)));
|
||||
}
|
||||
}
|
||||
if let Some(cp) = self.parse_line_continuation() {
|
||||
return Ok(Some(((offset_start, self.offset()), cp, EscapeKind::None)));
|
||||
}
|
||||
if let Some(cp) = self.parse_line_terminator_sequence() {
|
||||
return Ok(Some(((offset_start, self.offset()), cp, EscapeKind::None)));
|
||||
}
|
||||
@@ -200,9 +217,9 @@ impl Parser {
|
||||
// NonEscapeCharacter
|
||||
// ```
|
||||
fn parse_character_escape_sequence(&mut self) -> Option<u32> {
|
||||
if let Some(ch) = self.peek().filter(|&ch| is_single_escape_character(ch)) {
|
||||
if let Some(cp) = self.peek().and_then(map_single_escape_character) {
|
||||
self.advance();
|
||||
return Some(ch as u32);
|
||||
return Some(cp);
|
||||
}
|
||||
if let Some(ch) = self.peek().filter(|&ch| is_non_escape_character(ch)) {
|
||||
self.advance();
|
||||
@@ -400,17 +417,15 @@ impl Parser {
|
||||
// \ LineTerminatorSequence
|
||||
//
|
||||
// ```
|
||||
fn parse_line_continuation(&mut self) -> Option<u32> {
|
||||
fn parse_line_continuation(&mut self) -> bool {
|
||||
let checkpoint = self.checkpoint();
|
||||
|
||||
if self.eat('\\')
|
||||
&& let Some(terminator) = self.parse_line_terminator_sequence()
|
||||
{
|
||||
return Some(terminator);
|
||||
if self.eat('\\') && self.parse_line_terminator_sequence().is_some() {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.rewind(checkpoint);
|
||||
None
|
||||
false
|
||||
}
|
||||
|
||||
// ```
|
||||
@@ -422,35 +437,19 @@ impl Parser {
|
||||
// <CR> <LF>
|
||||
// ```
|
||||
fn parse_line_terminator_sequence(&mut self) -> Option<u32> {
|
||||
let checkpoint = self.checkpoint();
|
||||
|
||||
if self.peek() == Some(LF) {
|
||||
self.advance();
|
||||
return Some(LF as u32);
|
||||
match self.peek()? {
|
||||
CR => {
|
||||
self.advance();
|
||||
self.eat(LF);
|
||||
// TV normalizes both CR and CRLF to a single LF.
|
||||
Some(LF as u32)
|
||||
}
|
||||
ch @ (LF | LS | PS) => {
|
||||
self.advance();
|
||||
Some(ch as u32)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
if self.peek() == Some(CR) && self.peek2() != Some(LF) {
|
||||
self.advance();
|
||||
return Some(CR as u32);
|
||||
}
|
||||
if self.peek() == Some(LS) {
|
||||
self.advance();
|
||||
return Some(LS as u32);
|
||||
}
|
||||
if self.peek() == Some(PS) {
|
||||
self.advance();
|
||||
return Some(PS as u32);
|
||||
}
|
||||
// NOTE: CR+LF can not represent as a single code point.
|
||||
// I don't know the best way to handle this.
|
||||
// To distinguish this from CR and LF, structural change is needed...
|
||||
if self.peek() == Some(CR) && self.peek2() == Some(LF) {
|
||||
self.advance();
|
||||
self.advance();
|
||||
return Some(LF as u32);
|
||||
}
|
||||
|
||||
self.rewind(checkpoint);
|
||||
None
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
new RegExp("\b\f\n\r\t\v");
|
||||
new RegExp("\n", "u");
|
||||
new RegExp("\t", "g");
|
||||
new RegExp("a\
|
||||
b");
|
||||
new RegExp("\
|
||||
ab");
|
||||
new RegExp("ab\
|
||||
");
|
||||
new RegExp("\
|
||||
");
|
||||
new RegExp("a\\nb");
|
||||
new RegExp(`\b\f\n\r\t\v`);
|
||||
new RegExp(`a\
|
||||
b`);
|
||||
new RegExp(`a
|
||||
b`);
|
||||
@@ -0,0 +1,7 @@
|
||||
new RegExp("\01");
|
||||
new RegExp("\012", "u");
|
||||
new RegExp("\377", "v");
|
||||
new RegExp("\477");
|
||||
new RegExp("\777");
|
||||
new RegExp("\08");
|
||||
new RegExp("\09");
|
||||
@@ -0,0 +1,9 @@
|
||||
new RegExp(`a
b`);
|
||||
new RegExp(`a
|
||||
b`);
|
||||
new RegExp("a\
b");
|
||||
new RegExp("a\
|
||||
b");
|
||||
new RegExp(`a\
b`);
|
||||
new RegExp(`a\
|
||||
b`);
|
||||
@@ -0,0 +1,4 @@
|
||||
new RegExp("\\p{\u0041SCII}", "u");
|
||||
new RegExp("\\p{\x67c=\u004c}", "u");
|
||||
new RegExp("\\p{Script=\u0047reek}", "\u0075");
|
||||
new RegExp(`\\p{\u0053cript_Extensions=\x48ira}`, "v");
|
||||
@@ -1,3 +1,3 @@
|
||||
codegen_misc Summary:
|
||||
AST Parsed : 121/121 (100.00%)
|
||||
Positive Passed: 121/121 (100.00%)
|
||||
AST Parsed : 125/125 (100.00%)
|
||||
Positive Passed: 125/125 (100.00%)
|
||||
@@ -1,3 +1,3 @@
|
||||
formatter_misc Summary:
|
||||
AST Parsed : 121/121 (100.00%)
|
||||
Positive Passed: 121/121 (100.00%)
|
||||
AST Parsed : 125/125 (100.00%)
|
||||
Positive Passed: 125/125 (100.00%)
|
||||
@@ -1,3 +1,3 @@
|
||||
lexer_misc Summary:
|
||||
AST Parsed : 384/384 (100.00%)
|
||||
Positive Passed: 384/384 (100.00%)
|
||||
AST Parsed : 388/388 (100.00%)
|
||||
Positive Passed: 388/388 (100.00%)
|
||||
@@ -1,6 +1,6 @@
|
||||
parser_misc Summary:
|
||||
AST Parsed : 121/121 (100.00%)
|
||||
Positive Passed: 121/121 (100.00%)
|
||||
AST Parsed : 125/125 (100.00%)
|
||||
Positive Passed: 125/125 (100.00%)
|
||||
Negative Passed: 265/265 (100.00%)
|
||||
|
||||
× TS(1275): 'accessor' modifier can only appear on a property declaration.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
semantic_misc Summary:
|
||||
AST Parsed : 121/121 (100.00%)
|
||||
Positive Passed: 115/121 (95.04%)
|
||||
AST Parsed : 125/125 (100.00%)
|
||||
Positive Passed: 119/125 (95.20%)
|
||||
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 : 121/121 (100.00%)
|
||||
Positive Passed: 121/121 (100.00%)
|
||||
AST Parsed : 125/125 (100.00%)
|
||||
Positive Passed: 125/125 (100.00%)
|
||||
Reference in new issue
Block a user