mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter/eslint-plugin-vitest): implement prefer-to-be-falsy (#4770)
Related to https://github.com/oxc-project/oxc/issues/4656
This commit is contained in:
parent
0816255e5d
commit
c509a21a1f
5 changed files with 209 additions and 65 deletions
|
|
@ -449,6 +449,7 @@ mod promise {
|
||||||
|
|
||||||
mod vitest {
|
mod vitest {
|
||||||
pub mod no_import_node_test;
|
pub mod no_import_node_test;
|
||||||
|
pub mod prefer_to_be_falsy;
|
||||||
pub mod prefer_to_be_truthy;
|
pub mod prefer_to_be_truthy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -855,5 +856,6 @@ oxc_macros::declare_all_lint_rules! {
|
||||||
promise::no_new_statics,
|
promise::no_new_statics,
|
||||||
promise::param_names,
|
promise::param_names,
|
||||||
vitest::no_import_node_test,
|
vitest::no_import_node_test,
|
||||||
|
vitest::prefer_to_be_falsy,
|
||||||
vitest::prefer_to_be_truthy,
|
vitest::prefer_to_be_truthy,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
97
crates/oxc_linter/src/rules/vitest/prefer_to_be_falsy.rs
Normal file
97
crates/oxc_linter/src/rules/vitest/prefer_to_be_falsy.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
use oxc_macros::declare_oxc_lint;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
context::LintContext,
|
||||||
|
rule::Rule,
|
||||||
|
utils::{collect_possible_jest_call_node, prefer_to_be_simply_bool},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct PreferToBeFalsy;
|
||||||
|
|
||||||
|
declare_oxc_lint!(
|
||||||
|
/// ### What it does
|
||||||
|
///
|
||||||
|
/// This rule warns when `toBe(false)` is used with `expect` or `expectTypeOf`. With `--fix`, it will be replaced with `toBeFalsy()`.
|
||||||
|
///
|
||||||
|
/// ### Examples
|
||||||
|
///
|
||||||
|
/// ```javascript
|
||||||
|
/// // bad
|
||||||
|
/// expect(foo).toBe(false)
|
||||||
|
/// expectTypeOf(foo).toBe(false)
|
||||||
|
///
|
||||||
|
/// // good
|
||||||
|
/// expect(foo).toBeFalsy()
|
||||||
|
/// expectTypeOf(foo).toBeFalsy()
|
||||||
|
/// ```
|
||||||
|
PreferToBeFalsy,
|
||||||
|
style,
|
||||||
|
fix
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Rule for PreferToBeFalsy {
|
||||||
|
fn run_once(&self, ctx: &LintContext) {
|
||||||
|
for possible_vitest_node in &collect_possible_jest_call_node(ctx) {
|
||||||
|
prefer_to_be_simply_bool(possible_vitest_node, ctx, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
use crate::tester::Tester;
|
||||||
|
|
||||||
|
let pass = vec![
|
||||||
|
"[].push(false)",
|
||||||
|
r#"expect("something");"#,
|
||||||
|
"expect(true).toBeTrue();",
|
||||||
|
"expect(false).toBeTrue();",
|
||||||
|
"expect(false).toBeFalsy();",
|
||||||
|
"expect(true).toBeFalsy();",
|
||||||
|
"expect(value).toEqual();",
|
||||||
|
"expect(value).not.toBeFalsy();",
|
||||||
|
"expect(value).not.toEqual();",
|
||||||
|
"expect(value).toBe(undefined);",
|
||||||
|
"expect(value).not.toBe(undefined);",
|
||||||
|
"expect(false).toBe(true)",
|
||||||
|
"expect(value).toBe();",
|
||||||
|
"expect(true).toMatchSnapshot();",
|
||||||
|
r#"expect("a string").toMatchSnapshot(false);"#,
|
||||||
|
r#"expect("a string").not.toMatchSnapshot();"#,
|
||||||
|
"expect(something).toEqual('a string');",
|
||||||
|
"expect(false).toBe",
|
||||||
|
"expectTypeOf(false).toBe",
|
||||||
|
];
|
||||||
|
|
||||||
|
let fail = vec![
|
||||||
|
"expect(true).toBe(false);",
|
||||||
|
"expect(wasSuccessful).toEqual(false);",
|
||||||
|
"expect(fs.existsSync('/path/to/file')).toStrictEqual(false);",
|
||||||
|
r#"expect("a string").not.toBe(false);"#,
|
||||||
|
r#"expect("a string").not.toEqual(false);"#,
|
||||||
|
r#"expectTypeOf("a string").not.toEqual(false);"#,
|
||||||
|
];
|
||||||
|
|
||||||
|
let fix = vec![
|
||||||
|
("expect(true).toBe(false);", "expect(true).toBeFalsy();", None),
|
||||||
|
("expect(wasSuccessful).toEqual(false);", "expect(wasSuccessful).toBeFalsy();", None),
|
||||||
|
(
|
||||||
|
"expect(fs.existsSync('/path/to/file')).toStrictEqual(false);",
|
||||||
|
"expect(fs.existsSync('/path/to/file')).toBeFalsy();",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(r#"expect("a string").not.toBe(false);"#, r#"expect("a string").not.toBeFalsy();"#, None),
|
||||||
|
(
|
||||||
|
r#"expect("a string").not.toEqual(false);"#,
|
||||||
|
r#"expect("a string").not.toBeFalsy();"#,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r#"expectTypeOf("a string").not.toEqual(false);"#,
|
||||||
|
r#"expectTypeOf("a string").not.toBeFalsy();"#,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
Tester::new(PreferToBeFalsy::NAME, pass, fail).expect_fix(fix).test_and_snapshot();
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,11 @@
|
||||||
use oxc_ast::{
|
|
||||||
ast::{Argument, Expression},
|
|
||||||
AstKind,
|
|
||||||
};
|
|
||||||
use oxc_diagnostics::OxcDiagnostic;
|
|
||||||
use oxc_macros::declare_oxc_lint;
|
use oxc_macros::declare_oxc_lint;
|
||||||
use oxc_span::Span;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
context::LintContext,
|
context::LintContext,
|
||||||
rule::Rule,
|
rule::Rule,
|
||||||
utils::{
|
utils::{collect_possible_jest_call_node, prefer_to_be_simply_bool},
|
||||||
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)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct PreferToBeTruthy;
|
pub struct PreferToBeTruthy;
|
||||||
|
|
||||||
|
|
@ -46,55 +33,7 @@ declare_oxc_lint!(
|
||||||
impl Rule for PreferToBeTruthy {
|
impl Rule for PreferToBeTruthy {
|
||||||
fn run_once(&self, ctx: &LintContext) {
|
fn run_once(&self, ctx: &LintContext) {
|
||||||
for possible_vitest_node in &collect_possible_jest_call_node(ctx) {
|
for possible_vitest_node in &collect_possible_jest_call_node(ctx) {
|
||||||
Self::run(possible_vitest_node, ctx);
|
prefer_to_be_simply_bool(possible_vitest_node, ctx, true);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
44
crates/oxc_linter/src/snapshots/prefer_to_be_falsy.snap
Normal file
44
crates/oxc_linter/src/snapshots/prefer_to_be_falsy.snap
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
source: crates/oxc_linter/src/tester.rs
|
||||||
|
---
|
||||||
|
⚠ eslint-plugin-vitest(prefer-to-be-falsy): Use `toBeFalsy` instead.
|
||||||
|
╭─[prefer_to_be_falsy.tsx:1:14]
|
||||||
|
1 │ expect(true).toBe(false);
|
||||||
|
· ───────────
|
||||||
|
╰────
|
||||||
|
help: Replace `toBe(false)` with `toBeFalsy()`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-vitest(prefer-to-be-falsy): Use `toBeFalsy` instead.
|
||||||
|
╭─[prefer_to_be_falsy.tsx:1:23]
|
||||||
|
1 │ expect(wasSuccessful).toEqual(false);
|
||||||
|
· ──────────────
|
||||||
|
╰────
|
||||||
|
help: Replace `toEqual(false)` with `toBeFalsy()`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-vitest(prefer-to-be-falsy): Use `toBeFalsy` instead.
|
||||||
|
╭─[prefer_to_be_falsy.tsx:1:40]
|
||||||
|
1 │ expect(fs.existsSync('/path/to/file')).toStrictEqual(false);
|
||||||
|
· ────────────────────
|
||||||
|
╰────
|
||||||
|
help: Replace `toStrictEqual(false)` with `toBeFalsy()`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-vitest(prefer-to-be-falsy): Use `toBeFalsy` instead.
|
||||||
|
╭─[prefer_to_be_falsy.tsx:1:24]
|
||||||
|
1 │ expect("a string").not.toBe(false);
|
||||||
|
· ───────────
|
||||||
|
╰────
|
||||||
|
help: Replace `toBe(false)` with `toBeFalsy()`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-vitest(prefer-to-be-falsy): Use `toBeFalsy` instead.
|
||||||
|
╭─[prefer_to_be_falsy.tsx:1:24]
|
||||||
|
1 │ expect("a string").not.toEqual(false);
|
||||||
|
· ──────────────
|
||||||
|
╰────
|
||||||
|
help: Replace `toEqual(false)` with `toBeFalsy()`.
|
||||||
|
|
||||||
|
⚠ eslint-plugin-vitest(prefer-to-be-falsy): Use `toBeFalsy` instead.
|
||||||
|
╭─[prefer_to_be_falsy.tsx:1:30]
|
||||||
|
1 │ expectTypeOf("a string").not.toEqual(false);
|
||||||
|
· ──────────────
|
||||||
|
╰────
|
||||||
|
help: Replace `toEqual(false)` with `toBeFalsy()`.
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
use crate::LintContext;
|
use crate::LintContext;
|
||||||
use oxc_ast::ast::CallExpression;
|
use oxc_ast::{
|
||||||
|
ast::{Argument, CallExpression, Expression},
|
||||||
|
AstKind,
|
||||||
|
};
|
||||||
|
use oxc_diagnostics::OxcDiagnostic;
|
||||||
|
use oxc_span::Span;
|
||||||
|
|
||||||
use super::{parse_jest_fn_call, ParsedExpectFnCall, ParsedJestFnCallNew, PossibleJestNode};
|
use super::{
|
||||||
|
is_equality_matcher, parse_jest_fn_call, ParsedExpectFnCall, ParsedJestFnCallNew,
|
||||||
|
PossibleJestNode,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn parse_expect_and_typeof_vitest_fn_call<'a>(
|
pub fn parse_expect_and_typeof_vitest_fn_call<'a>(
|
||||||
call_expr: &'a CallExpression<'a>,
|
call_expr: &'a CallExpression<'a>,
|
||||||
|
|
@ -16,3 +24,57 @@ pub fn parse_expect_and_typeof_vitest_fn_call<'a>(
|
||||||
ParsedJestFnCallNew::GeneralJest(_) => None,
|
ParsedJestFnCallNew::GeneralJest(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn prefer_to_be_simply_bool<'a>(
|
||||||
|
possible_vitest_node: &PossibleJestNode<'a, '_>,
|
||||||
|
ctx: &LintContext<'a>,
|
||||||
|
value: bool,
|
||||||
|
) {
|
||||||
|
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 == 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
let call_name = if value { "toBeTruthy" } else { "toBeFalsy" };
|
||||||
|
|
||||||
|
ctx.diagnostic_with_fix(
|
||||||
|
OxcDiagnostic::warn(format!("Use `{call_name}` instead.")).with_label(span),
|
||||||
|
|fixer| {
|
||||||
|
let new_matcher = if is_cmp_mem_expr {
|
||||||
|
format!("[\"{call_name}\"]()")
|
||||||
|
} else {
|
||||||
|
format!("{call_name}()")
|
||||||
|
};
|
||||||
|
fixer.replace(span, new_matcher)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue