|
| 1 | +use oxc_ast::{ |
| 2 | + ast::{Argument, Expression}, |
| 3 | + AstKind, |
| 4 | +}; |
| 5 | +use oxc_diagnostics::OxcDiagnostic; |
| 6 | +use oxc_macros::declare_oxc_lint; |
| 7 | +use oxc_span::Span; |
| 8 | + |
| 9 | +use crate::{ |
| 10 | + context::LintContext, |
| 11 | + rule::Rule, |
| 12 | + utils::{ |
| 13 | + collect_possible_jest_call_node, is_equality_matcher, |
| 14 | + parse_expect_and_typeof_vitest_fn_call, PossibleJestNode, |
| 15 | + }, |
| 16 | +}; |
| 17 | + |
| 18 | +fn use_to_be_truthy(span0: Span) -> OxcDiagnostic { |
| 19 | + OxcDiagnostic::warn("Use `toBeTruthy` instead.").with_label(span0) |
| 20 | +} |
| 21 | + |
| 22 | +#[derive(Debug, Default, Clone)] |
| 23 | +pub struct PreferToBeTruthy; |
| 24 | + |
| 25 | +declare_oxc_lint!( |
| 26 | + /// ### What it does |
| 27 | + /// |
| 28 | + /// This rule warns when `toBe(true)` is used with `expect` or `expectTypeOf`. With `--fix`, it will be replaced with `toBeTruthy()`. |
| 29 | + /// |
| 30 | + /// ### Examples |
| 31 | + /// |
| 32 | + /// ```javascript |
| 33 | + /// // bad |
| 34 | + /// expect(foo).toBe(true) |
| 35 | + /// expectTypeOf(foo).toBe(true) |
| 36 | + /// |
| 37 | + /// // good |
| 38 | + /// expect(foo).toBeTruthy() |
| 39 | + /// expectTypeOf(foo).toBeTruthy() |
| 40 | + /// ``` |
| 41 | + PreferToBeTruthy, |
| 42 | + style, |
| 43 | + fix |
| 44 | +); |
| 45 | + |
| 46 | +impl Rule for PreferToBeTruthy { |
| 47 | + fn run_once(&self, ctx: &LintContext) { |
| 48 | + for possible_vitest_node in &collect_possible_jest_call_node(ctx) { |
| 49 | + Self::run(possible_vitest_node, ctx); |
| 50 | + } |
| 51 | + } |
| 52 | +} |
| 53 | + |
| 54 | +impl PreferToBeTruthy { |
| 55 | + fn run<'a>(possible_vitest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) { |
| 56 | + let node = possible_vitest_node.node; |
| 57 | + let AstKind::CallExpression(call_expr) = node.kind() else { |
| 58 | + return; |
| 59 | + }; |
| 60 | + let Some(vitest_expect_fn_call) = |
| 61 | + parse_expect_and_typeof_vitest_fn_call(call_expr, possible_vitest_node, ctx) |
| 62 | + else { |
| 63 | + return; |
| 64 | + }; |
| 65 | + let Some(matcher) = vitest_expect_fn_call.matcher() else { |
| 66 | + return; |
| 67 | + }; |
| 68 | + |
| 69 | + if !is_equality_matcher(matcher) || vitest_expect_fn_call.args.len() == 0 { |
| 70 | + return; |
| 71 | + } |
| 72 | + |
| 73 | + let Some(arg_expr) = vitest_expect_fn_call.args.first().and_then(Argument::as_expression) |
| 74 | + else { |
| 75 | + return; |
| 76 | + }; |
| 77 | + |
| 78 | + if let Expression::BooleanLiteral(arg) = arg_expr.get_inner_expression() { |
| 79 | + if arg.value { |
| 80 | + let span = Span::new(matcher.span.start, call_expr.span.end); |
| 81 | + |
| 82 | + let is_cmp_mem_expr = match matcher.parent { |
| 83 | + Some(Expression::ComputedMemberExpression(_)) => true, |
| 84 | + Some( |
| 85 | + Expression::StaticMemberExpression(_) |
| 86 | + | Expression::PrivateFieldExpression(_), |
| 87 | + ) => false, |
| 88 | + _ => return, |
| 89 | + }; |
| 90 | + |
| 91 | + ctx.diagnostic_with_fix(use_to_be_truthy(span), |fixer| { |
| 92 | + let new_matcher = |
| 93 | + if is_cmp_mem_expr { "[\"toBeTruthy\"]()" } else { "toBeTruthy()" }; |
| 94 | + |
| 95 | + fixer.replace(span, new_matcher) |
| 96 | + }); |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +#[test] |
| 103 | +fn test() { |
| 104 | + use crate::tester::Tester; |
| 105 | + |
| 106 | + let pass = vec![ |
| 107 | + "[].push(true)", |
| 108 | + r#"expect("something");"#, |
| 109 | + "expect(true).toBeTrue();", |
| 110 | + "expect(false).toBeTrue();", |
| 111 | + "expect(fal,se).toBeFalse();", |
| 112 | + "expect(true).toBeFalse();", |
| 113 | + "expect(value).toEqual();", |
| 114 | + "expect(value).not.toBeTrue();", |
| 115 | + "expect(value).not.toEqual();", |
| 116 | + "expect(value).toBe(undefined);", |
| 117 | + "expect(value).not.toBe(undefined);", |
| 118 | + "expect(true).toBe(false)", |
| 119 | + "expect(value).toBe();", |
| 120 | + "expect(true).toMatchSnapshot();", |
| 121 | + r#"expect("a string").toMatchSnapshot(true);"#, |
| 122 | + r#"expect("a string").not.toMatchSnapshot();"#, |
| 123 | + "expect(something).toEqual('a string');", |
| 124 | + "expect(true).toBe", |
| 125 | + "expectTypeOf(true).toBe()", |
| 126 | + ]; |
| 127 | + |
| 128 | + let fail = vec![ |
| 129 | + "expect(false).toBe(true);", |
| 130 | + "expectTypeOf(false).toBe(true);", |
| 131 | + "expect(wasSuccessful).toEqual(true);", |
| 132 | + "expect(fs.existsSync('/path/to/file')).toStrictEqual(true);", |
| 133 | + r#"expect("a string").not.toBe(true);"#, |
| 134 | + r#"expect("a string").not.toEqual(true);"#, |
| 135 | + r#"expectTypeOf("a string").not.toStrictEqual(true);"#, |
| 136 | + ]; |
| 137 | + |
| 138 | + let fix = vec![ |
| 139 | + ("expect(false).toBe(true);", "expect(false).toBeTruthy();", None), |
| 140 | + ("expectTypeOf(false).toBe(true);", "expectTypeOf(false).toBeTruthy();", None), |
| 141 | + ("expect(wasSuccessful).toEqual(true);", "expect(wasSuccessful).toBeTruthy();", None), |
| 142 | + ( |
| 143 | + "expect(fs.existsSync('/path/to/file')).toStrictEqual(true);", |
| 144 | + "expect(fs.existsSync('/path/to/file')).toBeTruthy();", |
| 145 | + None, |
| 146 | + ), |
| 147 | + (r#"expect("a string").not.toBe(true);"#, r#"expect("a string").not.toBeTruthy();"#, None), |
| 148 | + ( |
| 149 | + r#"expect("a string").not.toEqual(true);"#, |
| 150 | + r#"expect("a string").not.toBeTruthy();"#, |
| 151 | + None, |
| 152 | + ), |
| 153 | + ( |
| 154 | + r#"expectTypeOf("a string").not.toStrictEqual(true);"#, |
| 155 | + r#"expectTypeOf("a string").not.toBeTruthy();"#, |
| 156 | + None, |
| 157 | + ), |
| 158 | + ]; |
| 159 | + Tester::new(PreferToBeTruthy::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); |
| 160 | +} |
0 commit comments