Skip to content

Commit c94aa23

Browse files
committed
fix(regex-parser): parse simple TemplateLiterals
1 parent c215619 commit c94aa23

File tree

18 files changed

+815
-80
lines changed

18 files changed

+815
-80
lines changed

crates/oxc_linter/src/rules/eslint/no_control_regex.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,9 @@ mod tests {
317317
r"/\u{1F}/",
318318
r"/\u{1F}/g",
319319
r"new RegExp('\\u{20}', 'u')",
320+
r"new RegExp('\\u{20}', `u`)",
320321
r"new RegExp('\\u{1F}')",
322+
r"new RegExp(`\\u{1F}`)",
321323
r"new RegExp('\\u{1F}', 'g')",
322324
r"new RegExp('\\u{1F}', flags)", // unknown flags, we assume no 'u'
323325
// https://github.com/oxc-project/oxc/issues/6136
@@ -347,6 +349,8 @@ mod tests {
347349
r"/\u{1F}/u",
348350
r"/\u{1F}/ugi",
349351
r"new RegExp('\\u{1F}', 'u')",
352+
r"new RegExp(`\\u{1F}`, 'u')",
353+
r"new RegExp('\\u{1F}', `u`)",
350354
r"new RegExp('\\u{1F}', 'ugi')",
351355
// https://github.com/oxc-project/oxc/issues/6136
352356
r"/\u{0a}/u",

crates/oxc_linter/src/rules/eslint/no_useless_backreference.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,7 @@ fn test() {
365365
r"new RegExp(`${prefix}\\1(a)`)",
366366
r"let RegExp; new RegExp('\\1(a)');",
367367
r"function foo() { var RegExp; RegExp('\\1(a)', 'u'); }",
368+
r"function foo() { var RegExp; RegExp('\\1(a)', `u`); }",
368369
r"function foo(RegExp) { new RegExp('\\1(a)'); }",
369370
r"if (foo) { const RegExp = bar; RegExp('\\1(a)'); }",
370371
// we don't support globals off yet
476 Bytes
Binary file not shown.

crates/oxc_linter/src/utils/regex.rs

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,69 @@ use oxc_span::Span;
99

1010
use crate::{AstNode, context::LintContext};
1111

12+
fn run_on_arguments<'a, M>(
13+
arg1: Option<&'a Argument>,
14+
arg2: Option<&'a Argument>,
15+
ctx: &'a LintContext<'_>,
16+
cb: M,
17+
) where
18+
M: FnOnce(&Pattern<'_>, Span),
19+
{
20+
// note: improvements required for strings used via identifier references
21+
// Missing or non-string arguments will be runtime errors, but are not covered by this rule.
22+
match (arg1, arg2) {
23+
(Some(Argument::StringLiteral(pattern)), Some(Argument::StringLiteral(flags))) => {
24+
let allocator = Allocator::default();
25+
if let Some(pat) = parse_regex(&allocator, pattern.span, Some(flags.span), ctx) {
26+
cb(&pat, pattern.span);
27+
}
28+
}
29+
(Some(Argument::StringLiteral(pattern)), Some(Argument::TemplateLiteral(flags))) => {
30+
if !flags.is_no_substitution_template() {
31+
return;
32+
}
33+
let allocator = Allocator::default();
34+
if let Some(pat) = parse_regex(&allocator, pattern.span, Some(flags.span), ctx) {
35+
cb(&pat, pattern.span);
36+
}
37+
}
38+
(Some(Argument::StringLiteral(pattern)), _) => {
39+
let allocator = Allocator::default();
40+
if let Some(pat) = parse_regex(&allocator, pattern.span, None, ctx) {
41+
cb(&pat, pattern.span);
42+
}
43+
}
44+
(Some(Argument::TemplateLiteral(pattern)), Some(Argument::TemplateLiteral(flags))) => {
45+
if !pattern.is_no_substitution_template() || !flags.is_no_substitution_template() {
46+
return;
47+
}
48+
let allocator = Allocator::default();
49+
if let Some(pat) = parse_regex(&allocator, pattern.span, Some(flags.span), ctx) {
50+
cb(&pat, pattern.span);
51+
}
52+
}
53+
(Some(Argument::TemplateLiteral(pattern)), Some(Argument::StringLiteral(flags))) => {
54+
if !pattern.is_no_substitution_template() {
55+
return;
56+
}
57+
let allocator = Allocator::default();
58+
if let Some(pat) = parse_regex(&allocator, pattern.span, Some(flags.span), ctx) {
59+
cb(&pat, pattern.span);
60+
}
61+
}
62+
(Some(Argument::TemplateLiteral(pattern)), _) => {
63+
if !pattern.is_no_substitution_template() {
64+
return;
65+
}
66+
let allocator = Allocator::default();
67+
if let Some(pat) = parse_regex(&allocator, pattern.span, None, ctx) {
68+
cb(&pat, pattern.span);
69+
}
70+
}
71+
_ => {}
72+
}
73+
}
74+
1275
pub fn run_on_regex_node<'a, 'b, M>(node: &'a AstNode<'b>, ctx: &'a LintContext<'b>, cb: M)
1376
where
1477
M: FnOnce(&Pattern<'_>, Span),
@@ -20,46 +83,12 @@ where
2083
}
2184
}
2285
AstKind::NewExpression(expr) if is_regexp_callee(&expr.callee, ctx) => {
23-
// note: improvements required for strings used via identifier references
24-
// Missing or non-string arguments will be runtime errors, but are not covered by this rule.
25-
match (&expr.arguments.first(), &expr.arguments.get(1)) {
26-
(Some(Argument::StringLiteral(pattern)), Some(Argument::StringLiteral(flags))) => {
27-
let allocator = Allocator::default();
28-
if let Some(pat) = parse_regex(&allocator, pattern.span, Some(flags.span), ctx)
29-
{
30-
cb(&pat, pattern.span);
31-
}
32-
}
33-
(Some(Argument::StringLiteral(pattern)), _) => {
34-
let allocator = Allocator::default();
35-
if let Some(pat) = parse_regex(&allocator, pattern.span, None, ctx) {
36-
cb(&pat, pattern.span);
37-
}
38-
}
39-
_ => {}
40-
}
86+
run_on_arguments(expr.arguments.first(), expr.arguments.get(1), ctx, cb);
4187
}
4288

4389
// RegExp()
4490
AstKind::CallExpression(expr) if is_regexp_callee(&expr.callee, ctx) => {
45-
// note: improvements required for strings used via identifier references
46-
// Missing or non-string arguments will be runtime errors, but are not covered by this rule.
47-
match (&expr.arguments.first(), &expr.arguments.get(1)) {
48-
(Some(Argument::StringLiteral(pattern)), Some(Argument::StringLiteral(flags))) => {
49-
let allocator = Allocator::default();
50-
if let Some(pat) = parse_regex(&allocator, pattern.span, Some(flags.span), ctx)
51-
{
52-
cb(&pat, pattern.span);
53-
}
54-
}
55-
(Some(Argument::StringLiteral(pattern)), _) => {
56-
let allocator = Allocator::default();
57-
if let Some(pat) = parse_regex(&allocator, pattern.span, None, ctx) {
58-
cb(&pat, pattern.span);
59-
}
60-
}
61-
_ => {}
62-
}
91+
run_on_arguments(expr.arguments.first(), expr.arguments.get(1), ctx, cb);
6392
}
6493
_ => {}
6594
}

crates/oxc_regular_expression/src/parser/parser_impl.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ impl<'a> ConstructorParser<'a> {
9393
(false, false)
9494
};
9595

96-
let pattern_text = if matches!(self.pattern_text, r#""""# | "''") {
96+
let pattern_text = if matches!(self.pattern_text, r#""""# | "''" | "``") {
9797
r#""(?:)""#
9898
} else {
9999
self.pattern_text
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use oxc_span::Span;
2+
3+
/// Represents UTF-16 code unit(u16 as u32) or Unicode code point(char as u32).
4+
/// `Span` width may be more than 1, since there will be escape sequences.
5+
#[derive(Debug, Clone, Copy)]
6+
pub struct CodePoint {
7+
pub span: Span,
8+
// NOTE: If we need codegen, more information should be added.
9+
pub value: u32,
10+
}

crates/oxc_regular_expression/src/parser/reader/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
mod ast;
2+
mod characters;
3+
mod options;
14
mod reader_impl;
25
mod string_literal_parser;
6+
mod template_literal_parser;
37

8+
pub use ast::*;
9+
pub use options::Options;
410
pub use reader_impl::Reader;
511

612
#[cfg(test)]

crates/oxc_regular_expression/src/parser/reader/reader_impl.rs

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
use oxc_diagnostics::Result;
22
use oxc_span::Atom;
33

4-
use crate::parser::reader::string_literal_parser::{
5-
Options as StringLiteralParserOptions, Parser as StringLiteralParser, ast as StringLiteralAst,
6-
parse_regexp_literal,
4+
use crate::parser::reader::{
5+
Options,
6+
ast::CodePoint,
7+
string_literal_parser::{
8+
Parser as StringLiteralParser, ast as StringLiteralAst, parse_regexp_literal,
9+
},
10+
template_literal_parser::{Parser as TemplateLiteralParser, ast as TemplateLiteralAst},
711
};
812

13+
#[derive(Debug)]
914
pub struct Reader<'a> {
1015
source_text: &'a str,
11-
units: Vec<StringLiteralAst::CodePoint>,
16+
units: Vec<CodePoint>,
1217
index: usize,
1318
offset: u32,
1419
}
@@ -17,24 +22,37 @@ impl<'a> Reader<'a> {
1722
pub fn initialize(
1823
source_text: &'a str,
1924
unicode_mode: bool,
20-
parse_string_literal: bool,
25+
parse_string_or_template_literal: bool,
2126
) -> Result<Self> {
2227
// NOTE: This must be `0`.
2328
// Since `source_text` here may be a slice of the original source text,
2429
// using `Span` for `span.source_text(source_text)` will be out of range in some cases.
2530
let span_offset = 0;
2631

27-
let units = if parse_string_literal {
28-
let StringLiteralAst::StringLiteral { body, .. } = StringLiteralParser::new(
29-
source_text,
30-
StringLiteralParserOptions {
31-
strict_mode: false,
32-
span_offset,
33-
combine_surrogate_pair: unicode_mode,
34-
},
35-
)
36-
.parse()?;
37-
body
32+
let units = if parse_string_or_template_literal {
33+
if source_text.chars().next().is_some_and(|c| c == '`') {
34+
let TemplateLiteralAst::TemplateLiteral { body, .. } = TemplateLiteralParser::new(
35+
source_text,
36+
Options {
37+
strict_mode: false,
38+
span_offset,
39+
combine_surrogate_pair: unicode_mode,
40+
},
41+
)
42+
.parse()?;
43+
body
44+
} else {
45+
let StringLiteralAst::StringLiteral { body, .. } = StringLiteralParser::new(
46+
source_text,
47+
Options {
48+
strict_mode: false,
49+
span_offset,
50+
combine_surrogate_pair: unicode_mode,
51+
},
52+
)
53+
.parse()?;
54+
body
55+
}
3856
} else {
3957
parse_regexp_literal(source_text, span_offset, unicode_mode)
4058
};
@@ -45,7 +63,7 @@ impl<'a> Reader<'a> {
4563
index: 0,
4664
// If `parse_string_literal` is `true`, the first character is the opening quote.
4765
// We need to +1 to skip it.
48-
offset: u32::from(parse_string_literal),
66+
offset: u32::from(parse_string_or_template_literal),
4967
})
5068
}
5169

0 commit comments

Comments
 (0)