diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index f839a9742e1e2..0432cc8ec3fcd 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -107,6 +107,7 @@ mod typescript { mod jest { pub mod expect_expect; + pub mod max_expects; pub mod no_alias_methods; pub mod no_commented_out_tests; pub mod no_conditional_expect; @@ -276,6 +277,7 @@ oxc_macros::declare_all_lint_rules! { typescript::no_var_requires, typescript::prefer_as_const, jest::expect_expect, + jest::max_expects, jest::no_alias_methods, jest::no_commented_out_tests, jest::no_conditional_expect, diff --git a/crates/oxc_linter/src/rules/jest/max_expects.rs b/crates/oxc_linter/src/rules/jest/max_expects.rs new file mode 100644 index 0000000000000..b69a56f66a832 --- /dev/null +++ b/crates/oxc_linter/src/rules/jest/max_expects.rs @@ -0,0 +1,482 @@ +use std::{collections::HashMap, hash::BuildHasherDefault}; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{collect_possible_jest_call_node, PossibleJestNode}, +}; +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; +use rustc_hash::{FxHashMap, FxHasher}; + +#[derive(Debug, Error, Diagnostic)] +#[error( + "eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body." +)] +#[diagnostic( + severity(warning), + help("Too many assertion calls ({0:?}) - maximum allowed is {1:?}") +)] +pub struct ExceededMaxAssertion(pub usize, pub usize, #[label] pub Span); + +#[derive(Debug, Clone)] +pub struct MaxExpects { + pub max: usize, +} + +impl Default for MaxExpects { + fn default() -> Self { + Self { max: 5 } + } +} + +declare_oxc_lint!( + /// ### What it does + /// As more assertions are made, there is a possible tendency for the test to be + /// more likely to mix multiple objectives. To avoid this, this rule reports when + /// the maximum number of assertions is exceeded. + /// + /// ### Why is this bad? + /// + /// This rule enforces a maximum number of `expect()` calls. + /// The following patterns are considered warnings (with the default option of `{ "max": 5 } `): + /// + /// ### Example + /// + /// ```javascript + /// test('should not pass', () => { + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// }); + /// + /// it('should not pass', () => { + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// expect(true).toBeDefined(); + /// }); + /// ``` + MaxExpects, + style, +); + +impl Rule for MaxExpects { + fn from_configuration(value: serde_json::Value) -> Self { + let max = value + .get(0) + .and_then(|config| config.get("max")) + .and_then(serde_json::Value::as_number) + .and_then(serde_json::Number::as_u64) + .map_or(5, |v| usize::try_from(v).unwrap_or(5)); + + Self { max } + } + + fn run_once(&self, ctx: &LintContext) { + let mut count_map: HashMap> = + FxHashMap::default(); + + for possible_jest_node in &collect_possible_jest_call_node(ctx) { + self.run(possible_jest_node, &mut count_map, ctx); + } + } +} + +impl MaxExpects { + fn run<'a>( + &self, + jest_node: &PossibleJestNode<'a, '_>, + count_map: &mut HashMap>, + ctx: &LintContext<'a>, + ) { + let node = jest_node.node; + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + let Expression::Identifier(ident) = &call_expr.callee else { + return; + }; + + if ident.name == "expect" { + let position = node.scope_id().index(); + + if let Some(count) = count_map.get(&position) { + if count > &self.max { + ctx.diagnostic(ExceededMaxAssertion(*count, self.max, ident.span)); + } else { + count_map.insert(position, count + 1); + } + } else { + count_map.insert(position, 2); + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("test('should pass')", None), + ("test('should pass', () => {})", None), + ("test.skip('should pass', () => {})", None), + ( + " + test('should pass', function () { + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + // expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + it('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + test('should pass', async () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + test('should pass', async () => { + expect.hasAssertions(); + + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + test('should pass', async () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toEqual(expect.any(Boolean)); + }); + ", + None, + ), + ( + " + test('should pass', async () => { + expect.hasAssertions(); + + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toEqual(expect.any(Boolean)); + }); + ", + None, + ), + ( + " + describe('test', () => { + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + }); + ", + None, + ), + ( + " + test.each(['should', 'pass'], () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + function myHelper() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + function myHelper1() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + + function myHelper2() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + ", + None, + ), + ( + " + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + + function myHelper() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + ", + None, + ), + ( + " + const myHelper1 = () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + + test('should pass', function() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + + const myHelper2 = function() { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }; + ", + None, + ), + ( + " + test('should pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + Some(serde_json::json!([{ "max": 10 }])), + ), + ]; + + let fail = vec![ + ( + " + test('should not pass', function () { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + it('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + it('should not pass', async () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + describe('test', () => { + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + }); + ", + None, + ), + ( + " + test.each(['should', 'not', 'pass'], () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + None, + ), + ( + " + test('should not pass', () => { + expect(true).toBeDefined(); + expect(true).toBeDefined(); + }); + ", + Some(serde_json::json!([{ "max": 1 }])), + ), + ]; + + Tester::new(MaxExpects::NAME, pass, fail).with_jest_plugin(true).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/max_expects.snap b/crates/oxc_linter/src/snapshots/max_expects.snap new file mode 100644 index 0000000000000..ed12689187d6d --- /dev/null +++ b/crates/oxc_linter/src/snapshots/max_expects.snap @@ -0,0 +1,87 @@ +--- +source: crates/oxc_linter/src/tester.rs +assertion_line: 119 +expression: max_expects +--- + ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. + ╭─[max_expects.tsx:7:1] + 7 │ expect(true).toBeDefined(); + 8 │ expect(true).toBeDefined(); + · ────── + 9 │ }); + ╰──── + help: Too many assertion calls (6) - maximum allowed is 5 + + ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. + ╭─[max_expects.tsx:7:1] + 7 │ expect(true).toBeDefined(); + 8 │ expect(true).toBeDefined(); + · ────── + 9 │ }); + ╰──── + help: Too many assertion calls (6) - maximum allowed is 5 + + ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. + ╭─[max_expects.tsx:7:1] + 7 │ expect(true).toBeDefined(); + 8 │ expect(true).toBeDefined(); + · ────── + 9 │ }); + ╰──── + help: Too many assertion calls (6) - maximum allowed is 5 + + ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. + ╭─[max_expects.tsx:7:1] + 7 │ expect(true).toBeDefined(); + 8 │ expect(true).toBeDefined(); + · ────── + 9 │ }); + ╰──── + help: Too many assertion calls (6) - maximum allowed is 5 + + ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. + ╭─[max_expects.tsx:7:1] + 7 │ expect(true).toBeDefined(); + 8 │ expect(true).toBeDefined(); + · ────── + 9 │ }); + ╰──── + help: Too many assertion calls (6) - maximum allowed is 5 + + ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. + ╭─[max_expects.tsx:15:1] + 15 │ expect(true).toBeDefined(); + 16 │ expect(true).toBeDefined(); + · ────── + 17 │ }); + ╰──── + help: Too many assertion calls (6) - maximum allowed is 5 + + ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. + ╭─[max_expects.tsx:8:1] + 8 │ expect(true).toBeDefined(); + 9 │ expect(true).toBeDefined(); + · ────── + 10 │ }); + ╰──── + help: Too many assertion calls (6) - maximum allowed is 5 + + ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. + ╭─[max_expects.tsx:7:1] + 7 │ expect(true).toBeDefined(); + 8 │ expect(true).toBeDefined(); + · ────── + 9 │ }); + ╰──── + help: Too many assertion calls (6) - maximum allowed is 5 + + ⚠ eslint-plugin-jest(max-expects): Enforces a maximum number assertion calls in a test body. + ╭─[max_expects.tsx:3:1] + 3 │ expect(true).toBeDefined(); + 4 │ expect(true).toBeDefined(); + · ────── + 5 │ }); + ╰──── + help: Too many assertion calls (2) - maximum allowed is 1 + +