feat(linter): eslint-plugin-jest: no-hooks (#1172)

This commit is contained in:
cin 2023-11-07 21:52:36 +08:00 committed by GitHub
parent 633c469d2d
commit 9369424d8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 195 additions and 0 deletions

View file

@ -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,

View file

@ -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<String>,
}
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();
}

View file

@ -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 │
╰────

View file

@ -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,