From 0623a5335fdb77b50c88b78ea78a4cc6db21ca62 Mon Sep 17 00:00:00 2001 From: cinchen Date: Sun, 17 Mar 2024 09:44:01 +0800 Subject: [PATCH] feat(linter): eslint-plugin-jest: `prefer-to-contain` (#2735) Rule Detail: [link](https://github.com/jest-community/eslint-plugin-jest/blob/main/src/rules/prefer-to-contain.ts) --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/jest/prefer_to_contain.rs | 225 ++++++++++++++++++ .../src/snapshots/prefer_to_contain.snap | 204 ++++++++++++++++ 3 files changed, 431 insertions(+) create mode 100644 crates/oxc_linter/src/rules/jest/prefer_to_contain.rs create mode 100644 crates/oxc_linter/src/snapshots/prefer_to_contain.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 5678b8b2a..2d0f0ac4f 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -158,6 +158,7 @@ mod jest { pub mod prefer_spy_on; pub mod prefer_strict_equal; pub mod prefer_to_be; + pub mod prefer_to_contain; pub mod prefer_to_have_length; pub mod prefer_todo; pub mod require_to_throw_message; @@ -464,6 +465,7 @@ oxc_macros::declare_all_lint_rules! { jest::prefer_spy_on, jest::prefer_strict_equal, jest::prefer_to_be, + jest::prefer_to_contain, jest::prefer_to_have_length, jest::prefer_todo, jest::require_to_throw_message, diff --git a/crates/oxc_linter/src/rules/jest/prefer_to_contain.rs b/crates/oxc_linter/src/rules/jest/prefer_to_contain.rs new file mode 100644 index 000000000..e346fe919 --- /dev/null +++ b/crates/oxc_linter/src/rules/jest/prefer_to_contain.rs @@ -0,0 +1,225 @@ +use oxc_ast::{ + ast::{Argument, CallExpression, Expression}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{ + collect_possible_jest_call_node, is_equality_matcher, parse_expect_jest_fn_call, + KnownMemberExpressionParentKind, PossibleJestNode, + }, +}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`.")] +#[diagnostic(severity(warning))] +struct UseToContain(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct PreferToContain; + +declare_oxc_lint!( + /// ### What it does + /// + /// In order to have a better failure message, `toContain()` should be used upon + /// asserting expectations on an array containing an object. + /// + /// ### Why is this bad? + /// + /// TThis rule triggers a warning if `toBe()`, `toEqual()` or `toStrictEqual()` is + /// used to assert object inclusion in an array + /// + /// ### Example + /// + /// ```javascript + /// // valid + /// expect(a).toContain(b); + /// expect(a).not.toContain(b); + /// + /// // invalid + /// expect(a.includes(b)).toBe(true); + /// expect(a.includes(b)).not.toBe(true); + /// expect(a.includes(b)).toBe(false); + /// expect(a.includes(b)).toEqual(true); + /// expect(a.includes(b)).toStrictEqual(true); + /// ``` + /// + PreferToContain, + style, +); + +impl Rule for PreferToContain { + fn run_once(&self, ctx: &LintContext) { + for possible_jest_node in &collect_possible_jest_call_node(ctx) { + Self::run(possible_jest_node, ctx); + } + } +} + +impl PreferToContain { + fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) { + let node = possible_jest_node.node; + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + let Some(jest_expect_fn_call) = + parse_expect_jest_fn_call(call_expr, possible_jest_node, ctx) + else { + return; + }; + let Some(parent) = jest_expect_fn_call.head.parent else { + return; + }; + let Some(matcher) = jest_expect_fn_call.matcher() else { + return; + }; + + if !matches!( + jest_expect_fn_call.head.parent_kind.unwrap(), + KnownMemberExpressionParentKind::Call + ) || jest_expect_fn_call.args.is_empty() + { + return; + } + + let Some(Argument::Expression(jest_expect_first_arg)) = jest_expect_fn_call.args.first() + else { + return; + }; + let Expression::CallExpression(expect_call_expr) = parent else { + return; + }; + + // handle "expect()" + if expect_call_expr.arguments.is_empty() + || !matches!( + jest_expect_first_arg.get_inner_expression(), + Expression::BooleanLiteral(_) + ) + { + return; + } + + let Some(first_argument) = expect_call_expr.arguments.first() else { + return; + }; + let Argument::Expression(Expression::CallExpression(includes_call_expr)) = first_argument + else { + return; + }; + + if !is_equality_matcher(matcher) + || !Self::is_fixable_includes_call_expression(includes_call_expr) + { + return; + } + + ctx.diagnostic(UseToContain(matcher.span)); + } + + fn is_fixable_includes_call_expression(call_expr: &CallExpression) -> bool { + let Expression::MemberExpression(mem_expr) = &call_expr.callee else { + return false; + }; + + mem_expr.static_property_name() == Some("includes") + // handle "expect(a.includes())" + && !call_expr.arguments.is_empty() + // handle "expect(a.includes(b,c))" + && call_expr.arguments.len() == 1 + // handle "expect(a.includes(...[]))" + && matches!(call_expr.arguments.first(), Some(Argument::Expression(_))) + } +} + +#[test] +fn tests() { + use crate::tester::Tester; + + let pass = vec![ + ("expect.hasAssertions", None), + ("expect.hasAssertions()", None), + ("expect.assertions(1)", None), + ("expect().toBe(false);", None), + ("expect(a).toContain(b);", None), + ("expect(a.name).toBe('b');", None), + ("expect(a).toBe(true);", None), + ("expect(a).toEqual(b)", None), + ("expect(a.test(c)).toEqual(b)", None), + ("expect(a.includes(b)).toEqual()", None), + ("expect(a.includes(b)).toEqual(\"test\")", None), + ("expect(a.includes(b)).toBe(\"test\")", None), + ("expect(a.includes()).toEqual()", None), + ("expect(a.includes()).toEqual(true)", None), + ("expect(a.includes(b,c)).toBe(true)", None), + ("expect([{a:1}]).toContain({a:1})", None), + ("expect([1].includes(1)).toEqual", None), + ("expect([1].includes).toEqual", None), + ("expect([1].includes).not", None), + ("expect(a.test(b)).resolves.toEqual(true)", None), + ("expect(a.test(b)).resolves.not.toEqual(true)", None), + ("expect(a).not.toContain(b)", None), + ("expect(a.includes(...[])).toBe(true)", None), + ("expect(a.includes(b)).toBe(...true)", None), + ("expect(a);", None), + // typescript + ( + "(expect('Model must be bound to an array if the multiple property is true') as any).toHaveBeenTipped()", + None, + ), + ("expect(a.includes(b)).toEqual(0 as boolean);", None), + ]; + + let fail = vec![ + ("expect(a.includes(b)).toEqual(true);", None), + ("expect(a.includes(b,),).toEqual(true,)", None), + ("expect(a['includes'](b)).toEqual(true);", None), + ("expect(a['includes'](b))['toEqual'](true);", None), + ("expect(a['includes'](b)).toEqual(false);", None), + ("expect(a['includes'](b)).not.toEqual(false);", None), + ("expect(a['includes'](b))['not'].toEqual(false);", None), + ("expect(a['includes'](b))['not']['toEqual'](false);", None), + ("expect(a.includes(b)).toEqual(false);", None), + ("expect(a.includes(b)).not.toEqual(false);", None), + ("expect(a.includes(b)).not.toEqual(true);", None), + ("expect(a.includes(b)).toBe(true);", None), + ("expect(a.includes(b)).toBe(false);", None), + ("expect(a.includes(b)).not.toBe(false);", None), + ("expect(a.includes(b)).not.toBe(true);", None), + ("expect(a.includes(b)).toStrictEqual(true);", None), + ("expect(a.includes(b)).toStrictEqual(false);", None), + ("expect(a.includes(b)).not.toStrictEqual(false);", None), + ("expect(a.includes(b)).not.toStrictEqual(true);", None), + ("expect(a.test(t).includes(b.test(p))).toEqual(true);", None), + ("expect(a.test(t).includes(b.test(p))).toEqual(false);", None), + ("expect(a.test(t).includes(b.test(p))).not.toEqual(true);", None), + ("expect(a.test(t).includes(b.test(p))).not.toEqual(false);", None), + ("expect([{a:1}].includes({b:1})).toBe(true);", None), + ("expect([{a:1}].includes({a:1})).toBe(false);", None), + ("expect([{a:1}].includes({a:1})).not.toBe(true);", None), + ("expect([{a:1}].includes({a:1})).not.toBe(false);", None), + ("expect([{a:1}].includes({a:1})).toStrictEqual(true);", None), + ("expect([{a:1}].includes({a:1})).toStrictEqual(false);", None), + ("expect([{a:1}].includes({a:1})).not.toStrictEqual(true);", None), + ("expect([{a:1}].includes({a:1})).not.toStrictEqual(false);", None), + ( + " + import { expect as pleaseExpect } from '@jest/globals'; + pleaseExpect([{a:1}].includes({a:1})).not.toStrictEqual(false); + ", + None, + ), + // typescript + ("expect(a.includes(b)).toEqual(false as boolean);", None), + ]; + + Tester::new(PreferToContain::NAME, pass, fail).with_jest_plugin(true).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_to_contain.snap b/crates/oxc_linter/src/snapshots/prefer_to_contain.snap new file mode 100644 index 000000000..c0c784207 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_to_contain.snap @@ -0,0 +1,204 @@ +--- +source: crates/oxc_linter/src/tester.rs +assertion_line: 151 +expression: prefer_to_contain +--- + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:23] + 1 │ expect(a.includes(b)).toEqual(true); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:25] + 1 │ expect(a.includes(b,),).toEqual(true,) + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:26] + 1 │ expect(a['includes'](b)).toEqual(true); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:26] + 1 │ expect(a['includes'](b))['toEqual'](true); + · ───────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:26] + 1 │ expect(a['includes'](b)).toEqual(false); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:30] + 1 │ expect(a['includes'](b)).not.toEqual(false); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:33] + 1 │ expect(a['includes'](b))['not'].toEqual(false); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:33] + 1 │ expect(a['includes'](b))['not']['toEqual'](false); + · ───────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:23] + 1 │ expect(a.includes(b)).toEqual(false); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:27] + 1 │ expect(a.includes(b)).not.toEqual(false); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:27] + 1 │ expect(a.includes(b)).not.toEqual(true); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:23] + 1 │ expect(a.includes(b)).toBe(true); + · ──── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:23] + 1 │ expect(a.includes(b)).toBe(false); + · ──── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:27] + 1 │ expect(a.includes(b)).not.toBe(false); + · ──── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:27] + 1 │ expect(a.includes(b)).not.toBe(true); + · ──── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:23] + 1 │ expect(a.includes(b)).toStrictEqual(true); + · ───────────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:23] + 1 │ expect(a.includes(b)).toStrictEqual(false); + · ───────────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:27] + 1 │ expect(a.includes(b)).not.toStrictEqual(false); + · ───────────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:27] + 1 │ expect(a.includes(b)).not.toStrictEqual(true); + · ───────────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:39] + 1 │ expect(a.test(t).includes(b.test(p))).toEqual(true); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:39] + 1 │ expect(a.test(t).includes(b.test(p))).toEqual(false); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:43] + 1 │ expect(a.test(t).includes(b.test(p))).not.toEqual(true); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:43] + 1 │ expect(a.test(t).includes(b.test(p))).not.toEqual(false); + · ─────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:33] + 1 │ expect([{a:1}].includes({b:1})).toBe(true); + · ──── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:33] + 1 │ expect([{a:1}].includes({a:1})).toBe(false); + · ──── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:37] + 1 │ expect([{a:1}].includes({a:1})).not.toBe(true); + · ──── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:37] + 1 │ expect([{a:1}].includes({a:1})).not.toBe(false); + · ──── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:33] + 1 │ expect([{a:1}].includes({a:1})).toStrictEqual(true); + · ───────────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:33] + 1 │ expect([{a:1}].includes({a:1})).toStrictEqual(false); + · ───────────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:37] + 1 │ expect([{a:1}].includes({a:1})).not.toStrictEqual(true); + · ───────────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:37] + 1 │ expect([{a:1}].includes({a:1})).not.toStrictEqual(false); + · ───────────── + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:3:59] + 2 │ import { expect as pleaseExpect } from '@jest/globals'; + 3 │ pleaseExpect([{a:1}].includes({a:1})).not.toStrictEqual(false); + · ───────────── + 4 │ + ╰──── + + ⚠ eslint-plugin-jest(prefer-to-contain): Suggest using `toContain()`. + ╭─[prefer_to_contain.tsx:1:23] + 1 │ expect(a.includes(b)).toEqual(false as boolean); + · ─────── + ╰────