mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
742 lines
28 KiB
Rust
742 lines
28 KiB
Rust
use memchr::memmem;
|
||
use oxc_ast::{ast::RegExpFlags, AstKind};
|
||
use oxc_diagnostics::OxcDiagnostic;
|
||
use oxc_macros::declare_oxc_lint;
|
||
use oxc_regular_expression::ast::{
|
||
Alternative, Character, CharacterClass, CharacterClassContents, Disjunction, Pattern, Term,
|
||
};
|
||
use oxc_semantic::NodeId;
|
||
use oxc_span::Span;
|
||
|
||
use crate::{context::LintContext, rule::Rule, AstNode};
|
||
|
||
fn no_useless_escape_diagnostic(escape_char: char, span: Span) -> OxcDiagnostic {
|
||
OxcDiagnostic::warn(format!("Unnecessary escape character {escape_char:?}")).with_label(span)
|
||
}
|
||
|
||
#[derive(Debug, Default, Clone)]
|
||
pub struct NoUselessEscape;
|
||
|
||
declare_oxc_lint!(
|
||
/// ### What it does
|
||
///
|
||
/// Disallow unnecessary escape characters
|
||
///
|
||
/// ### Why is this bad?
|
||
///
|
||
///
|
||
/// ### Example
|
||
///
|
||
/// Examples of **incorrect** code for this rule:
|
||
///
|
||
/// ```javascript
|
||
/// /*eslint no-useless-escape: "error"*/
|
||
///
|
||
/// "\'";
|
||
/// '\"';
|
||
/// "\#";
|
||
/// "\e";
|
||
/// `\"`;
|
||
/// `\"${foo}\"`;
|
||
/// `\#{foo}`;
|
||
/// /\!/;
|
||
/// /\@/;
|
||
/// /[\[]/;
|
||
/// /[a-z\-]/;
|
||
/// ```
|
||
///
|
||
/// Examples of **correct** code for this rule:
|
||
///
|
||
/// ```javascript
|
||
/// /*eslint no-useless-escape: "error"*/
|
||
///
|
||
/// "\"";
|
||
/// '\'';
|
||
/// "\x12";
|
||
/// "\u00a9";
|
||
/// "\371";
|
||
/// "xs\u2111";
|
||
/// `\``;
|
||
/// `\${${foo}}`;
|
||
/// `$\{${foo}}`;
|
||
/// /\\/g;
|
||
/// /\t/g;
|
||
/// /\w\$\*\^\./;
|
||
/// /[[]/;
|
||
/// /[\]]/;
|
||
/// /[a-z-]/;
|
||
/// ```
|
||
NoUselessEscape,
|
||
correctness,
|
||
fix
|
||
);
|
||
|
||
impl Rule for NoUselessEscape {
|
||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||
match node.kind() {
|
||
AstKind::RegExpLiteral(literal)
|
||
if literal.regex.pattern.len() + literal.regex.flags.iter().count()
|
||
!= literal.span.size() as usize =>
|
||
{
|
||
if let Some(pattern) = literal.regex.pattern.as_pattern() {
|
||
let unicode_sets = literal.regex.flags.contains(RegExpFlags::V);
|
||
let useless_escape_spans = check_pattern(ctx, pattern, unicode_sets);
|
||
for span in useless_escape_spans {
|
||
let c = span.source_text(ctx.source_text()).chars().last().unwrap();
|
||
ctx.diagnostic_with_fix(no_useless_escape_diagnostic(c, span), |fixer| {
|
||
fixer.replace(span, c.to_string())
|
||
});
|
||
}
|
||
}
|
||
}
|
||
AstKind::StringLiteral(literal) => check(
|
||
ctx,
|
||
node.id(),
|
||
literal.span.start,
|
||
&check_string(literal.span.source_text(ctx.source_text())),
|
||
),
|
||
AstKind::TemplateLiteral(literal) if !matches!(ctx.nodes().parent_kind(node.id()), Some(AstKind::TaggedTemplateExpression(expr)) if expr.quasi.span == literal.span) => {
|
||
for template_element in &literal.quasis {
|
||
check(
|
||
ctx,
|
||
node.id(),
|
||
template_element.span.start - 1,
|
||
&check_template(template_element.span.source_text(ctx.source_text())),
|
||
);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn is_within_jsx_attribute_item(id: NodeId, ctx: &LintContext) -> bool {
|
||
if matches!(ctx.nodes().parent_kind(id), Some(AstKind::JSXAttributeItem(_))) {
|
||
return true;
|
||
}
|
||
false
|
||
}
|
||
|
||
#[allow(clippy::cast_possible_truncation)]
|
||
fn check(ctx: &LintContext<'_>, node_id: NodeId, start: u32, offsets: &[usize]) {
|
||
let source_text = ctx.source_text();
|
||
for offset in offsets {
|
||
let offset = start as usize + offset;
|
||
let c = source_text[offset..].chars().next().unwrap();
|
||
let offset = offset as u32;
|
||
let len = c.len_utf8() as u32;
|
||
|
||
if !is_within_jsx_attribute_item(node_id, ctx) {
|
||
let span = Span::new(offset - 1, offset + len);
|
||
ctx.diagnostic_with_fix(no_useless_escape_diagnostic(c, span), |fixer| {
|
||
fixer.replace(span, c.to_string())
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
const REGEX_GENERAL_ESCAPES: &str = "\\bcdDfnpPrsStvwWxu0123456789]";
|
||
const REGEX_NON_CHARCLASS_ESCAPES: &str = "\\bcdDfnpPrsStvwWxu0123456789]^/.$*+?[{}|()Bk";
|
||
const REGEX_CLASSSET_CHARACTER_ESCAPES: &str = "\\bcdDfnpPrsStvwWxu0123456789]q/[{}|()-";
|
||
const REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR: &str = "!#$%&*+,.:;<=>?@^`~";
|
||
|
||
fn check_pattern(ctx: &LintContext, pattern: &Pattern, unicode_sets: bool) -> Vec<Span> {
|
||
let mut spans = vec![];
|
||
|
||
visit_terms(pattern, &mut |term, stack| match term {
|
||
Term::CharacterClass(class) => {
|
||
check_character_class(ctx, class, unicode_sets, &mut spans);
|
||
}
|
||
Term::Character(ch) => {
|
||
let character_class = stack.iter().find_map(|visit| match visit {
|
||
Visit::Term(Term::CharacterClass(class)) => Some(class),
|
||
Visit::Term(_) => None,
|
||
});
|
||
if let Some(span) = check_character(ctx, ch, character_class, unicode_sets) {
|
||
spans.push(span);
|
||
}
|
||
}
|
||
_ => (),
|
||
});
|
||
|
||
spans
|
||
}
|
||
|
||
fn check_character_class(
|
||
ctx: &LintContext,
|
||
character_class: &oxc_allocator::Box<CharacterClass>,
|
||
unicode_sets: bool,
|
||
spans: &mut Vec<Span>,
|
||
) {
|
||
for term in &character_class.body {
|
||
match term {
|
||
CharacterClassContents::Character(ch) => {
|
||
if let Some(span) = check_character(ctx, ch, Some(character_class), unicode_sets) {
|
||
spans.push(span);
|
||
}
|
||
}
|
||
CharacterClassContents::NestedCharacterClass(nested_class) => {
|
||
check_character_class(ctx, nested_class, unicode_sets, spans);
|
||
}
|
||
_ => (),
|
||
}
|
||
}
|
||
}
|
||
|
||
fn check_character(
|
||
ctx: &LintContext,
|
||
character: &Character,
|
||
character_class: Option<&oxc_allocator::Box<CharacterClass>>,
|
||
unicode_sets: bool,
|
||
) -> Option<Span> {
|
||
let char_text = character.span.source_text(ctx.source_text());
|
||
let is_escaped = char_text.starts_with('\\');
|
||
if !is_escaped {
|
||
return None;
|
||
}
|
||
let span = character.span;
|
||
let escape_char = char_text.chars().nth(1).unwrap();
|
||
let escapes = if character_class.is_some() {
|
||
if unicode_sets {
|
||
REGEX_CLASSSET_CHARACTER_ESCAPES
|
||
} else {
|
||
REGEX_GENERAL_ESCAPES
|
||
}
|
||
} else {
|
||
REGEX_NON_CHARCLASS_ESCAPES
|
||
};
|
||
if escapes.contains(escape_char) {
|
||
return None;
|
||
}
|
||
|
||
if let Some(class) = character_class {
|
||
if escape_char == '^' {
|
||
/* The '^' character is also a special case; it must always be escaped outside of character classes, but
|
||
* it only needs to be escaped in character classes if it's at the beginning of the character class. To
|
||
* account for this, consider it to be a valid escape character outside of character classes, and filter
|
||
* out '^' characters that appear at the start of a character class.
|
||
* (From ESLint source: https://github.com/eslint/eslint/blob/main/lib/rules/no-useless-escape.js)
|
||
*/
|
||
if class.span.start + 1 == span.start {
|
||
return None;
|
||
}
|
||
}
|
||
if unicode_sets {
|
||
if REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR.contains(escape_char) {
|
||
if let Some(prev_char) = ctx.source_text().chars().nth(span.end as usize) {
|
||
// Escaping is valid when it is a reserved double punctuator
|
||
if prev_char == escape_char {
|
||
return None;
|
||
}
|
||
}
|
||
if let Some(prev_prev_char) = ctx.source_text().chars().nth(span.start as usize - 1)
|
||
{
|
||
if prev_prev_char == escape_char {
|
||
if escape_char != '^' {
|
||
return None;
|
||
}
|
||
|
||
// Escaping caret is unnecessary if the previous character is a `negate` caret(`^`).
|
||
if !class.negative {
|
||
return None;
|
||
}
|
||
|
||
let caret_index = class.span.start + 1;
|
||
if caret_index < span.start - 1 {
|
||
return None;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else if escape_char == '-' {
|
||
/* The '-' character is a special case, because it's only valid to escape it if it's in a character
|
||
* class, and is not at either edge of the character class. To account for this, don't consider '-'
|
||
* characters to be valid in general, and filter out '-' characters that appear in the middle of a
|
||
* character class.
|
||
* (From ESLint source: https://github.com/eslint/eslint/blob/main/lib/rules/no-useless-escape.js)
|
||
*/
|
||
if class.span.start + 1 != span.start && span.end != class.span.end - 1 {
|
||
return None;
|
||
}
|
||
}
|
||
}
|
||
|
||
Some(span)
|
||
}
|
||
|
||
const VALID_STRING_ESCAPES: &str = "\\nrvtbfux\n\r\u{2028}\u{2029}";
|
||
|
||
fn check_string(string: &str) -> Vec<usize> {
|
||
if string.len() <= 1 {
|
||
return vec![];
|
||
}
|
||
|
||
let quote_char = string.chars().next().unwrap();
|
||
let bytes = &string[1..string.len() - 1].as_bytes();
|
||
let escapes = memmem::find_iter(bytes, "\\").collect::<Vec<_>>();
|
||
|
||
if escapes.is_empty() {
|
||
return vec![];
|
||
}
|
||
|
||
let mut offsets = vec![];
|
||
let mut prev_offset = None; // for checking double escape `\\`
|
||
for offset in escapes {
|
||
// Safety:
|
||
// The offset comes from a utf8 checked string
|
||
|
||
let s = unsafe { std::str::from_utf8_unchecked(&bytes[offset..]) };
|
||
if let Some(c) = s.chars().nth(1) {
|
||
if !(c == quote_char
|
||
|| (offset > 0 && prev_offset == Some(offset - 1))
|
||
|| c.is_ascii_digit()
|
||
|| VALID_STRING_ESCAPES.contains(c))
|
||
{
|
||
// +1 for skipping the first string quote `"`
|
||
// +1 for skipping the escape char `\\`
|
||
offsets.push(offset + 2);
|
||
}
|
||
}
|
||
prev_offset.replace(offset);
|
||
}
|
||
|
||
offsets
|
||
}
|
||
|
||
fn check_template(string: &str) -> Vec<usize> {
|
||
if string.len() <= 1 {
|
||
return vec![];
|
||
}
|
||
|
||
let mut offsets = vec![];
|
||
let mut in_escape = false;
|
||
let mut prev_non_escape_char = '`';
|
||
let mut byte_offset = 1;
|
||
|
||
let mut chars = string.chars().peekable();
|
||
|
||
while let Some(c) = chars.next() {
|
||
byte_offset += c.len_utf8();
|
||
|
||
if in_escape {
|
||
in_escape = false;
|
||
match c {
|
||
c if c.is_ascii_digit() || c == '`' => { /* noop */ }
|
||
'{' => {
|
||
if prev_non_escape_char != '$' {
|
||
offsets.push(byte_offset - c.len_utf8());
|
||
}
|
||
}
|
||
'$' => {
|
||
if chars.peek().is_some_and(|c| *c != '{') {
|
||
offsets.push(byte_offset - c.len_utf8());
|
||
}
|
||
}
|
||
c if !VALID_STRING_ESCAPES.contains(c) => {
|
||
offsets.push(byte_offset - c.len_utf8());
|
||
}
|
||
_ => {}
|
||
}
|
||
prev_non_escape_char = c;
|
||
} else if c == '\\' {
|
||
in_escape = true;
|
||
} else {
|
||
prev_non_escape_char = c;
|
||
}
|
||
}
|
||
|
||
offsets
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy)]
|
||
enum Visit<'a> {
|
||
Term(&'a Term<'a>),
|
||
}
|
||
|
||
// TODO: Replace with proper visitor pattern for the regex AST when available
|
||
/// Calls the given closure on every [`Term`] in the [`Pattern`].
|
||
fn visit_terms<'a, F: FnMut(&'a Term<'a>, &Vec<Visit<'a>>)>(pattern: &'a Pattern, f: &mut F) {
|
||
// initialize visit stack with enough initial capacity so we will not need to reallocate
|
||
// in general (most regex patterns will probably not be this many items deep)
|
||
let mut stack: Vec<Visit> = Vec::with_capacity(16);
|
||
visit_terms_disjunction(&pattern.body, f, &mut stack);
|
||
}
|
||
|
||
/// Calls the given closure on every [`Term`] in the [`Disjunction`].
|
||
fn visit_terms_disjunction<'a, F: FnMut(&'a Term<'a>, &Vec<Visit<'a>>)>(
|
||
disjunction: &'a Disjunction,
|
||
f: &mut F,
|
||
stack: &mut Vec<Visit<'a>>,
|
||
) {
|
||
for alternative in &disjunction.body {
|
||
visit_terms_alternative(alternative, f, stack);
|
||
}
|
||
}
|
||
|
||
/// Calls the given closure on every [`Term`] in the [`Alternative`].
|
||
fn visit_terms_alternative<'a, F: FnMut(&'a Term<'a>, &Vec<Visit<'a>>)>(
|
||
alternative: &'a Alternative,
|
||
f: &mut F,
|
||
stack: &mut Vec<Visit<'a>>,
|
||
) {
|
||
for term in &alternative.body {
|
||
match term {
|
||
Term::LookAroundAssertion(lookaround) => {
|
||
stack.push(Visit::Term(term));
|
||
f(term, stack);
|
||
visit_terms_disjunction(&lookaround.body, f, stack);
|
||
stack.pop();
|
||
}
|
||
Term::Quantifier(quant) => {
|
||
stack.push(Visit::Term(term));
|
||
f(term, stack);
|
||
f(&quant.body, stack);
|
||
stack.pop();
|
||
}
|
||
Term::CapturingGroup(group) => {
|
||
stack.push(Visit::Term(term));
|
||
f(term, stack);
|
||
visit_terms_disjunction(&group.body, f, stack);
|
||
stack.pop();
|
||
}
|
||
Term::IgnoreGroup(group) => {
|
||
stack.push(Visit::Term(term));
|
||
f(term, stack);
|
||
visit_terms_disjunction(&group.body, f, stack);
|
||
stack.pop();
|
||
}
|
||
_ => f(term, stack),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test() {
|
||
use crate::tester::Tester;
|
||
|
||
let pass = vec![
|
||
"var foo = /\\./",
|
||
"var foo = /\\//g",
|
||
"var foo = /\"\"/",
|
||
"var foo = /''/",
|
||
"var foo = /([A-Z])\\t+/g",
|
||
"var foo = /([A-Z])\\n+/g",
|
||
"var foo = /([A-Z])\\v+/g",
|
||
"var foo = /\\D/",
|
||
"var foo = /\\W/",
|
||
"var foo = /\\w/",
|
||
"var foo = /\\B/",
|
||
"var foo = /\\\\/g",
|
||
"var foo = /\\w\\$\\*\\./",
|
||
"var foo = /\\^\\+\\./",
|
||
"var foo = /\\|\\}\\{\\./",
|
||
"var foo = /]\\[\\(\\)\\//",
|
||
"var foo = \"\\x123\"",
|
||
"var foo = \"\\u00a9\"",
|
||
"var foo = \"\\377\"",
|
||
"var foo = \"\\\"\"",
|
||
"var foo = \"xs\\u2111\"",
|
||
"var foo = \"foo \\\\ bar\";",
|
||
"var foo = \"\\t\";",
|
||
"var foo = \"foo \\b bar\";",
|
||
"var foo = '\\n';",
|
||
"var foo = 'foo \\r bar';",
|
||
"var foo = '\\v';",
|
||
"var foo = '\\f';",
|
||
"var foo = '\\\n';",
|
||
"var foo = '\\\r\n';",
|
||
"<foo attr=\"\\d\"/>",
|
||
"<div> Testing: \\ </div>",
|
||
"<div> Testing: \ </div>",
|
||
"<foo attr='\\d'></foo>",
|
||
"<> Testing: \\ </>",
|
||
"<> Testing: \ </>",
|
||
"var foo = `\\x123`",
|
||
"var foo = `\\u00a9`",
|
||
"var foo = `xs\\u2111`",
|
||
"var foo = `foo \\\\ bar`;",
|
||
"var foo = `\\t`;",
|
||
"var foo = `foo \\b bar`;",
|
||
"var foo = `\\n`;",
|
||
"var foo = `foo \\r bar`;",
|
||
"var foo = `\\v`;",
|
||
"var foo = `\\f`;",
|
||
"var foo = `\\\n`;",
|
||
"var foo = `\\\r\n`;",
|
||
"var foo = `${foo} \\x123`",
|
||
"var foo = `${foo} \\u00a9`",
|
||
"var foo = `${foo} xs\\u2111`",
|
||
"var foo = `${foo} \\\\ ${bar}`;",
|
||
"var foo = `${foo} \\b ${bar}`;",
|
||
"var foo = `${foo}\\t`;",
|
||
"var foo = `${foo}\\n`;",
|
||
"var foo = `${foo}\\r`;",
|
||
"var foo = `${foo}\\v`;",
|
||
"var foo = `${foo}\\f`;",
|
||
"var foo = `${foo}\\\n`;",
|
||
"var foo = `${foo}\\\r\n`;",
|
||
"var foo = `\\``",
|
||
"var foo = `\\`${foo}\\``",
|
||
"var foo = `\\${{${foo}`;",
|
||
"var foo = `$\\{{${foo}`;",
|
||
"var foo = String.raw`\\.`",
|
||
"var foo = myFunc`\\.`",
|
||
"var foo = /[\\d]/",
|
||
"var foo = /[a\\-b]/",
|
||
"var foo = /foo\\?/",
|
||
"var foo = /example\\.com/",
|
||
"var foo = /foo\\|bar/",
|
||
"var foo = /\\^bar/",
|
||
"var foo = /[\\^bar]/",
|
||
"var foo = /\\(bar\\)/",
|
||
r"var foo = /[[\]]/",
|
||
"var foo = /[[]\\./",
|
||
"var foo = /[\\]\\]]/",
|
||
"var foo = /\\[abc]/",
|
||
"var foo = /\\[foo\\.bar]/",
|
||
"var foo = /vi/m",
|
||
"var foo = /\\B/",
|
||
"var foo = /\\0/",
|
||
"var foo = /\\1/",
|
||
"var foo = /(a)\\1/",
|
||
"var foo = /(a)\\12/",
|
||
"var foo = /[\\0]/",
|
||
"var foo = 'foo \\
bar'",
|
||
"var foo = 'foo \\
bar'",
|
||
r"/]/",
|
||
r"/\]/",
|
||
r"/\]/u",
|
||
"var foo = /foo\\]/",
|
||
"var foo = /[[]\\]/",
|
||
"var foo = /\\[foo\\.bar\\]/",
|
||
// ES2018
|
||
"var foo = /(?<a>)\\k<a>/",
|
||
"var foo = /(\\\\?<a>)/",
|
||
"var foo = /\\p{ASCII}/u",
|
||
"var foo = /\\P{ASCII}/u",
|
||
"var foo = /[\\p{ASCII}]/u",
|
||
"var foo = /[\\P{ASCII}]/u",
|
||
"`${/\\s+/g}`",
|
||
// Carets
|
||
"/[^^]/u", // { "ecmaVersion": 2015 },
|
||
// ES2024
|
||
r"/[\q{abc}]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\(]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\)]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\{]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\]]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\}]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\/]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\-]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\|]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\$$]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\&&]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\!!]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\##]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\%%]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\**]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\++]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\,,]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\..]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\::]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\;;]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\<<]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\==]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\>>]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\??]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\@@]/v", // { "ecmaVersion": 2024 },
|
||
"/[\\``]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\~~]/v", // { "ecmaVersion": 2024 },
|
||
r"/[^\^^]/v", // { "ecmaVersion": 2024 },
|
||
r"/[_\^^]/v", // { "ecmaVersion": 2024 },
|
||
r"/[$\$]/v", // { "ecmaVersion": 2024 },
|
||
r"/[&\&]/v", // { "ecmaVersion": 2024 },
|
||
r"/[!\!]/v", // { "ecmaVersion": 2024 },
|
||
r"/[#\#]/v", // { "ecmaVersion": 2024 },
|
||
r"/[%\%]/v", // { "ecmaVersion": 2024 },
|
||
r"/[*\*]/v", // { "ecmaVersion": 2024 },
|
||
r"/[+\+]/v", // { "ecmaVersion": 2024 },
|
||
r"/[,\,]/v", // { "ecmaVersion": 2024 },
|
||
r"/[.\.]/v", // { "ecmaVersion": 2024 },
|
||
r"/[:\:]/v", // { "ecmaVersion": 2024 },
|
||
r"/[;\;]/v", // { "ecmaVersion": 2024 },
|
||
r"/[<\<]/v", // { "ecmaVersion": 2024 },
|
||
r"/[=\=]/v", // { "ecmaVersion": 2024 },
|
||
r"/[>\>]/v", // { "ecmaVersion": 2024 },
|
||
r"/[?\?]/v", // { "ecmaVersion": 2024 },
|
||
r"/[@\@]/v", // { "ecmaVersion": 2024 },
|
||
"/[`\\`]/v", // { "ecmaVersion": 2024 },
|
||
r"/[~\~]/v", // { "ecmaVersion": 2024 },
|
||
r"/[^^\^]/v", // { "ecmaVersion": 2024 },
|
||
r"/[_^\^]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\&&&\&]/v", // { "ecmaVersion": 2024 },
|
||
r"/[[\-]\-]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\^]/v", // { "ecmaVersion": 2024 }
|
||
r"/[\s\-(]/", // https://github.com/oxc-project/oxc/issues/5227
|
||
];
|
||
|
||
let fail = vec![
|
||
"var foo = /\\#/;",
|
||
"var foo = /\\;/;",
|
||
"var foo = \"\\'\";",
|
||
"var foo = \"\\#/\";",
|
||
"var foo = \"\\a\"",
|
||
"var foo = \"\\B\";",
|
||
"var foo = \"\\@\";",
|
||
"var foo = \"foo \\a bar\";",
|
||
"var foo = '\\\"';",
|
||
"var foo = '\\#';",
|
||
"var foo = '\\$';",
|
||
"var foo = '\\p';",
|
||
"var foo = '\\p\\a\\@';",
|
||
"<foo attr={\"\\d\"}/>",
|
||
"var foo = '\\`';",
|
||
"var foo = `\\\"`;",
|
||
"var foo = `\\'`;",
|
||
"var foo = `\\#`;",
|
||
"var foo = '\\`foo\\`';",
|
||
"var foo = `\\\"${foo}\\\"`;",
|
||
"var foo = `\\'${foo}\\'`;",
|
||
"var foo = `\\#${foo}`;",
|
||
"let foo = '\\ ';",
|
||
"let foo = /\\ /;",
|
||
"var foo = `\\$\\{{${foo}`;",
|
||
"var foo = `\\$a${foo}`;",
|
||
"var foo = `a\\{{${foo}`;",
|
||
"var foo = /[ab\\-]/",
|
||
"var foo = /[\\-ab]/",
|
||
"var foo = /[ab\\?]/",
|
||
"var foo = /[ab\\.]/",
|
||
"var foo = /[a\\|b]/",
|
||
"var foo = /\\-/",
|
||
"var foo = /[\\-]/",
|
||
"var foo = /[ab\\$]/",
|
||
"var foo = /[\\(paren]/",
|
||
"var foo = /[\\[]/",
|
||
"var foo = /[\\/]/",
|
||
"var foo = /[\\B]/",
|
||
"var foo = /[a][\\-b]/",
|
||
"var foo = /\\-[]/",
|
||
"var foo = /[a\\^]/",
|
||
"`multiline template\nliteral with useless \\escape`",
|
||
"`multiline template\r\nliteral with useless \\escape`",
|
||
"`template literal with line continuation \\\nand useless \\escape`",
|
||
"`template literal with line continuation \\\r\nand useless \\escape`",
|
||
"`template literal with mixed linebreaks \r\r\n\n\\and useless escape`",
|
||
"`template literal with mixed linebreaks in line continuations \\\n\\\r\\\r\n\\and useless escape`",
|
||
"`\\a```",
|
||
r"var foo = /\(([^\)\(]+)\)$|\(([^\)\)]+)\)$/;",
|
||
r#"var stringLiteralWithNextLine = "line 1\
line 2";"#,
|
||
r"var stringLiteralWithNextLine = `line 1\
line 2`;",
|
||
r#""use\ strict";"#,
|
||
// spellchecker:off
|
||
r#"({ foo() { "foo"; "bar"; "ba\z" } })"#, // { "ecmaVersion": 6 }
|
||
// spellchecker:on
|
||
// Carets
|
||
r"/[^\^]/",
|
||
r"/[^\^]/u", // { "ecmaVersion": 2015 },
|
||
// ES2024
|
||
r"/[\$]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\&\&]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\!\!]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\#\#]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\%\%]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\*\*]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\+\+]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\,\,]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\.\.]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\:\:]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\;\;]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\<\<]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\=\=]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\>\>]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\?\?]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\@\@]/v", // { "ecmaVersion": 2024 },
|
||
"/[\\`\\`]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\~\~]/v", // { "ecmaVersion": 2024 },
|
||
r"/[^\^\^]/v", // { "ecmaVersion": 2024 },
|
||
r"/[_\^\^]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\&\&&\&]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\p{ASCII}--\.]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\p{ASCII}&&\.]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\.--[.&]]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\.&&[.&]]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\.--\.--\.]/v", // { "ecmaVersion": 2024 },
|
||
r"/[\.&&\.&&\.]/v", // { "ecmaVersion": 2024 },
|
||
r"/[[\.&]--[\.&]]/v", // { "ecmaVersion": 2024 },
|
||
r"/[[\.&]&&[\.&]]/v", // { "ecmaVersion": 2024 }
|
||
];
|
||
|
||
let fix = vec![
|
||
("var foo = /\\#/;", "var foo = /#/;", None),
|
||
("var foo = /\\;/;", "var foo = /;/;", None),
|
||
("var foo = \"\\'\";", "var foo = \"'\";", None),
|
||
("var foo = \"\\#/\";", "var foo = \"#/\";", None),
|
||
("var foo = \"\\a\"", "var foo = \"a\"", None),
|
||
("var foo = \"\\B\";", "var foo = \"B\";", None),
|
||
("var foo = \"\\@\";", "var foo = \"@\";", None),
|
||
("var foo = \"foo \\a bar\";", "var foo = \"foo a bar\";", None),
|
||
("var foo = '\\\"';", "var foo = '\"';", None),
|
||
("var foo = '\\#';", "var foo = '#';", None),
|
||
("var foo = '\\$';", "var foo = '$';", None),
|
||
("var foo = '\\p';", "var foo = 'p';", None),
|
||
("var foo = '\\p\\a\\@';", "var foo = 'pa@';", None),
|
||
("<foo attr={\"\\d\"}/>", "<foo attr={\"d\"}/>", None),
|
||
("var foo = '\\`';", "var foo = '`';", None),
|
||
("var foo = `\\\"`;", "var foo = `\"`;", None),
|
||
("var foo = `\\'`;", "var foo = `'`;", None),
|
||
("var foo = `\\#`;", "var foo = `#`;", None),
|
||
("var foo = '\\`foo\\`';", "var foo = '`foo`';", None),
|
||
("var foo = `\\\"${foo}\\\"`;", "var foo = `\"${foo}\"`;", None),
|
||
("var foo = `\\'${foo}\\'`;", "var foo = `'${foo}'`;", None),
|
||
("var foo = `\\#${foo}`;", "var foo = `#${foo}`;", None),
|
||
("let foo = '\\ ';", "let foo = ' ';", None),
|
||
("let foo = /\\ /;", "let foo = / /;", None),
|
||
("var foo = `\\$\\{{${foo}`;", "var foo = `$\\{{${foo}`;", None),
|
||
(r#""use\ strict";"#, r#""use strict";"#, None),
|
||
// spellchecker:off
|
||
(r#"({ foo() { "foo"; "bar"; "ba\z" } })"#, r#"({ foo() { "foo"; "bar"; "baz" } })"#, None), // { "ecmaVersion": 6 }
|
||
// spellchecker:on
|
||
// Carets
|
||
(r"/[^\^]/", r"/[^^]/", None),
|
||
(r"/[^\^]/u", r"/[^^]/u", None), // { "ecmaVersion": 2015 },
|
||
// ES2024
|
||
(r"/[\$]/v", r"/[$]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\&\&]/v", r"/[&\&]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\!\!]/v", r"/[!\!]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\#\#]/v", r"/[#\#]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\%\%]/v", r"/[%\%]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\*\*]/v", r"/[*\*]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\+\+]/v", r"/[+\+]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\,\,]/v", r"/[,\,]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\.\.]/v", r"/[.\.]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\:\:]/v", r"/[:\:]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\;\;]/v", r"/[;\;]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\<\<]/v", r"/[<\<]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\=\=]/v", r"/[=\=]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\>\>]/v", r"/[>\>]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\?\?]/v", r"/[?\?]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\@\@]/v", r"/[@\@]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\`\`]/v", r"/[`\`]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\~\~]/v", r"/[~\~]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[^\^\^]/v", r"/[^^\^]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[_\^\^]/v", r"/[_^\^]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\&\&&\&]/v", r"/[&\&&\&]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\p{ASCII}--\.]/v", r"/[\p{ASCII}--.]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\p{ASCII}&&\.]/v", r"/[\p{ASCII}&&.]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\.--[.&]]/v", r"/[.--[.&]]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\.&&[.&]]/v", r"/[.&&[.&]]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\.--\.--\.]/v", r"/[.--.--.]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[\.&&\.&&\.]/v", r"/[.&&.&&.]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[[\.&]--[\.&]]/v", r"/[[.&]--[.&]]/v", None), // { "ecmaVersion": 2024 },
|
||
(r"/[[\.&]&&[\.&]]/v", r"/[[.&]&&[.&]]/v", None), // { "ecmaVersion": 2024 }
|
||
(
|
||
// https://github.com/oxc-project/oxc/issues/5227
|
||
r"const regex = /(https?:\/\/github\.com\/(([^\s]+)\/([^\s]+))\/([^\s]+\/)?(issues|pull)\/([0-9]+))|(([^\s]+)\/([^\s]+))?#([1-9][0-9]*)($|[\s\:\;\-\(\=])/;",
|
||
r"const regex = /(https?:\/\/github\.com\/(([^\s]+)\/([^\s]+))\/([^\s]+\/)?(issues|pull)\/([0-9]+))|(([^\s]+)\/([^\s]+))?#([1-9][0-9]*)($|[\s:;\-(=])/;",
|
||
None,
|
||
),
|
||
];
|
||
|
||
Tester::new(NoUselessEscape::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
|
||
}
|