Skip to content

Commit f2c2e12

Browse files
committed
fix(regex-parser): parse simple TemplateLiterals
1 parent 6eeeb67 commit f2c2e12

File tree

18 files changed

+822
-80
lines changed

18 files changed

+822
-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: 72 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,76 @@ use oxc_span::Span;
66

77
use crate::{AstNode, context::LintContext};
88

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

4295
// RegExp()
4396
AstKind::CallExpression(expr)
4497
if expr.callee.is_global_reference_name("RegExp", ctx.semantic().scoping()) =>
4598
{
46-
// note: improvements required for strings used via identifier references
47-
// Missing or non-string arguments will be runtime errors, but are not covered by this rule.
48-
match (&expr.arguments.first(), &expr.arguments.get(1)) {
49-
(Some(Argument::StringLiteral(pattern)), Some(Argument::StringLiteral(flags))) => {
50-
let allocator = Allocator::default();
51-
if let Some(pat) = parse_regex(&allocator, pattern.span, Some(flags.span), ctx)
52-
{
53-
cb(&pat, pattern.span);
54-
}
55-
}
56-
(Some(Argument::StringLiteral(pattern)), _) => {
57-
let allocator = Allocator::default();
58-
if let Some(pat) = parse_regex(&allocator, pattern.span, None, ctx) {
59-
cb(&pat, pattern.span);
60-
}
61-
}
62-
_ => {}
63-
}
99+
run_on_arguments(expr.arguments.first(), expr.arguments.get(1), ctx, cb);
64100
}
65101
_ => {}
66102
}

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+
}
File renamed without changes.

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)]
File renamed without changes.

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)