Skip to content

Commit 41f861f

Browse files
authored
feat(linter/eslint-plugin-vitest): implement prefer-to-be-truthy (#4755)
Related to #4656
1 parent b20e335 commit 41f861f

13 files changed

+264
-19
lines changed

crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ mod promise {
449449

450450
mod vitest {
451451
pub mod no_import_node_test;
452+
pub mod prefer_to_be_truthy;
452453
}
453454

454455
oxc_macros::declare_all_lint_rules! {
@@ -854,4 +855,5 @@ oxc_macros::declare_all_lint_rules! {
854855
promise::no_new_statics,
855856
promise::param_names,
856857
vitest::no_import_node_test,
858+
vitest::prefer_to_be_truthy,
857859
}

crates/oxc_linter/src/rules/jest/consistent_test_it.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ impl ConsistentTestIt {
219219
let AstKind::CallExpression(call_expr) = node.kind() else {
220220
return;
221221
};
222-
let Some(ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call)) =
222+
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
223223
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
224224
else {
225225
return;

crates/oxc_linter/src/rules/jest/no_disabled_tests.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>)
104104
let ParsedGeneralJestFnCall { kind, members, name, .. } = jest_fn_call;
105105
// `test('foo')`
106106
let kind = match kind {
107-
JestFnKind::Expect | JestFnKind::Unknown => return,
107+
JestFnKind::Expect | JestFnKind::ExpectTypeOf | JestFnKind::Unknown => return,
108108
JestFnKind::General(kind) => kind,
109109
};
110110
if matches!(kind, JestGeneralFnKind::Test)

crates/oxc_linter/src/rules/jest/no_duplicate_hooks.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ impl NoDuplicateHooks {
127127
let AstKind::CallExpression(call_expr) = node.kind() else {
128128
return;
129129
};
130-
let Some(ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call)) =
130+
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
131131
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
132132
else {
133133
return;

crates/oxc_linter/src/rules/jest/prefer_hooks_in_order.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ impl PreferHooksInOrder {
166166
call_expr: &'a CallExpression<'_>,
167167
ctx: &LintContext<'a>,
168168
) {
169-
let Some(ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call)) =
169+
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
170170
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
171171
else {
172172
*previous_hook_index = -1;

crates/oxc_linter/src/rules/jest/prefer_lowercase_title.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ impl PreferLowercaseTitle {
174174
let AstKind::CallExpression(call_expr) = node.kind() else {
175175
return;
176176
};
177-
let Some(ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call)) =
177+
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
178178
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
179179
else {
180180
return;

crates/oxc_linter/src/rules/jest/require_top_level_describe.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ impl RequireTopLevelDescribe {
144144
return;
145145
};
146146

147-
let Some(ParsedJestFnCallNew::GeneralJestFnCall(ParsedGeneralJestFnCall { kind, .. })) =
147+
let Some(ParsedJestFnCallNew::GeneralJest(ParsedGeneralJestFnCall { kind, .. })) =
148148
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
149149
else {
150150
return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
5+
╭─[prefer_to_be_truthy.tsx:1:15]
6+
1expect(false).toBe(true);
7+
· ──────────
8+
╰────
9+
help: Replace `toBe(true)` with `toBeTruthy()`.
10+
11+
eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
12+
╭─[prefer_to_be_truthy.tsx:1:21]
13+
1expectTypeOf(false).toBe(true);
14+
· ──────────
15+
╰────
16+
help: Replace `toBe(true)` with `toBeTruthy()`.
17+
18+
eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
19+
╭─[prefer_to_be_truthy.tsx:1:23]
20+
1expect(wasSuccessful).toEqual(true);
21+
· ─────────────
22+
╰────
23+
help: Replace `toEqual(true)` with `toBeTruthy()`.
24+
25+
eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
26+
╭─[prefer_to_be_truthy.tsx:1:40]
27+
1expect(fs.existsSync('/path/to/file')).toStrictEqual(true);
28+
· ───────────────────
29+
╰────
30+
help: Replace `toStrictEqual(true)` with `toBeTruthy()`.
31+
32+
eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
33+
╭─[prefer_to_be_truthy.tsx:1:24]
34+
1expect("a string").not.toBe(true);
35+
· ──────────
36+
╰────
37+
help: Replace `toBe(true)` with `toBeTruthy()`.
38+
39+
eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
40+
╭─[prefer_to_be_truthy.tsx:1:24]
41+
1expect("a string").not.toEqual(true);
42+
· ─────────────
43+
╰────
44+
help: Replace `toEqual(true)` with `toBeTruthy()`.
45+
46+
eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
47+
╭─[prefer_to_be_truthy.tsx:1:30]
48+
1expectTypeOf("a string").not.toStrictEqual(true);
49+
· ───────────────────
50+
╰────
51+
help: Replace `toStrictEqual(true)` with `toBeTruthy()`.

crates/oxc_linter/src/utils/jest.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub const JEST_METHOD_NAMES: phf::Set<&'static str> = phf_set![
2626
"beforeEach",
2727
"describe",
2828
"expect",
29+
"expectTypeOf",
2930
"fdescribe",
3031
"fit",
3132
"it",
@@ -40,6 +41,7 @@ pub const JEST_METHOD_NAMES: phf::Set<&'static str> = phf_set![
4041
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
4142
pub enum JestFnKind {
4243
Expect,
44+
ExpectTypeOf,
4345
General(JestGeneralFnKind),
4446
Unknown,
4547
}
@@ -48,6 +50,7 @@ impl JestFnKind {
4850
pub fn from(name: &str) -> Self {
4951
match name {
5052
"expect" => Self::Expect,
53+
"expectTypeOf" => Self::ExpectTypeOf,
5154
"jest" => Self::General(JestGeneralFnKind::Jest),
5255
"describe" | "fdescribe" | "xdescribe" => Self::General(JestGeneralFnKind::Describe),
5356
"fit" | "it" | "test" | "xit" | "xtest" => Self::General(JestGeneralFnKind::Test),
@@ -113,7 +116,7 @@ pub fn parse_general_jest_fn_call<'a>(
113116
) -> Option<ParsedGeneralJestFnCall<'a>> {
114117
let jest_fn_call = parse_jest_fn_call(call_expr, possible_jest_node, ctx)?;
115118

116-
if let ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call) = jest_fn_call {
119+
if let ParsedJestFnCallNew::GeneralJest(jest_fn_call) = jest_fn_call {
117120
return Some(jest_fn_call);
118121
}
119122
None
@@ -126,7 +129,7 @@ pub fn parse_expect_jest_fn_call<'a>(
126129
) -> Option<ParsedExpectFnCall<'a>> {
127130
let jest_fn_call = parse_jest_fn_call(call_expr, possible_jest_node, ctx)?;
128131

129-
if let ParsedJestFnCallNew::ExpectFnCall(jest_fn_call) = jest_fn_call {
132+
if let ParsedJestFnCallNew::Expect(jest_fn_call) = jest_fn_call {
130133
return Some(jest_fn_call);
131134
}
132135
None

0 commit comments

Comments
 (0)