From 51f5025c9c2a6c184ff212133a247ac0e3ce9277 Mon Sep 17 00:00:00 2001 From: DonIsaac <22823424+DonIsaac@users.noreply.github.com> Date: Sun, 21 Jul 2024 07:20:22 +0000 Subject: [PATCH] feat(linter): add fixer for unicorn/prefer-string-starts-ends-with (#4378) Part of #4179 --- .../unicorn/prefer_string_starts_ends_with.rs | 73 +++++++++++++++++-- .../prefer_string_starts_ends_with.snap | 23 ++++++ 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_string_starts_ends_with.rs b/crates/oxc_linter/src/rules/unicorn/prefer_string_starts_ends_with.rs index 900f2ee3c0002..3df52d5bb8c63 100644 --- a/crates/oxc_linter/src/rules/unicorn/prefer_string_starts_ends_with.rs +++ b/crates/oxc_linter/src/rules/unicorn/prefer_string_starts_ends_with.rs @@ -1,12 +1,17 @@ use oxc_ast::{ - ast::{Expression, MemberExpression, RegExpFlags, RegExpLiteral}, + ast::{CallExpression, Expression, MemberExpression, RegExpFlags, RegExpLiteral}, AstKind, }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; use oxc_span::{GetSpan, Span}; -use crate::{context::LintContext, rule::Rule, AstNode}; +use crate::{ + context::LintContext, + fixer::{RuleFix, RuleFixer}, + rule::Rule, + AstNode, +}; fn starts_with(span0: Span) -> OxcDiagnostic { OxcDiagnostic::warn("Prefer String#startsWith over a regex with a caret.").with_label(span0) @@ -74,15 +79,54 @@ impl Rule for PreferStringStartsEndsWith { match err_kind { ErrorKind::StartsWith => { - ctx.diagnostic(starts_with(member_expr.span())); + ctx.diagnostic_with_fix(starts_with(member_expr.span()), |fixer| { + do_fix(fixer, err_kind, call_expr, regex) + }); } ErrorKind::EndsWith => { - ctx.diagnostic(ends_with(member_expr.span())); + ctx.diagnostic_with_fix(ends_with(member_expr.span()), |fixer| { + do_fix(fixer, err_kind, call_expr, regex) + }); } } } } +fn do_fix<'a>( + fixer: RuleFixer<'_, 'a>, + err_kind: ErrorKind, + call_expr: &CallExpression<'a>, + regex: &RegExpLiteral, +) -> RuleFix<'a> { + let Some(target_span) = can_replace(call_expr) else { return fixer.noop() }; + let pattern = ®ex.regex.pattern; + let (argument, method) = match err_kind { + ErrorKind::StartsWith => (pattern.trim_start_matches('^'), "startsWith"), + ErrorKind::EndsWith => (pattern.trim_end_matches('$'), "endsWith"), + }; + let fix_text = format!(r#"{}.{}("{}")"#, fixer.source_range(target_span), method, argument); + + fixer.replace(call_expr.span, fix_text) +} +fn can_replace(call_expr: &CallExpression) -> Option { + if call_expr.arguments.len() != 1 { + return None; + } + + let arg = &call_expr.arguments[0]; + let expr = arg.as_expression()?; + match expr.without_parenthesized() { + Expression::StringLiteral(s) => Some(s.span), + Expression::TemplateLiteral(s) => Some(s.span), + Expression::Identifier(ident) => Some(ident.span), + Expression::StaticMemberExpression(m) => Some(m.span), + Expression::ComputedMemberExpression(m) => Some(m.span), + Expression::CallExpression(c) => Some(c.span), + _ => None, + } +} + +#[derive(Clone, Copy)] enum ErrorKind { StartsWith, EndsWith, @@ -211,5 +255,24 @@ fn test() { r"if (/#$/i.test(hex)) {}", ]; - Tester::new(PreferStringStartsEndsWith::NAME, pass, fail).test_and_snapshot(); + let fix = vec![ + ("/^foo/.test(x)", r#"x.startsWith("foo")"#, None), + ("/foo$/.test(x)", r#"x.endsWith("foo")"#, None), + ("/^foo/.test(x.y)", r#"x.y.startsWith("foo")"#, None), + ("/foo$/.test(x.y)", r#"x.y.endsWith("foo")"#, None), + ("/^foo/.test('x')", r#"'x'.startsWith("foo")"#, None), + ("/foo$/.test('x')", r#"'x'.endsWith("foo")"#, None), + ("/^foo/.test(`x${y}`)", r#"`x${y}`.startsWith("foo")"#, None), + ("/foo$/.test(`x${y}`)", r#"`x${y}`.endsWith("foo")"#, None), + ("/^foo/.test(String(x))", r#"String(x).startsWith("foo")"#, None), + ("/foo$/.test(String(x))", r#"String(x).endsWith("foo")"#, None), + // should not get fixed + ("/^foo/.test(new String('bar'))", "/^foo/.test(new String('bar'))", None), + ("/^foo/.test(x as string)", "/^foo/.test(x as string)", None), + ("/^foo/.test(5)", "/^foo/.test(5)", None), + ("/^foo/.test(x?.y)", "/^foo/.test(x?.y)", None), + ("/^foo/.test(x + y)", "/^foo/.test(x + y)", None), + ]; + + Tester::new(PreferStringStartsEndsWith::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/prefer_string_starts_ends_with.snap b/crates/oxc_linter/src/snapshots/prefer_string_starts_ends_with.snap index 61905332a6e6c..a6ec3b4a81243 100644 --- a/crates/oxc_linter/src/snapshots/prefer_string_starts_ends_with.snap +++ b/crates/oxc_linter/src/snapshots/prefer_string_starts_ends_with.snap @@ -6,66 +6,77 @@ source: crates/oxc_linter/src/tester.rs 1 │ /^foo/.test(bar) · ─────────── ╰──── + help: Replace `/^foo/.test(bar)` with `bar.startsWith("foo")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#endsWith over a regex with a dollar sign. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ /foo$/.test(bar) · ─────────── ╰──── + help: Replace `/foo$/.test(bar)` with `bar.endsWith("foo")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ /^!/.test(bar) · ───────── ╰──── + help: Replace `/^!/.test(bar)` with `bar.startsWith("!")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#endsWith over a regex with a dollar sign. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ /!$/.test(bar) · ───────── ╰──── + help: Replace `/!$/.test(bar)` with `bar.endsWith("!")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ /^ /.test(bar) · ───────── ╰──── + help: Replace `/^ /.test(bar)` with `bar.startsWith(" ")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#endsWith over a regex with a dollar sign. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ / $/.test(bar) · ───────── ╰──── + help: Replace `/ $/.test(bar)` with `bar.endsWith(" ")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:17] 1 │ const foo = {}; /^abc/.test(foo); · ─────────── ╰──── + help: Replace `/^abc/.test(foo)` with `foo.startsWith("abc")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:18] 1 │ const foo = 123; /^abc/.test(foo); · ─────────── ╰──── + help: Replace `/^abc/.test(foo)` with `foo.startsWith("abc")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:22] 1 │ const foo = "hello"; /^abc/.test(foo); · ─────────── ╰──── + help: Replace `/^abc/.test(foo)` with `foo.startsWith("abc")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ /^b/.test((a)) · ───────── ╰──── + help: Replace `/^b/.test((a))` with `a.startsWith("b")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ (/^b/).test((a)) · ─────────── ╰──── + help: Replace `(/^b/).test((a))` with `a.startsWith("b")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:24] @@ -84,6 +95,7 @@ source: crates/oxc_linter/src/tester.rs 1 │ /^a/.test("string") · ───────── ╰──── + help: Replace `/^a/.test("string")` with `"string".startsWith("a")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:1] @@ -150,12 +162,14 @@ source: crates/oxc_linter/src/tester.rs 1 │ /^a/.test(foo.bar) · ───────── ╰──── + help: Replace `/^a/.test(foo.bar)` with `foo.bar.startsWith("a")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ /^a/.test(foo.bar()) · ───────── ╰──── + help: Replace `/^a/.test(foo.bar())` with `foo.bar().startsWith("a")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:1] @@ -174,6 +188,7 @@ source: crates/oxc_linter/src/tester.rs 1 │ /^a/.test(`string`) · ───────── ╰──── + help: Replace `/^a/.test(`string`)` with ``string`.startsWith("a")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:1] @@ -216,45 +231,53 @@ source: crates/oxc_linter/src/tester.rs 1 │ /^a/u.test("string") · ────────── ╰──── + help: Replace `/^a/u.test("string")` with `"string".startsWith("a")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ /^a/v.test("string") · ────────── ╰──── + help: Replace `/^a/v.test("string")` with `"string".startsWith("a")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#endsWith over a regex with a dollar sign. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ /a$/.test(`${unknown}`) · ───────── ╰──── + help: Replace `/a$/.test(`${unknown}`)` with ``${unknown}`.endsWith("a")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#endsWith over a regex with a dollar sign. ╭─[prefer_string_starts_ends_with.tsx:1:1] 1 │ /a$/.test(String(unknown)) · ───────── ╰──── + help: Replace `/a$/.test(String(unknown))` with `String(unknown).endsWith("a")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#endsWith over a regex with a dollar sign. ╭─[prefer_string_starts_ends_with.tsx:1:11] 1 │ const a = /你$/.test('a'); · ────────── ╰──── + help: Replace `/你$/.test('a')` with `'a'.endsWith("你")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:11] 1 │ const a = /^你/.test('a'); · ────────── ╰──── + help: Replace `/^你/.test('a')` with `'a'.startsWith("你")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#startsWith over a regex with a caret. ╭─[prefer_string_starts_ends_with.tsx:1:5] 1 │ if (/^#/i.test(hex)) {} · ────────── ╰──── + help: Replace `/^#/i.test(hex)` with `hex.startsWith("#")`. ⚠ eslint-plugin-unicorn(prefer-string-starts-ends-with): Prefer String#endsWith over a regex with a dollar sign. ╭─[prefer_string_starts_ends_with.tsx:1:5] 1 │ if (/#$/i.test(hex)) {} · ────────── ╰──── + help: Replace `/#$/i.test(hex)` with `hex.endsWith("#")`.