diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 52b9a3500..b2410f1d1 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -115,6 +115,7 @@ mod jest { pub mod no_done_callback; pub mod no_export; pub mod no_focused_tests; + pub mod no_hooks; pub mod no_identical_title; pub mod no_interpolation_in_snapshots; pub mod no_jasmine_globals; @@ -267,6 +268,7 @@ oxc_macros::declare_all_lint_rules! { jest::no_done_callback, jest::no_export, jest::no_focused_tests, + jest::no_hooks, jest::no_identical_title, jest::no_interpolation_in_snapshots, jest::no_jasmine_globals, diff --git a/crates/oxc_linter/src/rules/jest/no_hooks.rs b/crates/oxc_linter/src/rules/jest/no_hooks.rs new file mode 100644 index 000000000..d9cb43a76 --- /dev/null +++ b/crates/oxc_linter/src/rules/jest/no_hooks.rs @@ -0,0 +1,147 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{is_type_of_jest_fn_call, JestFnKind, JestGeneralFnKind, JEST_HOOK_NAMES}, + AstNode, +}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-jest(no-hooks): Disallow setup and teardown hooks.")] +#[diagnostic(severity(warning))] +pub struct UnexpectedHookDiagonsitc(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct NoHooks { + pub allow: Vec, +} + +declare_oxc_lint!( + /// ### What it does + /// Jest provides global functions for setup and teardown tasks, which are called before/after each test case + /// and each test suite. The use of these hooks promotes shared state between tests. + /// + /// ### Why is this bad? + /// + /// This rule reports for the following function calls: + /// * beforeAll + /// * beforeEach + /// * afterAll + /// * afterEach + /// + /// ### Example + /// + /// ```javascript + /// function setupFoo(options) { /* ... */ } + /// function setupBar(options) { /* ... */ } + /// + /// describe('foo', () => { + /// let foo; + /// beforeEach(() => { + /// foo = setupFoo(); + /// }); + /// afterEach(() => { + /// foo = null; + /// }); + /// it('does something', () => { + /// expect(foo.doesSomething()).toBe(true); + /// }); + /// describe('with bar', () => { + /// let bar; + /// beforeEach(() => { + /// bar = setupBar(); + /// }); + /// afterEach(() => { + /// bar = null; + /// }); + /// it('does something with bar', () => { + /// expect(foo.doesSomething(bar)).toBe(true); + /// }); + /// }); + /// }); + /// ``` + NoHooks, + style, +); + +impl Rule for NoHooks { + fn from_configuration(value: serde_json::Value) -> Self { + let allow = value + .get(0) + .and_then(|config| config.get("allow")) + .and_then(serde_json::Value::as_array) + .map(|v| { + v.iter().filter_map(serde_json::Value::as_str).map(ToString::to_string).collect() + }) + .unwrap_or_default(); + + Self { allow } + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + if !is_type_of_jest_fn_call( + call_expr, + node, + ctx, + &[JestFnKind::General(JestGeneralFnKind::Hook)], + ) { + return; + } + + if let Expression::Identifier(ident) = &call_expr.callee { + if JEST_HOOK_NAMES.contains(&ident.name.as_str()) + && !self.allow.contains(&ident.name.to_string()) + { + ctx.diagnostic(UnexpectedHookDiagonsitc(call_expr.callee.span())); + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("test(\"foo\")", None), + ("describe(\"foo\", () => { it(\"bar\") })", None), + ("test(\"foo\", () => { expect(subject.beforeEach()).toBe(true) })", None), + ( + "afterEach(() => {}); afterAll(() => {});", + Some(serde_json::json!([{ "allow": ["afterEach", "afterAll"] }])), + ), + ("test(\"foo\")", Some(serde_json::json!([{ "allow": "undefined" }]))), + ]; + + let fail = vec![ + ("beforeAll(() => {})", None), + ("beforeEach(() => {})", None), + ("afterAll(() => {})", None), + ("afterEach(() => {})", None), + ( + "beforeEach(() => {}); afterEach(() => { jest.resetModules() });", + Some(serde_json::json!([{ "allow": ["afterEach"] }])), + ), + ( + " + import { beforeEach as afterEach, afterEach as beforeEach } from '@jest/globals'; + + afterEach(() => {}); + beforeEach(() => { jest.resetModules() }); + ", + Some(serde_json::json!([{ "allow": ["afterEach"] }])), + ), + ]; + + Tester::new(NoHooks::NAME, pass, fail).with_jest_plugin(true).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_hooks.snap b/crates/oxc_linter/src/snapshots/no_hooks.snap new file mode 100644 index 000000000..df6b81c07 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_hooks.snap @@ -0,0 +1,44 @@ +--- +source: crates/oxc_linter/src/tester.rs +assertion_line: 119 +expression: no_hooks +--- + ⚠ eslint-plugin-jest(no-hooks): Disallow setup and teardown hooks. + ╭─[no_hooks.tsx:1:1] + 1 │ beforeAll(() => {}) + · ───────── + ╰──── + + ⚠ eslint-plugin-jest(no-hooks): Disallow setup and teardown hooks. + ╭─[no_hooks.tsx:1:1] + 1 │ beforeEach(() => {}) + · ────────── + ╰──── + + ⚠ eslint-plugin-jest(no-hooks): Disallow setup and teardown hooks. + ╭─[no_hooks.tsx:1:1] + 1 │ afterAll(() => {}) + · ──────── + ╰──── + + ⚠ eslint-plugin-jest(no-hooks): Disallow setup and teardown hooks. + ╭─[no_hooks.tsx:1:1] + 1 │ afterEach(() => {}) + · ───────── + ╰──── + + ⚠ eslint-plugin-jest(no-hooks): Disallow setup and teardown hooks. + ╭─[no_hooks.tsx:1:1] + 1 │ beforeEach(() => {}); afterEach(() => { jest.resetModules() }); + · ────────── + ╰──── + + ⚠ eslint-plugin-jest(no-hooks): Disallow setup and teardown hooks. + ╭─[no_hooks.tsx:4:1] + 4 │ afterEach(() => {}); + 5 │ beforeEach(() => { jest.resetModules() }); + · ────────── + 6 │ + ╰──── + + diff --git a/crates/oxc_linter/src/utils/jest.rs b/crates/oxc_linter/src/utils/jest.rs index 01b4fec3d..1e2f75a6d 100644 --- a/crates/oxc_linter/src/utils/jest.rs +++ b/crates/oxc_linter/src/utils/jest.rs @@ -31,6 +31,8 @@ const JEST_METHOD_NAMES: [&str; 14] = [ "xtest", ]; +pub const JEST_HOOK_NAMES: [&str; 4] = ["afterAll", "afterEach", "beforeAll", "beforeEach"]; + #[derive(Clone, Copy, PartialEq, Eq)] pub enum JestFnKind { Expect,