diff --git a/crates/oxc_linter/src/rules/jest/prefer_spy_on.rs b/crates/oxc_linter/src/rules/jest/prefer_spy_on.rs index 3431452e4f753..0a09b5d2dd136 100644 --- a/crates/oxc_linter/src/rules/jest/prefer_spy_on.rs +++ b/crates/oxc_linter/src/rules/jest/prefer_spy_on.rs @@ -14,13 +14,13 @@ use crate::{ context::LintContext, fixer::RuleFixer, rule::Rule, - utils::{PossibleJestNode, get_node_name, parse_general_jest_fn_call}, + utils::{ + KnownMemberExpressionProperty, PossibleJestNode, get_node_name, parse_general_jest_fn_call, + }, }; fn use_jest_spy_on(span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Suggest using `jest.spyOn()`.") - .with_help("Use jest.spyOn() instead") - .with_label(span) + OxcDiagnostic::warn("Suggest using `jest.spyOn()` or `vi.spyOn()`.").with_label(span) } #[derive(Debug, Default, Clone)] @@ -61,6 +61,17 @@ declare_oxc_lint!( /// jest.spyOn(Date, 'now'); /// jest.spyOn(Date, 'now').mockImplementation(() => 10); /// ``` + /// + /// This rule is compatible with [eslint-plugin-vitest](https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-spy-on.md), + /// to use it, add the following configuration to your `.oxlintrc.json`: + /// + /// ```json + /// { + /// "rules": { + /// "vitest/prefer-spy-on": "error" + /// } + /// } + /// ``` PreferSpyOn, jest, style, @@ -73,22 +84,20 @@ impl Rule for PreferSpyOn { return; }; - let left = &assign_expr.left; - let right = &assign_expr.right; - - let Some(left_assign) = left + let Some(left_assign) = &assign_expr + .left .as_simple_assignment_target() .and_then(SimpleAssignmentTarget::as_member_expression) else { return; }; - match right { + match &assign_expr.right { Expression::CallExpression(call_expr) => { Self::check_and_fix(assign_expr, call_expr, left_assign, node, ctx); } _ => { - if let Some(mem_expr) = right.as_member_expression() { + if let Some(mem_expr) = assign_expr.right.as_member_expression() { let Expression::CallExpression(call_expr) = mem_expr.object() else { return; }; @@ -112,11 +121,18 @@ impl PreferSpyOn { else { return; }; + let Some(first_fn_member) = jest_fn_call.members.first() else { return; }; - if first_fn_member.name().unwrap() != "fn" { + let Some(first_fn_member_name) = + jest_fn_call.members.first().and_then(KnownMemberExpressionProperty::name) + else { + return; + }; + + if first_fn_member_name != "fn" { return; } @@ -150,8 +166,10 @@ impl PreferSpyOn { has_mock_implementation: bool, fixer: RuleFixer<'_, 'a>, ) -> String { + let (framework_spy, arguments) = Self::get_test_fn_call(call_expr); + let mut formatter = fixer.codegen(); - formatter.print_str("jest.spyOn("); + formatter.print_str(framework_spy); match left_assign { MemberExpression::ComputedMemberExpression(cmp_mem_expr) => { @@ -178,7 +196,7 @@ impl PreferSpyOn { formatter.print_str(".mockImplementation("); - if let Some(expr) = Self::get_jest_fn_call(call_expr) { + if let Some(expr) = arguments { formatter.print_expression(expr); } @@ -186,23 +204,30 @@ impl PreferSpyOn { formatter.into_source_text() } - fn get_jest_fn_call<'a>(call_expr: &'a CallExpression<'a>) -> Option<&'a Expression<'a>> { - let is_jest_fn = get_node_name(&call_expr.callee) == "jest.fn"; + fn get_test_fn_call<'a>( + call_expr: &'a CallExpression<'a>, + ) -> (&'a str, Option<&'a Expression<'a>>) { + let node_name = get_node_name(&call_expr.callee); + let is_test_fn = node_name == "jest.fn" || node_name == "vi.fn"; - if is_jest_fn { - return call_expr.arguments.first().and_then(Argument::as_expression); + if is_test_fn { + let framework_spy = match node_name.as_str() { + "vi.fn" => "vi.spyOn(", + _ => "jest.spyOn(", + }; + return (framework_spy, call_expr.arguments.first().and_then(Argument::as_expression)); } match &call_expr.callee { expr if expr.is_member_expression() => { let mem_expr = expr.to_member_expression(); if let Some(call_expr) = Self::find_mem_expr(mem_expr) { - return Self::get_jest_fn_call(call_expr); + return Self::get_test_fn_call(call_expr); } - None + ("", None) } - Expression::CallExpression(call_expr) => Self::get_jest_fn_call(call_expr), - _ => None, + Expression::CallExpression(call_expr) => Self::get_test_fn_call(call_expr), + _ => ("", None), } } @@ -225,7 +250,7 @@ impl PreferSpyOn { fn tests() { use crate::tester::Tester; - let pass = vec![ + let mut pass = vec![ ("Date.now = () => 10", None), ("window.fetch = jest.fn", None), ("Date.now = fn()", None), @@ -237,7 +262,7 @@ fn tests() { ("window[`${name}`] = jest[`fn${expression}`]()", None), ]; - let fail = vec![ + let mut fail = vec![ ("obj.a = jest.fn(); const test = 10;", None), ("Date['now'] = jest['fn']()", None), ("window[`${name}`] = jest[`fn`]()", None), @@ -259,7 +284,7 @@ fn tests() { ), ]; - let fix = vec![ + let mut fix = vec![ ( "obj.a = jest.fn(); const test = 10;", "jest.spyOn(obj, 'a').mockImplementation(); const test = 10;", @@ -310,8 +335,94 @@ fn tests() { ), ]; + let vitest_pass = vec![ + ("Date.now = () => 10", None), + ("window.fetch = vi.fn", None), + ("Date.now = fn()", None), + ("obj.mock = vi.something()", None), + ("const mock = vi.fn()", None), + ("mock = vi.fn()", None), + ("const mockObj = { mock: vi.fn() }", None), + ("mockObj = { mock: vi.fn() }", None), + ("window[`${name}`] = vi[`fn${expression}`]()", None), + ]; + + let vitest_fail = vec![ + ("obj.a = vi.fn(); const test = 10;", None), + ("Date['now'] = vi['fn']()", None), + ("window[`${name}`] = vi[`fn`]()", None), + ("obj['prop' + 1] = vi['fn']()", None), + ("obj.one.two = vi.fn(); const test = 10;", None), + ("obj.a = vi.fn(() => 10,)", None), // { "parserOptions": { "ecmaVersion": 2017 } } + ( + "obj.a.b = vi.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();", + None, + ), + ("window.fetch = vi.fn(() => ({})).one.two().three().four", None), + ("foo[bar] = vi.fn().mockReturnValue(undefined)", None), + ( + " + foo.bar = vi.fn().mockImplementation(baz => baz) + foo.bar = vi.fn(a => b).mockImplementation(baz => baz) + ", + None, + ), + ]; + + let vitest_fix = vec![ + ( + "obj.a = vi.fn(); const test = 10;", + "vi.spyOn(obj, 'a').mockImplementation(); const test = 10;", + None, + ), + ("Date['now'] = vi['fn']()", "vi.spyOn(Date, 'now').mockImplementation()", None), + ( + "window[`${name}`] = vi[`fn`]()", + "vi.spyOn(window, `${name}`).mockImplementation()", + None, + ), + ("obj['prop' + 1] = vi['fn']()", "vi.spyOn(obj, 'prop' + 1).mockImplementation()", None), + ( + "obj.one.two = vi.fn(); const test = 10;", + "vi.spyOn(obj.one, 'two').mockImplementation(); const test = 10;", + None, + ), + ("obj.a = vi.fn(() => 10,)", "vi.spyOn(obj, 'a').mockImplementation(() => 10)", None), + ( + "obj.a.b = vi.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();", + "vi.spyOn(obj.a, 'b').mockImplementation(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();", + None, + ), + ( + "window.fetch = vi.fn(() => ({})).one.two().three().four", + "vi.spyOn(window, 'fetch').mockImplementation(() => ({})).one.two().three().four", + None, + ), + ( + "foo[bar] = vi.fn().mockReturnValue(undefined)", + "vi.spyOn(foo, bar).mockImplementation().mockReturnValue(undefined)", + None, + ), + ( + " + foo.bar = vi.fn().mockImplementation(baz => baz) + foo.bar = vi.fn(a => b).mockImplementation(baz => baz) + ", + " + vi.spyOn(foo, 'bar').mockImplementation(baz => baz) + vi.spyOn(foo, 'bar').mockImplementation(baz => baz) + ", + None, + ), + ]; + + pass.extend(vitest_pass); + fail.extend(vitest_fail); + fix.extend(vitest_fix); + Tester::new(PreferSpyOn::NAME, PreferSpyOn::PLUGIN, pass, fail) - .with_jest_plugin(true) .expect_fix(fix) + .with_jest_plugin(true) + .with_vitest_plugin(true) .test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/jest_prefer_spy_on.snap b/crates/oxc_linter/src/snapshots/jest_prefer_spy_on.snap index d7a7eaecd9a43..28fb77c471949 100644 --- a/crates/oxc_linter/src/snapshots/jest_prefer_spy_on.snap +++ b/crates/oxc_linter/src/snapshots/jest_prefer_spy_on.snap @@ -1,83 +1,164 @@ --- source: crates/oxc_linter/src/tester.rs --- - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:1:9] 1 │ obj.a = jest.fn(); const test = 10; · ─────── ╰──── - help: Use jest.spyOn() instead + help: Replace `obj.a = jest.fn()` with `jest.spyOn(obj, 'a').mockImplementation()`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:1:15] 1 │ Date['now'] = jest['fn']() · ───────── ╰──── - help: Use jest.spyOn() instead + help: Replace `Date['now'] = jest['fn']()` with `jest.spyOn(Date, 'now').mockImplementation()`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:1:21] 1 │ window[`${name}`] = jest[`fn`]() · ───────── ╰──── - help: Use jest.spyOn() instead + help: Replace `window[`${name}`] = jest[`fn`]()` with `jest.spyOn(window, `${name}`).mockImplementation()`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:1:19] 1 │ obj['prop' + 1] = jest['fn']() · ───────── ╰──── - help: Use jest.spyOn() instead + help: Replace `obj['prop' + 1] = jest['fn']()` with `jest.spyOn(obj, 'prop' + 1).mockImplementation()`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:1:15] 1 │ obj.one.two = jest.fn(); const test = 10; · ─────── ╰──── - help: Use jest.spyOn() instead + help: Replace `obj.one.two = jest.fn()` with `jest.spyOn(obj.one, 'two').mockImplementation()`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:1:9] 1 │ obj.a = jest.fn(() => 10,) · ─────── ╰──── - help: Use jest.spyOn() instead + help: Replace `obj.a = jest.fn(() => 10,)` with `jest.spyOn(obj, 'a').mockImplementation(() => 10)`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:1:11] 1 │ obj.a.b = jest.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test(); · ─────── ╰──── - help: Use jest.spyOn() instead + help: Replace `obj.a.b = jest.fn(() => ({}))` with `jest.spyOn(obj.a, 'b').mockImplementation(() => ({}))`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:1:16] 1 │ window.fetch = jest.fn(() => ({})).one.two().three().four · ─────── ╰──── - help: Use jest.spyOn() instead + help: Replace `window.fetch = jest.fn(() => ({}))` with `jest.spyOn(window, 'fetch').mockImplementation(() => ({}))`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:1:12] 1 │ foo[bar] = jest.fn().mockReturnValue(undefined) · ─────── ╰──── - help: Use jest.spyOn() instead + help: Replace `foo[bar] = jest.fn()` with `jest.spyOn(foo, bar).mockImplementation()`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:2:27] 1 │ 2 │ foo.bar = jest.fn().mockImplementation(baz => baz) · ─────── 3 │ foo.bar = jest.fn(a => b).mockImplementation(baz => baz) ╰──── - help: Use jest.spyOn() instead + help: Replace `foo.bar = jest.fn()` with `jest.spyOn(foo, 'bar')`. - ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`. + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. ╭─[prefer_spy_on.tsx:3:27] 2 │ foo.bar = jest.fn().mockImplementation(baz => baz) 3 │ foo.bar = jest.fn(a => b).mockImplementation(baz => baz) · ─────── 4 │ ╰──── - help: Use jest.spyOn() instead + help: Replace `foo.bar = jest.fn(a => b)` with `jest.spyOn(foo, 'bar')`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:1:9] + 1 │ obj.a = vi.fn(); const test = 10; + · ───── + ╰──── + help: Replace `obj.a = vi.fn()` with `vi.spyOn(obj, 'a').mockImplementation()`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:1:15] + 1 │ Date['now'] = vi['fn']() + · ─────── + ╰──── + help: Replace `Date['now'] = vi['fn']()` with `vi.spyOn(Date, 'now').mockImplementation()`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:1:21] + 1 │ window[`${name}`] = vi[`fn`]() + · ─────── + ╰──── + help: Replace `window[`${name}`] = vi[`fn`]()` with `vi.spyOn(window, `${name}`).mockImplementation()`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:1:19] + 1 │ obj['prop' + 1] = vi['fn']() + · ─────── + ╰──── + help: Replace `obj['prop' + 1] = vi['fn']()` with `vi.spyOn(obj, 'prop' + 1).mockImplementation()`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:1:15] + 1 │ obj.one.two = vi.fn(); const test = 10; + · ───── + ╰──── + help: Replace `obj.one.two = vi.fn()` with `vi.spyOn(obj.one, 'two').mockImplementation()`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:1:9] + 1 │ obj.a = vi.fn(() => 10,) + · ───── + ╰──── + help: Replace `obj.a = vi.fn(() => 10,)` with `vi.spyOn(obj, 'a').mockImplementation(() => 10)`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:1:11] + 1 │ obj.a.b = vi.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test(); + · ───── + ╰──── + help: Replace `obj.a.b = vi.fn(() => ({}))` with `vi.spyOn(obj.a, 'b').mockImplementation(() => ({}))`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:1:16] + 1 │ window.fetch = vi.fn(() => ({})).one.two().three().four + · ───── + ╰──── + help: Replace `window.fetch = vi.fn(() => ({}))` with `vi.spyOn(window, 'fetch').mockImplementation(() => ({}))`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:1:12] + 1 │ foo[bar] = vi.fn().mockReturnValue(undefined) + · ───── + ╰──── + help: Replace `foo[bar] = vi.fn()` with `vi.spyOn(foo, bar).mockImplementation()`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:2:22] + 1 │ + 2 │ foo.bar = vi.fn().mockImplementation(baz => baz) + · ───── + 3 │ foo.bar = vi.fn(a => b).mockImplementation(baz => baz) + ╰──── + help: Replace `foo.bar = vi.fn()` with `vi.spyOn(foo, 'bar')`. + + ⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()` or `vi.spyOn()`. + ╭─[prefer_spy_on.tsx:3:22] + 2 │ foo.bar = vi.fn().mockImplementation(baz => baz) + 3 │ foo.bar = vi.fn(a => b).mockImplementation(baz => baz) + · ───── + 4 │ + ╰──── + help: Replace `foo.bar = vi.fn(a => b)` with `vi.spyOn(foo, 'bar')`. diff --git a/crates/oxc_linter/src/utils/mod.rs b/crates/oxc_linter/src/utils/mod.rs index d215ee8b3f53e..63c5e2a8912ce 100644 --- a/crates/oxc_linter/src/utils/mod.rs +++ b/crates/oxc_linter/src/utils/mod.rs @@ -33,7 +33,7 @@ pub use self::{ /// List of Jest rules that have Vitest equivalents. // When adding a new rule to this list, please ensure oxlint-migrate is also updated. // See https://github.com/oxc-project/oxlint-migrate/blob/2c336c67d75adb09a402ae66fb3099f1dedbe516/scripts/constants.ts -const VITEST_COMPATIBLE_JEST_RULES: [&str; 39] = [ +const VITEST_COMPATIBLE_JEST_RULES: [&str; 40] = [ "consistent-test-it", "expect-expect", "max-expects", @@ -64,6 +64,7 @@ const VITEST_COMPATIBLE_JEST_RULES: [&str; 39] = [ "prefer-hooks-on-top", "prefer-lowercase-title", "prefer-mock-promise-shorthand", + "prefer-spy-on", "prefer-strict-equal", "prefer-to-be", "prefer-to-contain",