mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(linter): eslint-plugin-jest: prefer-to-have-length (#2580)
Rule Detail: [link](https://github.com/jest-community/eslint-plugin-jest/blob/main/src/rules/prefer-to-have-length.ts)
This commit is contained in:
parent
951297ee59
commit
35ce3ccdc0
5 changed files with 328 additions and 4 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3214,4 +3214,4 @@ checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377"
|
|||
name = "zeroize"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
||||
|
|
@ -153,6 +153,7 @@ mod jest {
|
|||
pub mod prefer_called_with;
|
||||
pub mod prefer_equality_matcher;
|
||||
pub mod prefer_strict_equal;
|
||||
pub mod prefer_to_have_length;
|
||||
pub mod prefer_todo;
|
||||
pub mod require_to_throw_message;
|
||||
pub mod valid_describe_callback;
|
||||
|
|
@ -450,6 +451,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
jest::prefer_called_with,
|
||||
jest::prefer_equality_matcher,
|
||||
jest::prefer_strict_equal,
|
||||
jest::prefer_to_have_length,
|
||||
jest::prefer_todo,
|
||||
jest::require_to_throw_message,
|
||||
jest::valid_describe_callback,
|
||||
|
|
|
|||
258
crates/oxc_linter/src/rules/jest/prefer_to_have_length.rs
Normal file
258
crates/oxc_linter/src/rules/jest/prefer_to_have_length.rs
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
use oxc_ast::{
|
||||
ast::{Argument, CallExpression, Expression, MemberExpression},
|
||||
AstKind,
|
||||
};
|
||||
use oxc_diagnostics::{
|
||||
miette::{self, Diagnostic},
|
||||
thiserror::Error,
|
||||
};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{GetSpan, Span};
|
||||
|
||||
use crate::{
|
||||
context::LintContext,
|
||||
fixer::Fix,
|
||||
rule::Rule,
|
||||
utils::{
|
||||
collect_possible_jest_call_node, parse_expect_jest_fn_call, ParsedExpectFnCall,
|
||||
PossibleJestNode,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.")]
|
||||
#[diagnostic(severity(warning))]
|
||||
struct UseToHaveLength(#[label] pub Span);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PreferToHaveLength;
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// In order to have a better failure message, `toHaveLength()` should be used upon
|
||||
/// asserting expectations on objects length property.
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
/// This rule triggers a warning if `toBe()`, `toEqual()` or `toStrictEqual()` is
|
||||
/// used to assert objects length property.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```javascript
|
||||
/// // valid
|
||||
/// expect.hasAssertions;
|
||||
/// expect.hasAssertions();
|
||||
/// expect(files).toHaveLength(1);
|
||||
/// expect(files.name).toBe('file');
|
||||
///
|
||||
/// // invalid
|
||||
/// expect(files["length"]).toBe(1);
|
||||
/// expect(files["length"]).toBe(1,);
|
||||
/// expect(files["length"])["not"].toBe(1)
|
||||
/// ```
|
||||
///
|
||||
PreferToHaveLength,
|
||||
style,
|
||||
);
|
||||
|
||||
impl Rule for PreferToHaveLength {
|
||||
fn run_once(&self, ctx: &LintContext) {
|
||||
for possible_jest_node in &collect_possible_jest_call_node(ctx) {
|
||||
Self::run(possible_jest_node, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PreferToHaveLength {
|
||||
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 {
|
||||
return;
|
||||
};
|
||||
let Some(parsed_expect_call) =
|
||||
parse_expect_jest_fn_call(call_expr, possible_jest_node, ctx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Expression::MemberExpression(static_expr) = &call_expr.callee else {
|
||||
return;
|
||||
};
|
||||
|
||||
match static_expr.object() {
|
||||
Expression::MemberExpression(mem_expr) => {
|
||||
let Expression::CallExpression(expr_call_expr) = mem_expr.object() else {
|
||||
return;
|
||||
};
|
||||
match &mem_expr.0 {
|
||||
MemberExpression::ComputedMemberExpression(_) => Self::check_and_fix(
|
||||
call_expr,
|
||||
expr_call_expr,
|
||||
&parsed_expect_call,
|
||||
Some("ComputedMember"),
|
||||
mem_expr.static_property_name(),
|
||||
ctx,
|
||||
),
|
||||
MemberExpression::StaticMemberExpression(_) => Self::check_and_fix(
|
||||
call_expr,
|
||||
expr_call_expr,
|
||||
&parsed_expect_call,
|
||||
Some("StaticMember"),
|
||||
mem_expr.static_property_name(),
|
||||
ctx,
|
||||
),
|
||||
MemberExpression::PrivateFieldExpression(_) => (),
|
||||
};
|
||||
}
|
||||
Expression::CallExpression(expr_call_expr) => {
|
||||
Self::check_and_fix(
|
||||
call_expr,
|
||||
expr_call_expr,
|
||||
&parsed_expect_call,
|
||||
None,
|
||||
None,
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_and_fix<'a>(
|
||||
call_expr: &CallExpression<'a>,
|
||||
expr_call_expr: &CallExpression<'a>,
|
||||
parsed_expect_call: &ParsedExpectFnCall<'a>,
|
||||
kind: Option<&str>,
|
||||
property_name: Option<&str>,
|
||||
ctx: &LintContext<'a>,
|
||||
) {
|
||||
let Some(argument) = expr_call_expr.arguments.first() else {
|
||||
return;
|
||||
};
|
||||
let Argument::Expression(Expression::MemberExpression(static_mem_expr)) = argument else {
|
||||
return;
|
||||
};
|
||||
// Get property `name` field from expect(file.NAME) call
|
||||
let Some(expect_property_name) = static_mem_expr.static_property_name() else {
|
||||
return;
|
||||
};
|
||||
let Some(matcher) = parsed_expect_call.matcher() else {
|
||||
return;
|
||||
};
|
||||
let Some(matcher_name) = matcher.name() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if expect_property_name != "length" || !Self::is_equality_matcher(&matcher_name) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.diagnostic_with_fix(UseToHaveLength(matcher.span), || {
|
||||
let code = Self::build_code(static_mem_expr, kind, property_name, ctx);
|
||||
let end = if call_expr.arguments.len() > 0 {
|
||||
call_expr.arguments.first().unwrap().span().start
|
||||
} else {
|
||||
matcher.span.end
|
||||
};
|
||||
Fix::new(code, Span { start: call_expr.span.start, end: end - 1 })
|
||||
});
|
||||
}
|
||||
|
||||
fn is_equality_matcher(matcher_name: &str) -> bool {
|
||||
matcher_name == "toBe" || matcher_name == "toEqual" || matcher_name == "toStrictEqual"
|
||||
}
|
||||
|
||||
fn build_code(
|
||||
mem_expr: &MemberExpression,
|
||||
kind: Option<&str>,
|
||||
property_name: Option<&str>,
|
||||
ctx: &LintContext<'_>,
|
||||
) -> String {
|
||||
let mut formatter = ctx.codegen();
|
||||
let Expression::Identifier(prop_ident) = mem_expr.object() else {
|
||||
return formatter.into_source_text();
|
||||
};
|
||||
|
||||
formatter.print_str(b"expect(");
|
||||
formatter.print_str(prop_ident.name.as_bytes());
|
||||
formatter.print_str(b")");
|
||||
|
||||
if let Some(kind_val) = kind {
|
||||
if kind_val == "ComputedMember" {
|
||||
let property = property_name.unwrap();
|
||||
formatter.print_str(b"[\"");
|
||||
formatter.print_str(property.as_bytes());
|
||||
formatter.print_str(b"\"]");
|
||||
} else if kind_val == "StaticMember" {
|
||||
formatter.print_str(b".");
|
||||
let property = property_name.unwrap();
|
||||
formatter.print_str(property.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
formatter.print_str(b".toHaveLength");
|
||||
formatter.into_source_text()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tests() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
("expect.hasAssertions", None),
|
||||
("expect.hasAssertions()", None),
|
||||
("expect(files).toHaveLength(1);", None),
|
||||
("expect(files.name).toBe('file');", None),
|
||||
("expect(files[`name`]).toBe('file');", None),
|
||||
("expect(users[0]?.permissions?.length).toBe(1);", None),
|
||||
("expect(result).toBe(true);", None),
|
||||
("expect(user.getUserName(5)).resolves.toEqual('Paul')", None),
|
||||
("expect(user.getUserName(5)).rejects.toEqual('Paul')", None),
|
||||
("expect(a);", None),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
("expect(files[\"length\"]).toBe(1);", None),
|
||||
("expect(files[\"length\"]).toBe(1,);", None),
|
||||
("expect(files[\"length\"])[\"not\"].toBe(1);", None),
|
||||
("expect(files[\"length\"])[\"toBe\"](1);", None),
|
||||
("expect(files[\"length\"]).not[\"toBe\"](1);", None),
|
||||
("expect(files[\"length\"])[\"not\"][\"toBe\"](1);", None),
|
||||
("expect(files.length).toBe(1);", None),
|
||||
("expect(files.length).toEqual(1);", None),
|
||||
("expect(files.length).toStrictEqual(1);", None),
|
||||
("expect(files.length).not.toStrictEqual(1);", None),
|
||||
];
|
||||
|
||||
let fix = vec![
|
||||
("expect(files[\"length\"]).not.toBe(1);", "expect(files).not.toHaveLength(1);", None),
|
||||
(
|
||||
"expect(files[\"length\"])[\"resolves\"].toBe(1,);",
|
||||
"expect(files)[\"resolves\"].toHaveLength(1,);",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"expect(files[\"length\"])[\"not\"].toBe(1);",
|
||||
"expect(files)[\"not\"].toHaveLength(1);",
|
||||
None,
|
||||
),
|
||||
("expect(files[\"length\"])[\"toBe\"](1);", "expect(files).toHaveLength(1);", None),
|
||||
("expect(files[\"length\"]).not[\"toBe\"](1);", "expect(files).not.toHaveLength(1);", None),
|
||||
(
|
||||
"expect(files[\"length\"])[\"not\"][\"toBe\"](1);",
|
||||
"expect(files)[\"not\"].toHaveLength(1);",
|
||||
None,
|
||||
),
|
||||
("expect(files.length).toBe(1);", "expect(files).toHaveLength(1);", None),
|
||||
("expect(files.length).toEqual(1);", "expect(files).toHaveLength(1);", None),
|
||||
("expect(files.length).toStrictEqual(1);", "expect(files).toHaveLength(1);", None),
|
||||
("expect(files.length).not.toStrictEqual(1);", "expect(files).not.toHaveLength(1);", None),
|
||||
];
|
||||
|
||||
Tester::new(PreferToHaveLength::NAME, pass, fail)
|
||||
.with_jest_plugin(true)
|
||||
.expect_fix(fix)
|
||||
.test_and_snapshot();
|
||||
}
|
||||
|
|
@ -22,12 +22,12 @@ use crate::{
|
|||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("eslint-plugin-jest(prefer-todo): Suggest using `test.todo`.")]
|
||||
#[diagnostic(severity(warning))]
|
||||
pub struct EmptyTest(#[label] pub Span);
|
||||
struct EmptyTest(#[label] pub Span);
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("eslint-plugin-jest(prefer-todo): Suggest using `test.todo`.")]
|
||||
#[diagnostic(severity(warning))]
|
||||
struct UmImplementedTestDiagnostic(#[label] pub Span);
|
||||
struct UnImplementedTestDiagnostic(#[label] pub Span);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct PreferTodo;
|
||||
|
|
@ -82,7 +82,7 @@ fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>)
|
|||
|
||||
if counts == 1 && !filter_todo_case(call_expr) {
|
||||
let (content, span) = get_fix_content(call_expr);
|
||||
ctx.diagnostic_with_fix(UmImplementedTestDiagnostic(span), || Fix::new(content, span));
|
||||
ctx.diagnostic_with_fix(UnImplementedTestDiagnostic(span), || Fix::new(content, span));
|
||||
}
|
||||
|
||||
if counts > 1 && is_empty_function(call_expr) {
|
||||
|
|
|
|||
64
crates/oxc_linter/src/snapshots/prefer_to_have_length.snap
Normal file
64
crates/oxc_linter/src/snapshots/prefer_to_have_length.snap
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
assertion_line: 151
|
||||
expression: prefer_to_have_length
|
||||
---
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:25]
|
||||
1 │ expect(files["length"]).toBe(1);
|
||||
· ────
|
||||
╰────
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:25]
|
||||
1 │ expect(files["length"]).toBe(1,);
|
||||
· ────
|
||||
╰────
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:32]
|
||||
1 │ expect(files["length"])["not"].toBe(1);
|
||||
· ────
|
||||
╰────
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:25]
|
||||
1 │ expect(files["length"])["toBe"](1);
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:29]
|
||||
1 │ expect(files["length"]).not["toBe"](1);
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:32]
|
||||
1 │ expect(files["length"])["not"]["toBe"](1);
|
||||
· ──────
|
||||
╰────
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:22]
|
||||
1 │ expect(files.length).toBe(1);
|
||||
· ────
|
||||
╰────
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:22]
|
||||
1 │ expect(files.length).toEqual(1);
|
||||
· ───────
|
||||
╰────
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:22]
|
||||
1 │ expect(files.length).toStrictEqual(1);
|
||||
· ─────────────
|
||||
╰────
|
||||
|
||||
⚠ eslint-plugin-jest(prefer-to-have-length): Suggest using `toHaveLength()`.
|
||||
╭─[prefer_to_have_length.tsx:1:26]
|
||||
1 │ expect(files.length).not.toStrictEqual(1);
|
||||
· ─────────────
|
||||
╰────
|
||||
Loading…
Reference in a new issue