From fc48cb45607fb88fa06ec772e1ea90e1a6840a02 Mon Sep 17 00:00:00 2001 From: cinchen 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 a6d7156367a4c..266e85e809e24 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 0000000000000..51da7778b4723 --- /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, + ), + ( + "(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), + ( + "(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;", + None, + None, + None, + ), + ( + "const filter = (MessageService.getMessage as jest.Mock).mock.calls[0][0];", + None, + None, + None, + ), + ( + " + class A {} + (foo as jest.MockedClass) + ", + 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 + // ( + // "(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 + // ( + // "(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;", + "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) + ", + " + 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 0000000000000..7cc7a8f947be3 --- /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 │ (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 │ (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; + · ──────────────────────────────────────────────────── + ╰──── + 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) + · ────────────────────────── + 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()`