mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter): eslint-plugin-jest: prefer-spy-on (#2666)
Rule Detail: [link](https://github.com/jest-community/eslint-plugin-jest/blob/main/src/rules/prefer-spy-on.ts)
This commit is contained in:
parent
2dfc0cc28e
commit
265030d049
3 changed files with 388 additions and 0 deletions
|
|
@ -153,6 +153,7 @@ mod jest {
|
||||||
pub mod no_test_return_statement;
|
pub mod no_test_return_statement;
|
||||||
pub mod prefer_called_with;
|
pub mod prefer_called_with;
|
||||||
pub mod prefer_equality_matcher;
|
pub mod prefer_equality_matcher;
|
||||||
|
pub mod prefer_spy_on;
|
||||||
pub mod prefer_strict_equal;
|
pub mod prefer_strict_equal;
|
||||||
pub mod prefer_to_have_length;
|
pub mod prefer_to_have_length;
|
||||||
pub mod prefer_todo;
|
pub mod prefer_todo;
|
||||||
|
|
@ -456,6 +457,7 @@ oxc_macros::declare_all_lint_rules! {
|
||||||
jest::no_test_return_statement,
|
jest::no_test_return_statement,
|
||||||
jest::prefer_called_with,
|
jest::prefer_called_with,
|
||||||
jest::prefer_equality_matcher,
|
jest::prefer_equality_matcher,
|
||||||
|
jest::prefer_spy_on,
|
||||||
jest::prefer_strict_equal,
|
jest::prefer_strict_equal,
|
||||||
jest::prefer_to_have_length,
|
jest::prefer_to_have_length,
|
||||||
jest::prefer_todo,
|
jest::prefer_todo,
|
||||||
|
|
|
||||||
301
crates/oxc_linter/src/rules/jest/prefer_spy_on.rs
Normal file
301
crates/oxc_linter/src/rules/jest/prefer_spy_on.rs
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
use oxc_ast::{
|
||||||
|
ast::{
|
||||||
|
Argument, AssignmentExpression, AssignmentTarget, CallExpression, Expression,
|
||||||
|
MemberExpression, SimpleAssignmentTarget,
|
||||||
|
},
|
||||||
|
AstKind,
|
||||||
|
};
|
||||||
|
use oxc_diagnostics::{
|
||||||
|
miette::{self, Diagnostic},
|
||||||
|
thiserror::Error,
|
||||||
|
};
|
||||||
|
use oxc_macros::declare_oxc_lint;
|
||||||
|
use oxc_semantic::AstNode;
|
||||||
|
use oxc_span::Span;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
context::LintContext,
|
||||||
|
fixer::Fix,
|
||||||
|
rule::Rule,
|
||||||
|
utils::{get_node_name, parse_general_jest_fn_call, PossibleJestNode},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Diagnostic)]
|
||||||
|
#[error("eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.")]
|
||||||
|
#[diagnostic(severity(warning), help("Use jest.spyOn() instead"))]
|
||||||
|
struct UseJestSpyOn(#[label] pub Span);
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct PreferSpyOn;
|
||||||
|
|
||||||
|
declare_oxc_lint!(
|
||||||
|
/// ### What it does
|
||||||
|
///
|
||||||
|
/// When mocking a function by overwriting a property you have to manually restore
|
||||||
|
/// the original implementation when cleaning up. When using `jest.spyOn()` Jest
|
||||||
|
/// keeps track of changes, and they can be restored with `jest.restoreAllMocks()`,
|
||||||
|
/// `mockFn.mockRestore()` or by setting `restoreMocks` to `true` in the Jest
|
||||||
|
/// config.
|
||||||
|
///
|
||||||
|
/// Note: The mock created by `jest.spyOn()` still behaves the same as the original
|
||||||
|
/// function. The original function can be overwritten with
|
||||||
|
/// `mockFn.mockImplementation()` or by some of the
|
||||||
|
/// [other mock functions](https://jestjs.io/docs/en/mock-function-api).
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
///
|
||||||
|
/// ```javascript
|
||||||
|
/// // invalid
|
||||||
|
/// Date.now = jest.fn();
|
||||||
|
/// Date.now = jest.fn(() => 10);
|
||||||
|
///
|
||||||
|
/// // valid
|
||||||
|
/// jest.spyOn(Date, 'now');
|
||||||
|
/// jest.spyOn(Date, 'now').mockImplementation(() => 10);
|
||||||
|
/// ```
|
||||||
|
PreferSpyOn,
|
||||||
|
style,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Rule for PreferSpyOn {
|
||||||
|
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||||
|
let AstKind::AssignmentExpression(assign_expr) = node.kind() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let left = &assign_expr.left;
|
||||||
|
let right = &assign_expr.right;
|
||||||
|
|
||||||
|
let AssignmentTarget::SimpleAssignmentTarget(
|
||||||
|
SimpleAssignmentTarget::MemberAssignmentTarget(left_assign),
|
||||||
|
) = left
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match right {
|
||||||
|
Expression::CallExpression(call_expr) => {
|
||||||
|
Self::check_and_fix(assign_expr, call_expr, left_assign, node, ctx);
|
||||||
|
}
|
||||||
|
Expression::MemberExpression(mem_expr) => {
|
||||||
|
let Expression::CallExpression(call_expr) = mem_expr.object() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
Self::check_and_fix(assign_expr, call_expr, left_assign, node, ctx);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreferSpyOn {
|
||||||
|
fn check_and_fix<'a>(
|
||||||
|
assign_expr: &AssignmentExpression,
|
||||||
|
call_expr: &'a CallExpression<'a>,
|
||||||
|
left_assign: &MemberExpression,
|
||||||
|
node: &AstNode<'a>,
|
||||||
|
ctx: &LintContext<'a>,
|
||||||
|
) {
|
||||||
|
let Some(jest_fn_call) =
|
||||||
|
parse_general_jest_fn_call(call_expr, &PossibleJestNode { node, original: None }, ctx)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(first_fn_member) = jest_fn_call.members.first() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if first_fn_member.name().unwrap() != "fn" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.diagnostic_with_fix(
|
||||||
|
UseJestSpyOn(Span::new(call_expr.span.start, first_fn_member.span.end)),
|
||||||
|
|| {
|
||||||
|
let (end, has_mock_implementation) = if jest_fn_call.members.len() > 1 {
|
||||||
|
let second = &jest_fn_call.members[1];
|
||||||
|
let has_mock_implementation = jest_fn_call
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.filter(|member| member.is_name_equal("mockImplementation"))
|
||||||
|
.count()
|
||||||
|
> 0;
|
||||||
|
|
||||||
|
(second.span.start - 1, has_mock_implementation)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
first_fn_member.span.end + (call_expr.span.end - first_fn_member.span.end),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let content =
|
||||||
|
Self::build_code(call_expr, left_assign, has_mock_implementation, ctx);
|
||||||
|
Fix::new(content, Span::new(assign_expr.span.start, end))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_code<'a>(
|
||||||
|
call_expr: &'a CallExpression<'a>,
|
||||||
|
left_assign: &MemberExpression,
|
||||||
|
has_mock_implementation: bool,
|
||||||
|
ctx: &LintContext,
|
||||||
|
) -> String {
|
||||||
|
let mut formatter = ctx.codegen();
|
||||||
|
formatter.print_str(b"jest.spyOn(");
|
||||||
|
|
||||||
|
match left_assign {
|
||||||
|
MemberExpression::ComputedMemberExpression(cmp_mem_expr) => {
|
||||||
|
formatter.print_expression(&cmp_mem_expr.object);
|
||||||
|
formatter.print(b',');
|
||||||
|
formatter.print_hard_space();
|
||||||
|
formatter.print_expression(&cmp_mem_expr.expression);
|
||||||
|
}
|
||||||
|
MemberExpression::StaticMemberExpression(static_mem_expr) => {
|
||||||
|
let name = &static_mem_expr.property.name;
|
||||||
|
formatter.print_expression(&static_mem_expr.object);
|
||||||
|
formatter.print(b',');
|
||||||
|
formatter.print_hard_space();
|
||||||
|
formatter.print_str(format!("\'{name}\'").as_bytes());
|
||||||
|
}
|
||||||
|
MemberExpression::PrivateFieldExpression(_) => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.print(b')');
|
||||||
|
|
||||||
|
if has_mock_implementation {
|
||||||
|
return formatter.into_source_text();
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.print_str(b".mockImplementation(");
|
||||||
|
|
||||||
|
if let Some(Argument::Expression(expr)) = Self::get_jest_fn_call(call_expr) {
|
||||||
|
formatter.print_expression(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.print(b')');
|
||||||
|
formatter.into_source_text()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_jest_fn_call<'a>(call_expr: &'a CallExpression<'a>) -> Option<&'a Argument<'a>> {
|
||||||
|
let is_jest_fn = get_node_name(&call_expr.callee) == "jest.fn";
|
||||||
|
|
||||||
|
if is_jest_fn {
|
||||||
|
return call_expr.arguments.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
match &call_expr.callee {
|
||||||
|
Expression::MemberExpression(mem_expr) => {
|
||||||
|
if let Some(call_expr) = Self::find_mem_expr(mem_expr) {
|
||||||
|
return Self::get_jest_fn_call(call_expr);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Expression::CallExpression(call_expr) => Self::get_jest_fn_call(call_expr),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_mem_expr<'a>(mem_expr: &'a MemberExpression<'a>) -> Option<&'a CallExpression<'a>> {
|
||||||
|
match mem_expr.object() {
|
||||||
|
Expression::CallExpression(call_expr) => Some(call_expr),
|
||||||
|
Expression::MemberExpression(mem_expr) => Self::find_mem_expr(mem_expr),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tests() {
|
||||||
|
use crate::tester::Tester;
|
||||||
|
|
||||||
|
let pass = vec![
|
||||||
|
("Date.now = () => 10", None),
|
||||||
|
("window.fetch = jest.fn", None),
|
||||||
|
("Date.now = fn()", None),
|
||||||
|
("obj.mock = jest.something()", None),
|
||||||
|
("const mock = jest.fn()", None),
|
||||||
|
("mock = jest.fn()", None),
|
||||||
|
("const mockObj = { mock: jest.fn() }", None),
|
||||||
|
("mockObj = { mock: jest.fn() }", None),
|
||||||
|
("window[`${name}`] = jest[`fn${expression}`]()", None),
|
||||||
|
];
|
||||||
|
|
||||||
|
let fail = vec![
|
||||||
|
("obj.a = jest.fn(); const test = 10;", None),
|
||||||
|
("Date['now'] = jest['fn']()", None),
|
||||||
|
("window[`${name}`] = jest[`fn`]()", None),
|
||||||
|
("obj['prop' + 1] = jest['fn']()", None),
|
||||||
|
("obj.one.two = jest.fn(); const test = 10;", None),
|
||||||
|
("obj.a = jest.fn(() => 10,)", None),
|
||||||
|
(
|
||||||
|
"obj.a.b = jest.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
("window.fetch = jest.fn(() => ({})).one.two().three().four", None),
|
||||||
|
("foo[bar] = jest.fn().mockReturnValue(undefined)", None),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
foo.bar = jest.fn().mockImplementation(baz => baz)
|
||||||
|
foo.bar = jest.fn(a => b).mockImplementation(baz => baz)
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let fix = vec![
|
||||||
|
(
|
||||||
|
"obj.a = jest.fn(); const test = 10;",
|
||||||
|
"jest.spyOn(obj, 'a').mockImplementation(); const test = 10;",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
("Date['now'] = jest['fn']()", "jest.spyOn(Date, 'now').mockImplementation()", None),
|
||||||
|
(
|
||||||
|
"window[`${name}`] = jest[`fn`]()",
|
||||||
|
"jest.spyOn(window, `${name}`).mockImplementation()",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"obj['prop' + 1] = jest['fn']()",
|
||||||
|
"jest.spyOn(obj, 'prop' + 1).mockImplementation()",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"obj.one.two = jest.fn(); const test = 10;",
|
||||||
|
"jest.spyOn(obj.one, 'two').mockImplementation(); const test = 10;",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
("obj.a = jest.fn(() => 10,)", "jest.spyOn(obj, 'a').mockImplementation(() => 10)", None),
|
||||||
|
(
|
||||||
|
"obj.a.b = jest.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();",
|
||||||
|
"jest.spyOn(obj.a, 'b').mockImplementation(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"window.fetch = jest.fn(() => ({})).one.two().three().four",
|
||||||
|
"jest.spyOn(window, 'fetch').mockImplementation(() => ({})).one.two().three().four",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"foo[bar] = jest.fn().mockReturnValue(undefined)",
|
||||||
|
"jest.spyOn(foo, bar).mockImplementation().mockReturnValue(undefined)",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"
|
||||||
|
foo.bar = jest.fn().mockImplementation(baz => baz)
|
||||||
|
foo.bar = jest.fn(a => b).mockImplementation(baz => baz)
|
||||||
|
",
|
||||||
|
"
|
||||||
|
jest.spyOn(foo, 'bar').mockImplementation(baz => baz)
|
||||||
|
jest.spyOn(foo, 'bar').mockImplementation(baz => baz)
|
||||||
|
",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
Tester::new(PreferSpyOn::NAME, pass, fail)
|
||||||
|
.with_jest_plugin(true)
|
||||||
|
.expect_fix(fix)
|
||||||
|
.test_and_snapshot();
|
||||||
|
}
|
||||||
85
crates/oxc_linter/src/snapshots/prefer_spy_on.snap
Normal file
85
crates/oxc_linter/src/snapshots/prefer_spy_on.snap
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
---
|
||||||
|
source: crates/oxc_linter/src/tester.rs
|
||||||
|
assertion_line: 151
|
||||||
|
expression: prefer_spy_on
|
||||||
|
---
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.
|
||||||
|
╭─[prefer_spy_on.tsx:1:9]
|
||||||
|
1 │ obj.a = jest.fn(); const test = 10;
|
||||||
|
· ───────
|
||||||
|
╰────
|
||||||
|
help: Use jest.spyOn() instead
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.
|
||||||
|
╭─[prefer_spy_on.tsx:1:15]
|
||||||
|
1 │ Date['now'] = jest['fn']()
|
||||||
|
· ─────────
|
||||||
|
╰────
|
||||||
|
help: Use jest.spyOn() instead
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.
|
||||||
|
╭─[prefer_spy_on.tsx:1:21]
|
||||||
|
1 │ window[`${name}`] = jest[`fn`]()
|
||||||
|
· ─────────
|
||||||
|
╰────
|
||||||
|
help: Use jest.spyOn() instead
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.
|
||||||
|
╭─[prefer_spy_on.tsx:1:19]
|
||||||
|
1 │ obj['prop' + 1] = jest['fn']()
|
||||||
|
· ─────────
|
||||||
|
╰────
|
||||||
|
help: Use jest.spyOn() instead
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.
|
||||||
|
╭─[prefer_spy_on.tsx:1:15]
|
||||||
|
1 │ obj.one.two = jest.fn(); const test = 10;
|
||||||
|
· ───────
|
||||||
|
╰────
|
||||||
|
help: Use jest.spyOn() instead
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.
|
||||||
|
╭─[prefer_spy_on.tsx:1:9]
|
||||||
|
1 │ obj.a = jest.fn(() => 10,)
|
||||||
|
· ───────
|
||||||
|
╰────
|
||||||
|
help: Use jest.spyOn() instead
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.
|
||||||
|
╭─[prefer_spy_on.tsx:1:11]
|
||||||
|
1 │ obj.a.b = jest.fn(() => ({})).mockReturnValue('default').mockReturnValueOnce('first call'); test();
|
||||||
|
· ───────
|
||||||
|
╰────
|
||||||
|
help: Use jest.spyOn() instead
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.
|
||||||
|
╭─[prefer_spy_on.tsx:1:16]
|
||||||
|
1 │ window.fetch = jest.fn(() => ({})).one.two().three().four
|
||||||
|
· ───────
|
||||||
|
╰────
|
||||||
|
help: Use jest.spyOn() instead
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.spyOn()`.
|
||||||
|
╭─[prefer_spy_on.tsx:1:12]
|
||||||
|
1 │ foo[bar] = jest.fn().mockReturnValue(undefined)
|
||||||
|
· ───────
|
||||||
|
╰────
|
||||||
|
help: Use jest.spyOn() instead
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.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
|
||||||
|
|
||||||
|
⚠ eslint-plugin-jest(prefer-spy-on): Suggest using `jest.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
|
||||||
Loading…
Reference in a new issue