diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index ec19595e5..c86c3bc99 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -141,6 +141,7 @@ mod jest { pub mod no_test_prefixes; pub mod no_test_return_statement; pub mod prefer_called_with; + pub mod prefer_equality_matcher; pub mod prefer_todo; pub mod valid_describe_callback; pub mod valid_expect; @@ -428,6 +429,7 @@ oxc_macros::declare_all_lint_rules! { jest::no_test_prefixes, jest::no_test_return_statement, jest::prefer_called_with, + jest::prefer_equality_matcher, jest::prefer_todo, jest::valid_describe_callback, jest::valid_expect, diff --git a/crates/oxc_linter/src/rules/jest/prefer_equality_matcher.rs b/crates/oxc_linter/src/rules/jest/prefer_equality_matcher.rs new file mode 100644 index 000000000..405c1a00e --- /dev/null +++ b/crates/oxc_linter/src/rules/jest/prefer_equality_matcher.rs @@ -0,0 +1,125 @@ +use crate::{ + context::LintContext, + rule::Rule, + utils::{collect_possible_jest_call_node, parse_expect_jest_fn_call, PossibleJestNode}, +}; + +use oxc_ast::{ + ast::{Argument, Expression}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use oxc_syntax::operator::BinaryOperator; + +#[derive(Debug, Error, Diagnostic)] +#[error( + "eslint-plugin-jest(prefer-equality-matcher): Suggest using the built-in equality matchers." +)] +#[diagnostic(severity(warning), help("Prefer using one of the equality matchers instead"))] +struct UseEqualityMatcherDiagnostic(#[label] Span); + +#[derive(Debug, Default, Clone)] +pub struct PreferEqualityMatcher; + +declare_oxc_lint!( + /// ### What it does + /// Jest has built-in matchers for expecting equality, which allow for more readable + /// tests and error messages if an expectation fails. + /// + /// ### Example + /// + /// ```javascript + /// // invalid + /// expect(x === 5).toBe(true); + /// expect(name === 'Carl').not.toEqual(true); + /// expect(myObj !== thatObj).toStrictEqual(true); + /// + /// // valid + /// expect(x).toBe(5); + /// expect(name).not.toEqual('Carl'); + /// expect(myObj).toStrictEqual(thatObj); + /// ``` + PreferEqualityMatcher, + style, +); + +impl Rule for PreferEqualityMatcher { + fn run_once(&self, ctx: &LintContext) { + for possible_jest_node in &collect_possible_jest_call_node(ctx) { + Self::run(possible_jest_node, ctx); + } + } +} + +impl PreferEqualityMatcher { + pub 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_fn_call) = parse_expect_jest_fn_call(call_expr, possible_jest_node, ctx) + else { + return; + }; + + let Some(expect_parent) = jest_fn_call.head.parent else { + return; + }; + let expr = expect_parent.get_inner_expression(); + let Expression::CallExpression(call_expr) = expr else { + return; + }; + let Some(argument) = call_expr.arguments.first() else { + return; + }; + + let Argument::Expression(Expression::BinaryExpression(binary_expr)) = argument else { + return; + }; + + if binary_expr.operator != BinaryOperator::StrictEquality + && binary_expr.operator != BinaryOperator::StrictInequality + { + return; + } + + let Some(matcher) = jest_fn_call.matcher() else { + return; + }; + + ctx.diagnostic(UseEqualityMatcherDiagnostic(matcher.span)); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("expect.hasAssertions", None), + ("expect.hasAssertions()", None), + ("expect.assertions(1)", None), + ("expect(true).toBe(...true)", None), + ("expect(a == 1).toBe(true)", None), + ("expect(1 == a).toBe(true)", None), + ("expect(a == b).toBe(true)", None), + ]; + + let fail = vec![ + ("expect(a !== b).toBe(true)", None), + ("expect(a !== b).toBe(false)", None), + ("expect(a !== b).resolves.toBe(true)", None), + ("expect(a !== b).resolves.toBe(false)", None), + ("expect(a !== b).not.toBe(true)", None), + ("expect(a !== b).not.toBe(false)", None), + ("expect(a !== b).resolves.not.toBe(true)", None), + ("expect(a !== b).resolves.not.toBe(false)", None), + ]; + + Tester::new(PreferEqualityMatcher::NAME, pass, fail).with_jest_plugin(true).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_equality_matcher.snap b/crates/oxc_linter/src/snapshots/prefer_equality_matcher.snap new file mode 100644 index 000000000..14b2ae728 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_equality_matcher.snap @@ -0,0 +1,62 @@ +--- +source: crates/oxc_linter/src/tester.rs +assertion_line: 150 +expression: prefer_equality_matcher +--- + + ⚠ eslint-plugin-jest(prefer-equality-matcher): Suggest using the built-in equality matchers. + ╭─[prefer_equality_matcher.tsx:1:17] + 1 │ expect(a !== b).toBe(true) + · ──── + ╰──── + help: Prefer using one of the equality matchers instead + + ⚠ eslint-plugin-jest(prefer-equality-matcher): Suggest using the built-in equality matchers. + ╭─[prefer_equality_matcher.tsx:1:17] + 1 │ expect(a !== b).toBe(false) + · ──── + ╰──── + help: Prefer using one of the equality matchers instead + + ⚠ eslint-plugin-jest(prefer-equality-matcher): Suggest using the built-in equality matchers. + ╭─[prefer_equality_matcher.tsx:1:26] + 1 │ expect(a !== b).resolves.toBe(true) + · ──── + ╰──── + help: Prefer using one of the equality matchers instead + + ⚠ eslint-plugin-jest(prefer-equality-matcher): Suggest using the built-in equality matchers. + ╭─[prefer_equality_matcher.tsx:1:26] + 1 │ expect(a !== b).resolves.toBe(false) + · ──── + ╰──── + help: Prefer using one of the equality matchers instead + + ⚠ eslint-plugin-jest(prefer-equality-matcher): Suggest using the built-in equality matchers. + ╭─[prefer_equality_matcher.tsx:1:21] + 1 │ expect(a !== b).not.toBe(true) + · ──── + ╰──── + help: Prefer using one of the equality matchers instead + + ⚠ eslint-plugin-jest(prefer-equality-matcher): Suggest using the built-in equality matchers. + ╭─[prefer_equality_matcher.tsx:1:21] + 1 │ expect(a !== b).not.toBe(false) + · ──── + ╰──── + help: Prefer using one of the equality matchers instead + + ⚠ eslint-plugin-jest(prefer-equality-matcher): Suggest using the built-in equality matchers. + ╭─[prefer_equality_matcher.tsx:1:30] + 1 │ expect(a !== b).resolves.not.toBe(true) + · ──── + ╰──── + help: Prefer using one of the equality matchers instead + + ⚠ eslint-plugin-jest(prefer-equality-matcher): Suggest using the built-in equality matchers. + ╭─[prefer_equality_matcher.tsx:1:30] + 1 │ expect(a !== b).resolves.not.toBe(false) + · ──── + ╰──── + help: Prefer using one of the equality matchers instead + diff --git a/crates/oxc_linter/src/utils/jest.rs b/crates/oxc_linter/src/utils/jest.rs index cf043f815..a50885f56 100644 --- a/crates/oxc_linter/src/utils/jest.rs +++ b/crates/oxc_linter/src/utils/jest.rs @@ -37,7 +37,7 @@ const JEST_METHOD_NAMES: phf::Set<&'static str> = phf_set![ "pending" ]; -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum JestFnKind { Expect, General(JestGeneralFnKind),