From fc48cb45607fb88fa06ec772e1ea90e1a6840a02 Mon Sep 17 00:00:00 2001
From: cinchen <eryue0220@gmail.com>
Date: Wed, 26 Jun 2024 21:46:46 +0800
Subject: [PATCH] feat(linter): eslint-plugin-jest/prefer-jest-mocked (#3865)

part of https://github.com/oxc-project/oxc/issues/492

Rule Detail:
[link](https://github.com/jest-community/eslint-plugin-jest/blob/main/src/rules/prefer-jest-mocked.ts)
---
 crates/oxc_linter/src/rules.rs                |   2 +
 .../src/rules/jest/prefer_jest_mocked.rs      | 328 ++++++++++++++++++
 .../src/snapshots/prefer_jest_mocked.snap     | 159 +++++++++
 3 files changed, 489 insertions(+)
 create mode 100644 crates/oxc_linter/src/rules/jest/prefer_jest_mocked.rs
 create mode 100644 crates/oxc_linter/src/snapshots/prefer_jest_mocked.snap

diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index a6d715636..266e85e80 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -184,6 +184,7 @@ mod jest {
     pub mod prefer_equality_matcher;
     pub mod prefer_expect_resolves;
     pub mod prefer_hooks_on_top;
+    pub mod prefer_jest_mocked;
     pub mod prefer_lowercase_title;
     pub mod prefer_mock_promise_shorthand;
     pub mod prefer_spy_on;
@@ -573,6 +574,7 @@ oxc_macros::declare_all_lint_rules! {
     jest::prefer_equality_matcher,
     jest::prefer_expect_resolves,
     jest::prefer_hooks_on_top,
+    jest::prefer_jest_mocked,
     jest::prefer_lowercase_title,
     jest::prefer_mock_promise_shorthand,
     jest::prefer_spy_on,
diff --git a/crates/oxc_linter/src/rules/jest/prefer_jest_mocked.rs b/crates/oxc_linter/src/rules/jest/prefer_jest_mocked.rs
new file mode 100644
index 000000000..51da7778b
--- /dev/null
+++ b/crates/oxc_linter/src/rules/jest/prefer_jest_mocked.rs
@@ -0,0 +1,328 @@
+use oxc_ast::{
+    ast::{TSAsExpression, TSType, TSTypeAssertion, TSTypeName, TSTypeReference},
+    AstKind,
+};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::{GetSpan, Span};
+use phf::{phf_set, Set};
+
+use crate::{context::LintContext, rule::Rule, AstNode};
+
+fn use_jest_mocked(span0: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn(
+        "eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.",
+    )
+    .with_help("Prefer `jest.mocked()`")
+    .with_labels([span0.into()])
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct PreferJestMocked;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// When working with mocks of functions using Jest, it's recommended to use the
+    /// `jest.mocked()` helper function to properly type the mocked functions. This rule
+    /// enforces the use of `jest.mocked()` for better type safety and readability.
+    ///
+    /// Restricted types:
+    ///
+    ///
+    /// - `jest.Mock`
+    /// - `jest.MockedFunction`
+    /// - `jest.MockedClass`
+    /// - `jest.MockedObject`
+    ///
+    /// ### Examples
+    ///
+    /// ```typescript
+    /// // invalid
+    /// (foo as jest.Mock).mockReturnValue(1);
+    /// const mock = (foo as jest.Mock).mockReturnValue(1);
+    /// (foo as unknown as jest.Mock).mockReturnValue(1);
+    /// (Obj.foo as jest.Mock).mockReturnValue(1);
+    /// ([].foo as jest.Mock).mockReturnValue(1);
+    ///
+    /// // valid
+    /// jest.mocked(foo).mockReturnValue(1);
+    /// const mock = jest.mocked(foo).mockReturnValue(1);
+    /// jest.mocked(Obj.foo).mockReturnValue(1);
+    /// jest.mocked([].foo).mockReturnValue(1);
+    /// ```
+    PreferJestMocked,
+    style,
+);
+
+impl Rule for PreferJestMocked {
+    fn run(&self, node: &AstNode, ctx: &LintContext) {
+        if let AstKind::TSAsExpression(ts_expr) = node.kind() {
+            if !matches!(ctx.nodes().parent_kind(node.id()), Some(AstKind::TSAsExpression(_))) {
+                Self::check_ts_as_expression(ts_expr, ctx);
+            }
+        } else if let AstKind::TSTypeAssertion(assert_type) = node.kind() {
+            Self::check_assert_type(assert_type, ctx);
+        }
+    }
+}
+
+const MOCK_TYPES: Set<&'static str> = phf_set! {
+    "Mock",
+    "MockedFunction",
+    "MockedClass",
+    "MockedObject",
+};
+
+impl PreferJestMocked {
+    fn check_ts_as_expression(as_expr: &TSAsExpression, ctx: &LintContext) {
+        let TSType::TSTypeReference(ts_reference) = &as_expr.type_annotation else {
+            return;
+        };
+        let arg_span = as_expr.expression.get_inner_expression().span();
+        Self::check(ts_reference, arg_span, as_expr.span, ctx);
+    }
+
+    fn check_assert_type(assert_type: &TSTypeAssertion, ctx: &LintContext) {
+        let TSType::TSTypeReference(ts_reference) = &assert_type.type_annotation else {
+            return;
+        };
+        let arg_span = assert_type.expression.get_inner_expression().span();
+        Self::check(ts_reference, arg_span, assert_type.span, ctx);
+    }
+
+    fn check(ts_reference: &TSTypeReference, arg_span: Span, span: Span, ctx: &LintContext) {
+        let TSTypeName::QualifiedName(qualified_name) = &ts_reference.type_name else {
+            return;
+        };
+        let TSTypeName::IdentifierReference(ident) = &qualified_name.left else {
+            return;
+        };
+
+        if !&ident.name.eq_ignore_ascii_case("jest")
+            || !MOCK_TYPES.contains(qualified_name.right.name.as_str())
+        {
+            return;
+        }
+
+        ctx.diagnostic_with_fix(use_jest_mocked(span), |fixer| {
+            let span_source_code = fixer.source_range(arg_span);
+            fixer.replace(span, format!("jest.mocked({span_source_code})"))
+        });
+    }
+}
+
+#[test]
+fn test() {
+    use crate::tester::Tester;
+    use std::path::PathBuf;
+
+    let pass = vec![
+        ("foo();", None, None, None),
+        ("jest.mocked(foo).mockReturnValue(1);", None, None, None),
+        ("bar.mockReturnValue(1);", None, None, None),
+        ("sinon.stub(foo).returns(1);", None, None, None),
+        ("foo.mockImplementation(() => 1);", None, None, None),
+        ("obj.foo();", None, None, None),
+        ("mockFn.mockReturnValue(1);", None, None, None),
+        ("arr[0]();", None, None, None),
+        ("obj.foo.mockReturnValue(1);", None, None, None),
+        ("jest.spyOn(obj, 'foo').mockReturnValue(1);", None, None, None),
+        ("(foo as Mock.jest).mockReturnValue(1);", None, None, None),
+        (
+            "
+                type MockType = jest.Mock;
+                const mockFn = jest.fn();
+                (mockFn as MockType).mockReturnValue(1);
+            ",
+            None,
+            None,
+            None,
+        ),
+    ];
+
+    let fail = vec![
+        ("(foo as jest.Mock).mockReturnValue(1);", None, None, None),
+        (
+            "(foo as unknown as string as unknown as jest.Mock).mockReturnValue(1);",
+            None,
+            None,
+            None,
+        ),
+        (
+            "(foo as unknown as jest.Mock as unknown as jest.Mock).mockReturnValue(1);",
+            None,
+            None,
+            None,
+        ),
+        (
+            "(<jest.Mock>foo).mockReturnValue(1);",
+            None,
+            None,
+            Some(PathBuf::from("/prefer-jest-mocked.ts")),
+        ),
+        ("(foo as jest.Mock).mockImplementation(1);", None, None, None),
+        ("(foo as unknown as jest.Mock).mockReturnValue(1);", None, None, None),
+        (
+            "(<jest.Mock>foo as unknown).mockReturnValue(1);",
+            None,
+            None,
+            Some(PathBuf::from("/prefer-jest-mocked.ts")),
+        ),
+        ("(Obj.foo as jest.Mock).mockReturnValue(1);", None, None, None),
+        ("([].foo as jest.Mock).mockReturnValue(1);", None, None, None),
+        ("(foo as jest.MockedFunction).mockReturnValue(1);", None, None, None),
+        ("(foo as jest.MockedFunction).mockImplementation(1);", None, None, None),
+        ("(foo as unknown as jest.MockedFunction).mockReturnValue(1);", None, None, None),
+        ("(Obj.foo as jest.MockedFunction).mockReturnValue(1);", None, None, None),
+        (
+            "(new Array(0).fill(null).foo as jest.MockedFunction).mockReturnValue(1);",
+            None,
+            None,
+            None,
+        ),
+        ("(jest.fn(() => foo) as jest.MockedFunction).mockReturnValue(1);", None, None, None),
+        (
+            "const mockedUseFocused = useFocused as jest.MockedFunction<typeof useFocused>;",
+            None,
+            None,
+            None,
+        ),
+        (
+            "const filter = (MessageService.getMessage as jest.Mock).mock.calls[0][0];",
+            None,
+            None,
+            None,
+        ),
+        (
+            "
+                class A {}
+                (foo as jest.MockedClass<A>)
+            ",
+            None,
+            None,
+            None,
+        ),
+        ("(foo as jest.MockedObject<{method: () => void}>)", None, None, None),
+        ("(Obj['foo'] as jest.MockedFunction).mockReturnValue(1);", None, None, None),
+        (
+            "
+                (
+                new Array(100)
+                    .fill(undefined)
+                    .map(x => x.value)
+                    .filter(v => !!v).myProperty as jest.MockedFunction<{
+                    method: () => void;
+                }>
+                ).mockReturnValue(1);
+            ",
+            None,
+            None,
+            None,
+        ),
+    ];
+
+    let fix = vec![
+        ("(foo as jest.Mock).mockReturnValue(1);", "(jest.mocked(foo)).mockReturnValue(1);"),
+        (
+            "(foo as unknown as string as unknown as jest.Mock).mockReturnValue(1);",
+            "(jest.mocked(foo)).mockReturnValue(1);",
+        ),
+        (
+            "(foo as unknown as jest.Mock as unknown as jest.Mock).mockReturnValue(1);",
+            "(jest.mocked(foo)).mockReturnValue(1);",
+        ),
+        // Note: couldn't fix
+        // (
+        //     "(<jest.Mock>foo).mockReturnValue(1);",
+        //     "(jest.mocked(foo)).mockReturnValue(1);",
+        // ),
+        ("(foo as jest.Mock).mockImplementation(1);", "(jest.mocked(foo)).mockImplementation(1);"),
+        (
+            "(foo as unknown as jest.Mock).mockReturnValue(1);",
+            "(jest.mocked(foo)).mockReturnValue(1);",
+        ),
+        // Note: couldn't fix
+        // (
+        //     "(<jest.Mock>foo as unknown).mockReturnValue(1);",
+        //     "(jest.mocked(foo) as unknown).mockReturnValue(1);",
+        // ),
+        (
+            "(Obj.foo as jest.Mock).mockReturnValue(1);",
+            "(jest.mocked(Obj.foo)).mockReturnValue(1);",
+        ),
+        ("([].foo as jest.Mock).mockReturnValue(1);", "(jest.mocked([].foo)).mockReturnValue(1);"),
+        (
+            "(foo as jest.MockedFunction).mockReturnValue(1);",
+            "(jest.mocked(foo)).mockReturnValue(1);",
+        ),
+        (
+            "(foo as jest.MockedFunction).mockImplementation(1);",
+            "(jest.mocked(foo)).mockImplementation(1);",
+        ),
+        (
+            "(foo as unknown as jest.MockedFunction).mockReturnValue(1);",
+            "(jest.mocked(foo)).mockReturnValue(1);",
+        ),
+        (
+            "(Obj.foo as jest.MockedFunction).mockReturnValue(1);",
+            "(jest.mocked(Obj.foo)).mockReturnValue(1);",
+        ),
+        (
+            "(new Array(0).fill(null).foo as jest.MockedFunction).mockReturnValue(1);",
+            "(jest.mocked(new Array(0).fill(null).foo)).mockReturnValue(1);",
+        ),
+        (
+            "(jest.fn(() => foo) as jest.MockedFunction).mockReturnValue(1);",
+            "(jest.mocked(jest.fn(() => foo))).mockReturnValue(1);",
+        ),
+        (
+            "const mockedUseFocused = useFocused as jest.MockedFunction<typeof useFocused>;",
+            "const mockedUseFocused = jest.mocked(useFocused);",
+        ),
+        (
+            "const filter = (MessageService.getMessage as jest.Mock).mock.calls[0][0];",
+            "const filter = (jest.mocked(MessageService.getMessage)).mock.calls[0][0];",
+        ),
+        (
+            "
+                class A {}
+                (foo as jest.MockedClass<A>)
+            ",
+            "
+                class A {}
+                (jest.mocked(foo))
+            ",
+        ),
+        ("(foo as jest.MockedObject<{method: () => void}>)", "(jest.mocked(foo))"),
+        (
+            "(Obj['foo'] as jest.MockedFunction).mockReturnValue(1);",
+            "(jest.mocked(Obj['foo'])).mockReturnValue(1);",
+        ),
+        (
+            "
+                (
+                new Array(100)
+                    .fill(undefined)
+                    .map(x => x.value)
+                    .filter(v => !!v).myProperty as jest.MockedFunction<{
+                    method: () => void;
+                }>
+                ).mockReturnValue(1);
+            ",
+            "
+                (
+                jest.mocked(new Array(100)
+                    .fill(undefined)
+                    .map(x => x.value)
+                    .filter(v => !!v).myProperty)
+                ).mockReturnValue(1);
+            ",
+        ),
+    ];
+
+    Tester::new(PreferJestMocked::NAME, pass, fail)
+        .with_jest_plugin(true)
+        .expect_fix(fix)
+        .test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/prefer_jest_mocked.snap b/crates/oxc_linter/src/snapshots/prefer_jest_mocked.snap
new file mode 100644
index 000000000..7cc7a8f94
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/prefer_jest_mocked.snap
@@ -0,0 +1,159 @@
+---
+source: crates/oxc_linter/src/tester.rs
+assertion_line: 209
+expression: prefer_jest_mocked
+---
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (foo as jest.Mock).mockReturnValue(1);
+   ·  ────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (foo as unknown as string as unknown as jest.Mock).mockReturnValue(1);
+   ·  ────────────────────────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (foo as unknown as jest.Mock as unknown as jest.Mock).mockReturnValue(1);
+   ·  ───────────────────────────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (<jest.Mock>foo).mockReturnValue(1);
+   ·  ──────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (foo as jest.Mock).mockImplementation(1);
+   ·  ────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (foo as unknown as jest.Mock).mockReturnValue(1);
+   ·  ───────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (<jest.Mock>foo as unknown).mockReturnValue(1);
+   ·  ──────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (Obj.foo as jest.Mock).mockReturnValue(1);
+   ·  ────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ ([].foo as jest.Mock).mockReturnValue(1);
+   ·  ───────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (foo as jest.MockedFunction).mockReturnValue(1);
+   ·  ──────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (foo as jest.MockedFunction).mockImplementation(1);
+   ·  ──────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (foo as unknown as jest.MockedFunction).mockReturnValue(1);
+   ·  ─────────────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (Obj.foo as jest.MockedFunction).mockReturnValue(1);
+   ·  ──────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (new Array(0).fill(null).foo as jest.MockedFunction).mockReturnValue(1);
+   ·  ──────────────────────────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (jest.fn(() => foo) as jest.MockedFunction).mockReturnValue(1);
+   ·  ─────────────────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:26]
+ 1 │ const mockedUseFocused = useFocused as jest.MockedFunction<typeof useFocused>;
+   ·                          ────────────────────────────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:17]
+ 1 │ const filter = (MessageService.getMessage as jest.Mock).mock.calls[0][0];
+   ·                 ──────────────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:3:18]
+ 2 │                 class A {}
+ 3 │                 (foo as jest.MockedClass<A>)
+   ·                  ──────────────────────────
+ 4 │             
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (foo as jest.MockedObject<{method: () => void}>)
+   ·  ──────────────────────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:1:2]
+ 1 │ (Obj['foo'] as jest.MockedFunction).mockReturnValue(1);
+   ·  ─────────────────────────────────
+   ╰────
+  help: Prefer `jest.mocked()`
+
+  ⚠ eslint-plugin-jest(prefer-jest-mocked): Prefer `jest.mocked()` over `fn as jest.Mock`.
+   ╭─[prefer_jest_mocked.tsx:3:17]
+ 2 │                     (
+ 3 │ ╭─▶                 new Array(100)
+ 4 │ │                       .fill(undefined)
+ 5 │ │                       .map(x => x.value)
+ 6 │ │                       .filter(v => !!v).myProperty as jest.MockedFunction<{
+ 7 │ │                       method: () => void;
+ 8 │ ╰─▶                 }>
+ 9 │                     ).mockReturnValue(1);
+   ╰────
+  help: Prefer `jest.mocked()`
