mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(regex): reject templates without cooked values (#28345)
Reject no-substitution template inputs without a defined cooked value, so RegExp constructor parsing cannot invent characters for invalid escapes. AI-assisted implementation.
This commit is contained in:
1 parent
c2d663de29
commit
8e6165265e
10 files changed
+91
-134
No files matched your search
@@ -53,11 +53,12 @@ impl<'a, 'b> LiteralParser<'a, 'b> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Regular expression parser for `new RegExp("constrocutor")` usage.
|
||||
/// - `pattern_text`: the string literal text as 1st argument of `RegExp` constructor
|
||||
/// - `flags_text`: the string literal text as 2nd argument of `RegExp` constructor
|
||||
/// Regular expression parser for `RegExp` constructor arguments.
|
||||
/// - `pattern_text`: the source text of the first argument
|
||||
/// - `flags_text`: the source text of the second argument
|
||||
///
|
||||
/// String literal text should be in the form of `'...'` or `"..."` and may contain escape sequences.
|
||||
/// Arguments must be quoted strings (`'...'` or `"..."`) or no-substitution
|
||||
/// templates with a defined cooked value. Escape sequences are decoded.
|
||||
pub struct ConstructorParser<'a> {
|
||||
allocator: &'a Allocator,
|
||||
pattern_text: &'a str,
|
||||
|
||||
+6
-112
@@ -154,9 +154,12 @@ impl Parser {
|
||||
if let Some((cp, escape_kind)) = self.parse_template_escape_sequence(offset_start)? {
|
||||
return Ok(Some(((offset_start, self.offset()), cp, escape_kind)));
|
||||
}
|
||||
if let Some(cp) = self.parse_not_escape_sequence()? {
|
||||
return Ok(Some(((offset_start, self.offset()), cp, EscapeKind::None)));
|
||||
}
|
||||
// NotEscapeSequence has no cooked value. Constructor arguments are
|
||||
// untagged templates, so these escapes cannot form a pattern string.
|
||||
return Err(diagnostics::invalid_input(Span::new(
|
||||
self.options.span_offset + offset_start,
|
||||
self.options.span_offset + self.offset(),
|
||||
)));
|
||||
}
|
||||
if let Some(cp) = self.parse_line_terminator_sequence() {
|
||||
return Ok(Some(((offset_start, self.offset()), cp, EscapeKind::None)));
|
||||
@@ -229,115 +232,6 @@ impl Parser {
|
||||
None
|
||||
}
|
||||
|
||||
// ```
|
||||
// NotEscapeSequence ::
|
||||
// 0 DecimalDigit
|
||||
// DecimalDigit but not 0
|
||||
// x [lookahead ∉ HexDigit]
|
||||
// x HexDigit [lookahead ∉ HexDigit]
|
||||
// u [lookahead ∉ HexDigit] [lookahead ≠ {]
|
||||
// u HexDigit [lookahead ∉ HexDigit]
|
||||
// u HexDigit HexDigit [lookahead ∉ HexDigit]
|
||||
// u HexDigit HexDigit HexDigit [lookahead ∉ HexDigit]
|
||||
// u { [lookahead ∉ HexDigit]
|
||||
// u { NotCodePoint [lookahead ∉ HexDigit]
|
||||
// u { CodePoint [lookahead ∉ HexDigit] [lookahead ≠ }]
|
||||
// ```
|
||||
fn parse_not_escape_sequence(&mut self) -> Result<Option<u32>> {
|
||||
let checkpoint = self.checkpoint();
|
||||
|
||||
// 0 DecimalDigit
|
||||
if self.eat('0') {
|
||||
if let Some(ch) = self.peek()
|
||||
&& ch.is_ascii_digit()
|
||||
{
|
||||
self.advance();
|
||||
return Ok(Some(ch as u32));
|
||||
}
|
||||
self.rewind(checkpoint);
|
||||
}
|
||||
|
||||
// DecimalDigit but not 0
|
||||
if let Some(ch) = self.peek()
|
||||
&& ch.is_ascii_digit()
|
||||
&& ch != '0'
|
||||
{
|
||||
self.advance();
|
||||
return Ok(Some(ch as u32));
|
||||
}
|
||||
|
||||
// x [lookahead ∉ HexDigit] or x HexDigit [lookahead ∉ HexDigit]
|
||||
if self.eat('x') {
|
||||
let offset_start = self.offset();
|
||||
match self.consume_hex_digits(offset_start) {
|
||||
Ok(Some(_)) => {
|
||||
self.rewind(checkpoint);
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(None) => {
|
||||
return Ok(Some('x' as u32));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// u [lookahead ∉ HexDigit] [lookahead ≠ {]
|
||||
// u HexDigit [lookahead ∉ HexDigit]
|
||||
// u HexDigit HexDigit [lookahead ∉ HexDigit]
|
||||
// u HexDigit HexDigit HexDigit [lookahead ∉ HexDigit]
|
||||
// u { [lookahead ∉ HexDigit]
|
||||
// u { NotCodePoint [lookahead ∉ HexDigit]
|
||||
// u { CodePoint [lookahead ∉ HexDigit] [lookahead ≠ }]
|
||||
if self.eat('u') {
|
||||
let offset_start = self.offset();
|
||||
if self.eat('{') {
|
||||
match self.consume_hex_digits(offset_start) {
|
||||
Ok(Some(_)) => {
|
||||
if !self.eat('}') {
|
||||
return Ok(Some('u' as u32));
|
||||
}
|
||||
self.rewind(checkpoint);
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(None) => {
|
||||
return Ok(Some('u' as u32));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut hex_count = 0;
|
||||
for _ in 0..4 {
|
||||
if let Some(ch) = self.peek() {
|
||||
if ch.is_ascii_hexdigit() {
|
||||
self.advance();
|
||||
hex_count += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if hex_count == 0 || hex_count < 4 {
|
||||
return Ok(Some('u' as u32));
|
||||
}
|
||||
if let Some(ch) = self.peek()
|
||||
&& ch.is_ascii_hexdigit()
|
||||
{
|
||||
self.rewind(checkpoint);
|
||||
return Ok(None);
|
||||
}
|
||||
self.rewind(checkpoint);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// ```
|
||||
// HexEscapeSequence ::
|
||||
// x HexDigit HexDigit
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const tag = () => {};
|
||||
|
||||
tag`\1`;
|
||||
tag`\2`;
|
||||
tag`\3`;
|
||||
tag`\4`;
|
||||
tag`\5`;
|
||||
tag`\6`;
|
||||
tag`\7`;
|
||||
tag`\8`;
|
||||
tag`\9`;
|
||||
tag`\00`;
|
||||
tag`\01`;
|
||||
tag`\02`;
|
||||
tag`\03`;
|
||||
tag`\04`;
|
||||
tag`\05`;
|
||||
tag`\06`;
|
||||
tag`\07`;
|
||||
tag`\08`;
|
||||
tag`\09`;
|
||||
|
||||
tag`\x`;
|
||||
tag`\x0`;
|
||||
tag`\u`;
|
||||
tag`\u000`;
|
||||
tag`\u{}`;
|
||||
tag`\u{110000}`;
|
||||
|
||||
new RegExp(`\0`);
|
||||
new RegExp(`\x30`);
|
||||
new RegExp(`\u0030`);
|
||||
new RegExp(`\u{30}`);
|
||||
new RegExp(`\z`);
|
||||
new RegExp(`\$`);
|
||||
new RegExp(`\``);
|
||||
new RegExp(`\\`);
|
||||
@@ -1,3 +1,3 @@
|
||||
codegen_misc Summary:
|
||||
AST Parsed : 127/127 (100.00%)
|
||||
Positive Passed: 127/127 (100.00%)
|
||||
AST Parsed : 128/128 (100.00%)
|
||||
Positive Passed: 128/128 (100.00%)
|
||||
@@ -1,3 +1,3 @@
|
||||
formatter_misc Summary:
|
||||
AST Parsed : 127/127 (100.00%)
|
||||
Positive Passed: 127/127 (100.00%)
|
||||
AST Parsed : 128/128 (100.00%)
|
||||
Positive Passed: 128/128 (100.00%)
|
||||
@@ -1,3 +1,3 @@
|
||||
lexer_misc Summary:
|
||||
AST Parsed : 390/390 (100.00%)
|
||||
Positive Passed: 390/390 (100.00%)
|
||||
AST Parsed : 391/391 (100.00%)
|
||||
Positive Passed: 391/391 (100.00%)
|
||||
@@ -1,6 +1,6 @@
|
||||
parser_misc Summary:
|
||||
AST Parsed : 127/127 (100.00%)
|
||||
Positive Passed: 127/127 (100.00%)
|
||||
AST Parsed : 128/128 (100.00%)
|
||||
Positive Passed: 128/128 (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 : 127/127 (100.00%)
|
||||
Positive Passed: 121/127 (95.28%)
|
||||
AST Parsed : 128/128 (100.00%)
|
||||
Positive Passed: 122/128 (95.31%)
|
||||
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 : 127/127 (100.00%)
|
||||
Positive Passed: 127/127 (100.00%)
|
||||
AST Parsed : 128/128 (100.00%)
|
||||
Positive Passed: 128/128 (100.00%)
|
||||
@@ -7,14 +7,14 @@ use oxc::{
|
||||
allocator::Allocator,
|
||||
ast::{
|
||||
AstKind, Comment,
|
||||
ast::{Program, RegExpLiteral},
|
||||
ast::{Program, RegExpLiteral, TemplateLiteral},
|
||||
},
|
||||
ast_visit::{Visit, walk},
|
||||
codegen::{CodegenOptions, CodegenReturn},
|
||||
diagnostics::{Diagnostics, OxcDiagnostic},
|
||||
minifier::CompressOptions,
|
||||
parser::{ParseOptions, ParserReturn},
|
||||
regular_expression::{LiteralParser, Options},
|
||||
regular_expression::{ConstructorParser, LiteralParser, Options},
|
||||
semantic::SemanticBuilderReturn,
|
||||
span::{ContentEq, SourceType, Span},
|
||||
syntax::node::NodeId,
|
||||
@@ -82,7 +82,7 @@ impl CompilerInterface for Driver {
|
||||
let ParserReturn { program, fatal_error, diagnostics, .. } = parser_return;
|
||||
self.fatal_error = *fatal_error;
|
||||
self.source_type = Some(program.source_type);
|
||||
self.check_ast_nodes(program);
|
||||
self.check_ast_nodes(program, diagnostics.is_empty() && !*fatal_error);
|
||||
if self.check_comments(&program.comments) {
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
@@ -165,8 +165,8 @@ impl Driver {
|
||||
false
|
||||
}
|
||||
|
||||
fn check_ast_nodes(&mut self, program: &Program<'_>) {
|
||||
CheckASTNodes::new(self, program.source_text).check(program);
|
||||
fn check_ast_nodes(&mut self, program: &Program<'_>, check_template_cooked_values: bool) {
|
||||
CheckASTNodes::new(self, program.source_text, check_template_cooked_values).check(program);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,15 +175,21 @@ struct CheckASTNodes<'a> {
|
||||
source_text: &'a str,
|
||||
allocator: Allocator,
|
||||
node_ids: FxHashSet<NodeId>,
|
||||
check_template_cooked_values: bool,
|
||||
}
|
||||
|
||||
impl<'a> CheckASTNodes<'a> {
|
||||
fn new(driver: &'a mut Driver, source_text: &'a str) -> Self {
|
||||
fn new(
|
||||
driver: &'a mut Driver,
|
||||
source_text: &'a str,
|
||||
check_template_cooked_values: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
driver,
|
||||
source_text,
|
||||
allocator: Allocator::default(),
|
||||
node_ids: FxHashSet::default(),
|
||||
check_template_cooked_values,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +199,25 @@ impl<'a> CheckASTNodes<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Visit<'a> for CheckASTNodes<'a> {
|
||||
fn visit_template_literal(&mut self, template: &TemplateLiteral<'a>) {
|
||||
walk::walk_template_literal(self, template);
|
||||
if self.check_template_cooked_values
|
||||
&& template.expressions.is_empty()
|
||||
&& let [quasi] = template.quasis.as_slice()
|
||||
&& quasi.value.cooked.is_none()
|
||||
{
|
||||
// Tagged templates can contain NotEscapeSequence, but a standalone
|
||||
// constructor argument needs a string value from an untagged template.
|
||||
let raw = template.span.source_text(self.source_text);
|
||||
assert!(
|
||||
ConstructorParser::new(&self.allocator, raw, None, Options::default())
|
||||
.parse()
|
||||
.is_err(),
|
||||
"RegExp constructor accepted a template without a cooked value: {raw:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_node(&mut self, kind: AstKind<'a>) {
|
||||
let id = kind.node_id();
|
||||
// These are AST invariants, including for recovered syntax errors.
|
||||
|
||||
Reference in new issue
Block a user