diff --git a/crates/oxc_linter/src/rules/jest/prefer_equality_matcher.rs b/crates/oxc_linter/src/rules/jest/prefer_equality_matcher.rs index dfd34df48388b..1394dce57b6de 100644 --- a/crates/oxc_linter/src/rules/jest/prefer_equality_matcher.rs +++ b/crates/oxc_linter/src/rules/jest/prefer_equality_matcher.rs @@ -1,6 +1,6 @@ use oxc_ast::{ AstKind, - ast::{Argument, Expression}, + ast::{Argument, BinaryExpression, Expression}, }; use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; @@ -9,8 +9,12 @@ use oxc_syntax::operator::BinaryOperator; use crate::{ context::LintContext, + fixer::RuleFixer, rule::Rule, - utils::{PossibleJestNode, parse_expect_jest_fn_call}, + utils::{ + KnownMemberExpressionProperty, PossibleJestNode, is_equality_matcher, + parse_expect_jest_fn_call, + }, }; fn use_equality_matcher_diagnostic(span: Span) -> OxcDiagnostic { @@ -65,7 +69,7 @@ declare_oxc_lint!( PreferEqualityMatcher, jest, style, - pending, + suggestion ); impl Rule for PreferEqualityMatcher { @@ -81,10 +85,11 @@ impl Rule for PreferEqualityMatcher { 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 { + let AstKind::CallExpression(matcher_call_expr) = node.kind() else { return; }; - let Some(jest_fn_call) = parse_expect_jest_fn_call(call_expr, possible_jest_node, ctx) + let Some(jest_fn_call) = + parse_expect_jest_fn_call(matcher_call_expr, possible_jest_node, ctx) else { return; }; @@ -93,10 +98,10 @@ impl PreferEqualityMatcher { return; }; let expr = expect_parent.get_inner_expression(); - let Expression::CallExpression(call_expr) = expr else { + let Expression::CallExpression(expect_call_expr) = expr else { return; }; - let Some(argument) = call_expr.arguments.first() else { + let Some(argument) = expect_call_expr.arguments.first() else { return; }; @@ -114,13 +119,91 @@ impl PreferEqualityMatcher { return; }; - ctx.diagnostic(use_equality_matcher_diagnostic(matcher.span)); + if !is_equality_matcher(matcher) { + return; + } + let Some(first_matcher_arg) = jest_fn_call.args.first().and_then(Argument::as_expression) + else { + return; + }; + let Expression::BooleanLiteral(matcher_arg_value) = + first_matcher_arg.get_inner_expression() + else { + return; + }; + + let modifiers = jest_fn_call.modifiers(); + let has_not_modifier = modifiers.iter().any(|modifier| modifier.is_name_equal("not")); + let add_not_modifier = (if binary_expr.operator == BinaryOperator::StrictInequality { + !matcher_arg_value.value + } else { + matcher_arg_value.value + }) == has_not_modifier; + + let fixer = RuleFixer::new(FixKind::Suggestion, ctx); + let suggestions = ["toBe", "toEqual", "toStrictEqual"].into_iter().map(|eq_matcher| { + // Preserve trailing commas: expect(a === b,).toBe(true,) -> expect(a,).toBe(b,) + let call_span_end = + fixer.source_range(Span::new(binary_expr.span.end, expect_call_expr.span.end)); + let arg_span_end = fixer + .source_range(Span::new(matcher_arg_value.span.end, matcher_call_expr.span.end)); + let content = Self::build_code( + binary_expr, + call_span_end, + arg_span_end, + &jest_fn_call.local, + &modifiers, + eq_matcher, + add_not_modifier, + fixer, + ); + fixer + .replace(matcher_call_expr.span, content) + .with_message(format!("Use `{eq_matcher}`")) + }); + + ctx.diagnostic_with_suggestions(use_equality_matcher_diagnostic(matcher.span), suggestions); + } + + fn build_code<'a>( + binary_expr: &BinaryExpression<'a>, + call_span_end: &str, + arg_span_end: &str, + local_name: &str, + modifiers: &[&KnownMemberExpressionProperty<'a>], + equality_matcher: &str, + add_not_modifier: bool, + fixer: RuleFixer<'_, 'a>, + ) -> String { + let mut content = fixer.codegen(); + content.print_str(local_name); + content.print_ascii_byte(b'('); + content.print_expression(&binary_expr.left); + content.print_str(call_span_end); + content.print_ascii_byte(b'.'); + for modifier in modifiers { + let Some(modifier_name) = modifier.name() else { + continue; + }; + if modifier_name != "not" { + content.print_str(&modifier_name); + content.print_ascii_byte(b'.'); + } + } + if add_not_modifier { + content.print_str("not."); + } + content.print_str(equality_matcher); + content.print_ascii_byte(b'('); + content.print_expression(&binary_expr.right); + content.print_str(arg_span_end); + content.into_source_text() } } #[test] fn test() { - use crate::tester::Tester; + use crate::tester::{ExpectFixTestCase, Tester}; let mut pass = vec![ ("expect.hasAssertions", None), @@ -182,10 +265,166 @@ fn test() { ("expect(a !== b).resolves.not.toBe(false);", None), ]; + let fix: Vec = vec![ + ( + "expect(a === b).toBe(true);", + ("expect(a).toBe(b);", "expect(a).toEqual(b);", "expect(a).toStrictEqual(b);"), + ) + .into(), + ( + "expect(a === b,).toBe(true,);", + ("expect(a,).toBe(b,);", "expect(a,).toEqual(b,);", "expect(a,).toStrictEqual(b,);"), + ) + .into(), + ( + "expect(a === b).toBe(false);", + ( + "expect(a).not.toBe(b);", + "expect(a).not.toEqual(b);", + "expect(a).not.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a === b).resolves.toBe(true);", + ( + "expect(a).resolves.toBe(b);", + "expect(a).resolves.toEqual(b);", + "expect(a).resolves.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a === b).resolves.toBe(false);", + ( + "expect(a).resolves.not.toBe(b);", + "expect(a).resolves.not.toEqual(b);", + "expect(a).resolves.not.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a === b).not.toBe(true);", + ( + "expect(a).not.toBe(b);", + "expect(a).not.toEqual(b);", + "expect(a).not.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a === b).not.toBe(false);", + ("expect(a).toBe(b);", "expect(a).toEqual(b);", "expect(a).toStrictEqual(b);"), + ) + .into(), + ( + "expect(a === b).resolves.not.toBe(true);", + ( + "expect(a).resolves.not.toBe(b);", + "expect(a).resolves.not.toEqual(b);", + "expect(a).resolves.not.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a === b).resolves.not.toBe(false);", + ( + "expect(a).resolves.toBe(b);", + "expect(a).resolves.toEqual(b);", + "expect(a).resolves.toStrictEqual(b);", + ), + ) + .into(), + ( + r#"expect(a === b)["resolves"].not.toBe(false);"#, + ( + "expect(a).resolves.toBe(b);", + "expect(a).resolves.toEqual(b);", + "expect(a).resolves.toStrictEqual(b);", + ), + ) + .into(), + ( + r#"expect(a === b)["resolves"]["not"]["toBe"](false);"#, + ( + "expect(a).resolves.toBe(b);", + "expect(a).resolves.toEqual(b);", + "expect(a).resolves.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a !== b).toBe(true);", + ( + "expect(a).not.toBe(b);", + "expect(a).not.toEqual(b);", + "expect(a).not.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a !== b).toBe(false);", + ("expect(a).toBe(b);", "expect(a).toEqual(b);", "expect(a).toStrictEqual(b);"), + ) + .into(), + ( + "expect(a !== b).resolves.toBe(true);", + ( + "expect(a).resolves.not.toBe(b);", + "expect(a).resolves.not.toEqual(b);", + "expect(a).resolves.not.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a !== b).resolves.toBe(false);", + ( + "expect(a).resolves.toBe(b);", + "expect(a).resolves.toEqual(b);", + "expect(a).resolves.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a !== b).not.toBe(true);", + ("expect(a).toBe(b);", "expect(a).toEqual(b);", "expect(a).toStrictEqual(b);"), + ) + .into(), + ( + "expect(a !== b).not.toBe(false);", + ( + "expect(a).not.toBe(b);", + "expect(a).not.toEqual(b);", + "expect(a).not.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a !== b).resolves.not.toBe(true);", + ( + "expect(a).resolves.toBe(b);", + "expect(a).resolves.toEqual(b);", + "expect(a).resolves.toStrictEqual(b);", + ), + ) + .into(), + ( + "expect(a !== b).resolves.not.toBe(false);", + ( + "expect(a).resolves.not.toBe(b);", + "expect(a).resolves.not.toEqual(b);", + "expect(a).resolves.not.toStrictEqual(b);", + ), + ) + .into(), + ]; + pass.extend(pass_vitest); fail.extend(fail_vitest); Tester::new(PreferEqualityMatcher::NAME, PreferEqualityMatcher::PLUGIN, pass, fail) .with_jest_plugin(true) + .with_vitest_plugin(true) + .expect_fix(fix) .test_and_snapshot(); }