mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(linter/eslint-plugin-vitest): implement prefer-to-be-truthy (#4755)
Related to #4656
This commit is contained in:
parent
b20e3351fb
commit
41f861fd15
13 changed files with 264 additions and 19 deletions
|
|
@ -449,6 +449,7 @@ mod promise {
|
|||
|
||||
mod vitest {
|
||||
pub mod no_import_node_test;
|
||||
pub mod prefer_to_be_truthy;
|
||||
}
|
||||
|
||||
oxc_macros::declare_all_lint_rules! {
|
||||
|
|
@ -854,4 +855,5 @@ oxc_macros::declare_all_lint_rules! {
|
|||
promise::no_new_statics,
|
||||
promise::param_names,
|
||||
vitest::no_import_node_test,
|
||||
vitest::prefer_to_be_truthy,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ impl ConsistentTestIt {
|
|||
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
let Some(ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call)) =
|
||||
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
|
||||
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
|
||||
else {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>)
|
|||
let ParsedGeneralJestFnCall { kind, members, name, .. } = jest_fn_call;
|
||||
// `test('foo')`
|
||||
let kind = match kind {
|
||||
JestFnKind::Expect | JestFnKind::Unknown => return,
|
||||
JestFnKind::Expect | JestFnKind::ExpectTypeOf | JestFnKind::Unknown => return,
|
||||
JestFnKind::General(kind) => kind,
|
||||
};
|
||||
if matches!(kind, JestGeneralFnKind::Test)
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ impl NoDuplicateHooks {
|
|||
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
let Some(ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call)) =
|
||||
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
|
||||
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
|
||||
else {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ impl PreferHooksInOrder {
|
|||
call_expr: &'a CallExpression<'_>,
|
||||
ctx: &LintContext<'a>,
|
||||
) {
|
||||
let Some(ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call)) =
|
||||
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
|
||||
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
|
||||
else {
|
||||
*previous_hook_index = -1;
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ impl PreferLowercaseTitle {
|
|||
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
let Some(ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call)) =
|
||||
let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) =
|
||||
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
|
||||
else {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ impl RequireTopLevelDescribe {
|
|||
return;
|
||||
};
|
||||
|
||||
let Some(ParsedJestFnCallNew::GeneralJestFnCall(ParsedGeneralJestFnCall { kind, .. })) =
|
||||
let Some(ParsedJestFnCallNew::GeneralJest(ParsedGeneralJestFnCall { kind, .. })) =
|
||||
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
|
||||
else {
|
||||
return;
|
||||
|
|
|
|||
160
crates/oxc_linter/src/rules/vitest/prefer_to_be_truthy.rs
Normal file
160
crates/oxc_linter/src/rules/vitest/prefer_to_be_truthy.rs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
use oxc_ast::{
|
||||
ast::{Argument, Expression},
|
||||
AstKind,
|
||||
};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
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_and_typeof_vitest_fn_call, PossibleJestNode,
|
||||
},
|
||||
};
|
||||
|
||||
fn use_to_be_truthy(span0: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn("Use `toBeTruthy` instead.").with_label(span0)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PreferToBeTruthy;
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// This rule warns when `toBe(true)` is used with `expect` or `expectTypeOf`. With `--fix`, it will be replaced with `toBeTruthy()`.
|
||||
///
|
||||
/// ### Examples
|
||||
///
|
||||
/// ```javascript
|
||||
/// // bad
|
||||
/// expect(foo).toBe(true)
|
||||
/// expectTypeOf(foo).toBe(true)
|
||||
///
|
||||
/// // good
|
||||
/// expect(foo).toBeTruthy()
|
||||
/// expectTypeOf(foo).toBeTruthy()
|
||||
/// ```
|
||||
PreferToBeTruthy,
|
||||
style,
|
||||
fix
|
||||
);
|
||||
|
||||
impl Rule for PreferToBeTruthy {
|
||||
fn run_once(&self, ctx: &LintContext) {
|
||||
for possible_vitest_node in &collect_possible_jest_call_node(ctx) {
|
||||
Self::run(possible_vitest_node, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PreferToBeTruthy {
|
||||
fn run<'a>(possible_vitest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) {
|
||||
let node = possible_vitest_node.node;
|
||||
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
let Some(vitest_expect_fn_call) =
|
||||
parse_expect_and_typeof_vitest_fn_call(call_expr, possible_vitest_node, ctx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(matcher) = vitest_expect_fn_call.matcher() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !is_equality_matcher(matcher) || vitest_expect_fn_call.args.len() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(arg_expr) = vitest_expect_fn_call.args.first().and_then(Argument::as_expression)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Expression::BooleanLiteral(arg) = arg_expr.get_inner_expression() {
|
||||
if arg.value {
|
||||
let span = Span::new(matcher.span.start, call_expr.span.end);
|
||||
|
||||
let is_cmp_mem_expr = match matcher.parent {
|
||||
Some(Expression::ComputedMemberExpression(_)) => true,
|
||||
Some(
|
||||
Expression::StaticMemberExpression(_)
|
||||
| Expression::PrivateFieldExpression(_),
|
||||
) => false,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
ctx.diagnostic_with_fix(use_to_be_truthy(span), |fixer| {
|
||||
let new_matcher =
|
||||
if is_cmp_mem_expr { "[\"toBeTruthy\"]()" } else { "toBeTruthy()" };
|
||||
|
||||
fixer.replace(span, new_matcher)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
"[].push(true)",
|
||||
r#"expect("something");"#,
|
||||
"expect(true).toBeTrue();",
|
||||
"expect(false).toBeTrue();",
|
||||
"expect(fal,se).toBeFalse();",
|
||||
"expect(true).toBeFalse();",
|
||||
"expect(value).toEqual();",
|
||||
"expect(value).not.toBeTrue();",
|
||||
"expect(value).not.toEqual();",
|
||||
"expect(value).toBe(undefined);",
|
||||
"expect(value).not.toBe(undefined);",
|
||||
"expect(true).toBe(false)",
|
||||
"expect(value).toBe();",
|
||||
"expect(true).toMatchSnapshot();",
|
||||
r#"expect("a string").toMatchSnapshot(true);"#,
|
||||
r#"expect("a string").not.toMatchSnapshot();"#,
|
||||
"expect(something).toEqual('a string');",
|
||||
"expect(true).toBe",
|
||||
"expectTypeOf(true).toBe()",
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
"expect(false).toBe(true);",
|
||||
"expectTypeOf(false).toBe(true);",
|
||||
"expect(wasSuccessful).toEqual(true);",
|
||||
"expect(fs.existsSync('/path/to/file')).toStrictEqual(true);",
|
||||
r#"expect("a string").not.toBe(true);"#,
|
||||
r#"expect("a string").not.toEqual(true);"#,
|
||||
r#"expectTypeOf("a string").not.toStrictEqual(true);"#,
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
("expect(false).toBe(true);", "expect(false).toBeTruthy();", None),
|
||||
("expectTypeOf(false).toBe(true);", "expectTypeOf(false).toBeTruthy();", None),
|
||||
("expect(wasSuccessful).toEqual(true);", "expect(wasSuccessful).toBeTruthy();", None),
|
||||
(
|
||||
"expect(fs.existsSync('/path/to/file')).toStrictEqual(true);",
|
||||
"expect(fs.existsSync('/path/to/file')).toBeTruthy();",
|
||||
None,
|
||||
),
|
||||
(r#"expect("a string").not.toBe(true);"#, r#"expect("a string").not.toBeTruthy();"#, None),
|
||||
(
|
||||
r#"expect("a string").not.toEqual(true);"#,
|
||||
r#"expect("a string").not.toBeTruthy();"#,
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"expectTypeOf("a string").not.toStrictEqual(true);"#,
|
||||
r#"expectTypeOf("a string").not.toBeTruthy();"#,
|
||||
None,
|
||||
),
|
||||
];
|
||||
Tester::new(PreferToBeTruthy::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
|
||||
}
|
||||
51
crates/oxc_linter/src/snapshots/prefer_to_be_truthy.snap
Normal file
51
crates/oxc_linter/src/snapshots/prefer_to_be_truthy.snap
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
⚠ eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
|
||||
╭─[prefer_to_be_truthy.tsx:1:15]
|
||||
1 │ expect(false).toBe(true);
|
||||
· ──────────
|
||||
╰────
|
||||
help: Replace `toBe(true)` with `toBeTruthy()`.
|
||||
|
||||
⚠ eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
|
||||
╭─[prefer_to_be_truthy.tsx:1:21]
|
||||
1 │ expectTypeOf(false).toBe(true);
|
||||
· ──────────
|
||||
╰────
|
||||
help: Replace `toBe(true)` with `toBeTruthy()`.
|
||||
|
||||
⚠ eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
|
||||
╭─[prefer_to_be_truthy.tsx:1:23]
|
||||
1 │ expect(wasSuccessful).toEqual(true);
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Replace `toEqual(true)` with `toBeTruthy()`.
|
||||
|
||||
⚠ eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
|
||||
╭─[prefer_to_be_truthy.tsx:1:40]
|
||||
1 │ expect(fs.existsSync('/path/to/file')).toStrictEqual(true);
|
||||
· ───────────────────
|
||||
╰────
|
||||
help: Replace `toStrictEqual(true)` with `toBeTruthy()`.
|
||||
|
||||
⚠ eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
|
||||
╭─[prefer_to_be_truthy.tsx:1:24]
|
||||
1 │ expect("a string").not.toBe(true);
|
||||
· ──────────
|
||||
╰────
|
||||
help: Replace `toBe(true)` with `toBeTruthy()`.
|
||||
|
||||
⚠ eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
|
||||
╭─[prefer_to_be_truthy.tsx:1:24]
|
||||
1 │ expect("a string").not.toEqual(true);
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Replace `toEqual(true)` with `toBeTruthy()`.
|
||||
|
||||
⚠ eslint-plugin-vitest(prefer-to-be-truthy): Use `toBeTruthy` instead.
|
||||
╭─[prefer_to_be_truthy.tsx:1:30]
|
||||
1 │ expectTypeOf("a string").not.toStrictEqual(true);
|
||||
· ───────────────────
|
||||
╰────
|
||||
help: Replace `toStrictEqual(true)` with `toBeTruthy()`.
|
||||
|
|
@ -26,6 +26,7 @@ pub const JEST_METHOD_NAMES: phf::Set<&'static str> = phf_set![
|
|||
"beforeEach",
|
||||
"describe",
|
||||
"expect",
|
||||
"expectTypeOf",
|
||||
"fdescribe",
|
||||
"fit",
|
||||
"it",
|
||||
|
|
@ -40,6 +41,7 @@ pub const JEST_METHOD_NAMES: phf::Set<&'static str> = phf_set![
|
|||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum JestFnKind {
|
||||
Expect,
|
||||
ExpectTypeOf,
|
||||
General(JestGeneralFnKind),
|
||||
Unknown,
|
||||
}
|
||||
|
|
@ -48,6 +50,7 @@ impl JestFnKind {
|
|||
pub fn from(name: &str) -> Self {
|
||||
match name {
|
||||
"expect" => Self::Expect,
|
||||
"expectTypeOf" => Self::ExpectTypeOf,
|
||||
"jest" => Self::General(JestGeneralFnKind::Jest),
|
||||
"describe" | "fdescribe" | "xdescribe" => Self::General(JestGeneralFnKind::Describe),
|
||||
"fit" | "it" | "test" | "xit" | "xtest" => Self::General(JestGeneralFnKind::Test),
|
||||
|
|
@ -113,7 +116,7 @@ pub fn parse_general_jest_fn_call<'a>(
|
|||
) -> Option<ParsedGeneralJestFnCall<'a>> {
|
||||
let jest_fn_call = parse_jest_fn_call(call_expr, possible_jest_node, ctx)?;
|
||||
|
||||
if let ParsedJestFnCallNew::GeneralJestFnCall(jest_fn_call) = jest_fn_call {
|
||||
if let ParsedJestFnCallNew::GeneralJest(jest_fn_call) = jest_fn_call {
|
||||
return Some(jest_fn_call);
|
||||
}
|
||||
None
|
||||
|
|
@ -126,7 +129,7 @@ pub fn parse_expect_jest_fn_call<'a>(
|
|||
) -> Option<ParsedExpectFnCall<'a>> {
|
||||
let jest_fn_call = parse_jest_fn_call(call_expr, possible_jest_node, ctx)?;
|
||||
|
||||
if let ParsedJestFnCallNew::ExpectFnCall(jest_fn_call) = jest_fn_call {
|
||||
if let ParsedJestFnCallNew::Expect(jest_fn_call) = jest_fn_call {
|
||||
return Some(jest_fn_call);
|
||||
}
|
||||
None
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ pub fn parse_jest_fn_call<'a>(
|
|||
let node = possible_jest_node.node;
|
||||
let callee = &call_expr.callee;
|
||||
// If bailed out, we're not jest function
|
||||
|
||||
let resolved = resolve_to_jest_fn(call_expr, original)?;
|
||||
|
||||
let params = NodeChainParams {
|
||||
|
|
@ -67,7 +68,7 @@ pub fn parse_jest_fn_call<'a>(
|
|||
members.push(member);
|
||||
}
|
||||
|
||||
if matches!(kind, JestFnKind::Expect) {
|
||||
if matches!(kind, JestFnKind::Expect | JestFnKind::ExpectTypeOf) {
|
||||
let options = ExpectFnCallOptions {
|
||||
call_expr,
|
||||
members,
|
||||
|
|
@ -77,7 +78,7 @@ pub fn parse_jest_fn_call<'a>(
|
|||
node,
|
||||
ctx,
|
||||
};
|
||||
return parse_jest_expect_fn_call(options);
|
||||
return parse_jest_expect_fn_call(options, matches!(kind, JestFnKind::ExpectTypeOf));
|
||||
}
|
||||
|
||||
// Ensure that we're at the "top" of the function call chain otherwise when
|
||||
|
|
@ -104,7 +105,7 @@ pub fn parse_jest_fn_call<'a>(
|
|||
return None;
|
||||
}
|
||||
|
||||
return Some(ParsedJestFnCall::GeneralJestFnCall(ParsedGeneralJestFnCall {
|
||||
return Some(ParsedJestFnCall::GeneralJest(ParsedGeneralJestFnCall {
|
||||
kind,
|
||||
members,
|
||||
name: Cow::Borrowed(name),
|
||||
|
|
@ -117,6 +118,7 @@ pub fn parse_jest_fn_call<'a>(
|
|||
|
||||
fn parse_jest_expect_fn_call<'a>(
|
||||
options: ExpectFnCallOptions<'a, '_>,
|
||||
is_type_of: bool,
|
||||
) -> Option<ParsedJestFnCall<'a>> {
|
||||
let ExpectFnCallOptions { call_expr, members, name, local, head, node, ctx } = options;
|
||||
let (modifiers, matcher, mut expect_error) = match find_modifiers_and_matcher(&members) {
|
||||
|
|
@ -137,7 +139,7 @@ fn parse_jest_expect_fn_call<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
return Some(ParsedJestFnCall::ExpectFnCall(ParsedExpectFnCall {
|
||||
let parsed_expect_fn = ParsedExpectFnCall {
|
||||
kind: JestFnKind::Expect,
|
||||
head,
|
||||
members,
|
||||
|
|
@ -147,7 +149,13 @@ fn parse_jest_expect_fn_call<'a>(
|
|||
matcher_index: matcher,
|
||||
modifier_indices: modifiers,
|
||||
expect_error,
|
||||
}));
|
||||
};
|
||||
|
||||
return Some(if is_type_of {
|
||||
ParsedJestFnCall::ExpectTypeOf(parsed_expect_fn)
|
||||
} else {
|
||||
ParsedJestFnCall::Expect(parsed_expect_fn)
|
||||
});
|
||||
}
|
||||
|
||||
type ModifiersAndMatcherIndex = (Vec<usize>, Option<usize>);
|
||||
|
|
@ -242,7 +250,7 @@ fn parse_jest_jest_fn_call<'a>(
|
|||
return None;
|
||||
}
|
||||
|
||||
return Some(ParsedJestFnCall::GeneralJestFnCall(ParsedGeneralJestFnCall {
|
||||
return Some(ParsedJestFnCall::GeneralJest(ParsedGeneralJestFnCall {
|
||||
kind: JestFnKind::General(JestGeneralFnKind::Jest),
|
||||
members,
|
||||
name: Cow::Borrowed(name),
|
||||
|
|
@ -307,15 +315,16 @@ fn resolve_first_ident<'a>(expr: &'a Expression<'a>) -> Option<&'a IdentifierRef
|
|||
}
|
||||
|
||||
pub enum ParsedJestFnCall<'a> {
|
||||
GeneralJestFnCall(ParsedGeneralJestFnCall<'a>),
|
||||
ExpectFnCall(ParsedExpectFnCall<'a>),
|
||||
GeneralJest(ParsedGeneralJestFnCall<'a>),
|
||||
Expect(ParsedExpectFnCall<'a>),
|
||||
ExpectTypeOf(ParsedExpectFnCall<'a>),
|
||||
}
|
||||
|
||||
impl<'a> ParsedJestFnCall<'a> {
|
||||
pub fn kind(&self) -> JestFnKind {
|
||||
match self {
|
||||
Self::GeneralJestFnCall(call) => call.kind,
|
||||
Self::ExpectFnCall(call) => call.kind,
|
||||
Self::GeneralJest(call) => call.kind,
|
||||
Self::Expect(call) | Self::ExpectTypeOf(call) => call.kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -328,6 +337,7 @@ pub struct ParsedGeneralJestFnCall<'a> {
|
|||
pub local: Cow<'a, str>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedExpectFnCall<'a> {
|
||||
pub kind: JestFnKind,
|
||||
pub members: Vec<KnownMemberExpressionProperty<'a>>,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ mod react;
|
|||
mod react_perf;
|
||||
mod tree_shaking;
|
||||
mod unicorn;
|
||||
mod vitest;
|
||||
|
||||
pub use self::{
|
||||
jest::*, jsdoc::*, nextjs::*, react::*, react_perf::*, tree_shaking::*, unicorn::*,
|
||||
jest::*, jsdoc::*, nextjs::*, react::*, react_perf::*, tree_shaking::*, unicorn::*, vitest::*,
|
||||
};
|
||||
|
||||
/// Check if the Jest rule is adapted to Vitest.
|
||||
|
|
|
|||
18
crates/oxc_linter/src/utils/vitest.rs
Normal file
18
crates/oxc_linter/src/utils/vitest.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use crate::LintContext;
|
||||
use oxc_ast::ast::CallExpression;
|
||||
|
||||
use super::{parse_jest_fn_call, ParsedExpectFnCall, ParsedJestFnCallNew, PossibleJestNode};
|
||||
|
||||
pub fn parse_expect_and_typeof_vitest_fn_call<'a>(
|
||||
call_expr: &'a CallExpression<'a>,
|
||||
possible_jest_node: &PossibleJestNode<'a, '_>,
|
||||
ctx: &LintContext<'a>,
|
||||
) -> Option<ParsedExpectFnCall<'a>> {
|
||||
let jest_fn_call = parse_jest_fn_call(call_expr, possible_jest_node, ctx)?;
|
||||
|
||||
match jest_fn_call {
|
||||
ParsedJestFnCallNew::Expect(jest_fn_call)
|
||||
| ParsedJestFnCallNew::ExpectTypeOf(jest_fn_call) => Some(jest_fn_call),
|
||||
ParsedJestFnCallNew::GeneralJest(_) => None,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue