mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 20:28:58 +00:00
feat(linter): eslint-plugin-jest/require-top-level-describe (#3439)
part of https://github.com/oxc-project/oxc/issues/492 Rule Detail: [link](https://github.com/jest-community/eslint-plugin-jest/blob/main/src/rules/require-top-level-describe.ts) --------- Co-authored-by: wenzhe <mysteryven@gmail.com>
This commit is contained in:
parent
edaa555620
commit
ded59bc35b
3 changed files with 527 additions and 0 deletions
|
|
@ -183,6 +183,7 @@ mod jest {
|
|||
pub mod prefer_todo;
|
||||
pub mod require_hook;
|
||||
pub mod require_to_throw_message;
|
||||
pub mod require_top_level_describe;
|
||||
pub mod valid_describe_callback;
|
||||
pub mod valid_expect;
|
||||
pub mod valid_title;
|
||||
|
|
@ -552,6 +553,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
jest::prefer_todo,
|
||||
jest::require_hook,
|
||||
jest::require_to_throw_message,
|
||||
jest::require_top_level_describe,
|
||||
jest::valid_describe_callback,
|
||||
jest::valid_expect,
|
||||
jest::valid_title,
|
||||
|
|
|
|||
393
crates/oxc_linter/src/rules/jest/require_top_level_describe.rs
Normal file
393
crates/oxc_linter/src/rules/jest/require_top_level_describe.rs
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use oxc_ast::AstKind;
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_semantic::ScopeId;
|
||||
use oxc_span::Span;
|
||||
|
||||
use crate::{
|
||||
context::LintContext,
|
||||
rule::Rule,
|
||||
utils::{
|
||||
collect_possible_jest_call_node, parse_jest_fn_call, JestFnKind, JestGeneralFnKind,
|
||||
ParsedGeneralJestFnCall, ParsedJestFnCallNew, PossibleJestNode,
|
||||
},
|
||||
};
|
||||
|
||||
fn too_many_describes(max: usize, repeat: &str, span0: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(
|
||||
"eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block",
|
||||
)
|
||||
.with_help(format!("There should not be more than {max:?} describe{repeat} at the top level."))
|
||||
.with_labels([span0.into()])
|
||||
}
|
||||
|
||||
fn unexpected_test_case(span0: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(
|
||||
"eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block",
|
||||
)
|
||||
.with_help("All test cases must be wrapped in a describe block.")
|
||||
.with_labels([span0.into()])
|
||||
}
|
||||
|
||||
fn unexpected_hook(span0: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(
|
||||
"eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block",
|
||||
)
|
||||
.with_help("All hooks must be wrapped in a describe block.")
|
||||
.with_labels([span0.into()])
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequireTopLevelDescribe {
|
||||
pub max_number_of_top_level_describes: usize,
|
||||
}
|
||||
|
||||
impl Default for RequireTopLevelDescribe {
|
||||
fn default() -> Self {
|
||||
Self { max_number_of_top_level_describes: usize::MAX }
|
||||
}
|
||||
}
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// This rule triggers a warning if a test case (`test` and `it`) or a hook
|
||||
/// (`beforeAll`, `beforeEach`, `afterEach`, `afterAll`) is not located in a
|
||||
/// top-level `describe` block.
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```javascript
|
||||
/// // invalid
|
||||
/// Above a describe block
|
||||
/// test('my test', () => {});
|
||||
/// describe('test suite', () => {
|
||||
/// it('test', () => {});
|
||||
/// });
|
||||
/// // Below a describe block
|
||||
/// describe('test suite', () => {});
|
||||
/// test('my test', () => {});
|
||||
/// // Same for hooks
|
||||
/// beforeAll('my beforeAll', () => {});
|
||||
/// describe('test suite', () => {});
|
||||
/// afterEach('my afterEach', () => {});
|
||||
///
|
||||
/// //valid
|
||||
/// // Above a describe block
|
||||
/// // In a describe block
|
||||
/// describe('test suite', () => {
|
||||
/// test('my test', () => {});
|
||||
/// });
|
||||
///
|
||||
/// // In a nested describe block
|
||||
/// describe('test suite', () => {
|
||||
/// test('my test', () => {});
|
||||
/// describe('another test suite', () => {
|
||||
/// test('my other test', () => {});
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// ### Options
|
||||
///
|
||||
/// You can also enforce a limit on the number of describes allowed at the top-level
|
||||
/// using the `maxNumberOfTopLevelDescribes` option:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "jest/require-top-level-describe": [
|
||||
/// "error",
|
||||
/// {
|
||||
/// "maxNumberOfTopLevelDescribes": 2
|
||||
/// }
|
||||
/// ]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
RequireTopLevelDescribe,
|
||||
style,
|
||||
);
|
||||
|
||||
impl Rule for RequireTopLevelDescribe {
|
||||
fn from_configuration(value: serde_json::Value) -> Self {
|
||||
let max_number_of_top_level_describes = value
|
||||
.get(0)
|
||||
.and_then(|config| config.get("maxNumberOfTopLevelDescribes"))
|
||||
.and_then(serde_json::Value::as_number)
|
||||
.and_then(serde_json::Number::as_u64)
|
||||
.map_or(usize::MAX, |v| usize::try_from(v).unwrap_or(usize::MAX));
|
||||
|
||||
Self { max_number_of_top_level_describes }
|
||||
}
|
||||
|
||||
fn run_once(&self, ctx: &LintContext) {
|
||||
let mut describe_contexts: HashMap<ScopeId, usize> = HashMap::new();
|
||||
let mut possibles_jest_nodes = collect_possible_jest_call_node(ctx);
|
||||
possibles_jest_nodes.sort_by_key(|n| n.node.id());
|
||||
|
||||
for possible_jest_node in &possibles_jest_nodes {
|
||||
self.run(possible_jest_node, &mut describe_contexts, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequireTopLevelDescribe {
|
||||
fn run<'a>(
|
||||
&self,
|
||||
possible_jest_node: &PossibleJestNode<'a, '_>,
|
||||
describe_contexts: &mut HashMap<ScopeId, usize>,
|
||||
ctx: &LintContext<'a>,
|
||||
) {
|
||||
let node = possible_jest_node.node;
|
||||
let scopes = ctx.scopes();
|
||||
let is_top = scopes.get_flags(node.scope_id()).is_top();
|
||||
|
||||
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(ParsedJestFnCallNew::GeneralJestFnCall(ParsedGeneralJestFnCall { kind, .. })) =
|
||||
parse_jest_fn_call(call_expr, possible_jest_node, ctx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
match kind {
|
||||
JestFnKind::General(JestGeneralFnKind::Test) => {
|
||||
if is_top {
|
||||
ctx.diagnostic(unexpected_test_case(call_expr.span));
|
||||
}
|
||||
}
|
||||
JestFnKind::General(JestGeneralFnKind::Hook) => {
|
||||
if is_top {
|
||||
ctx.diagnostic(unexpected_hook(call_expr.span));
|
||||
}
|
||||
}
|
||||
JestFnKind::General(JestGeneralFnKind::Describe) => {
|
||||
if !is_top {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some((_, count)) = describe_contexts.get_key_value(&node.scope_id()) else {
|
||||
describe_contexts.insert(node.scope_id(), 1);
|
||||
return;
|
||||
};
|
||||
|
||||
if count >= &self.max_number_of_top_level_describes {
|
||||
ctx.diagnostic(too_many_describes(
|
||||
self.max_number_of_top_level_describes,
|
||||
if *count == 1 { "" } else { "s" },
|
||||
call_expr.span,
|
||||
));
|
||||
} else {
|
||||
describe_contexts.insert(node.scope_id(), count + 1);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
("it.each()", None),
|
||||
("describe(\"test suite\", () => { test(\"my test\") });", None),
|
||||
("describe(\"test suite\", () => { it(\"my test\") });", None),
|
||||
(
|
||||
"
|
||||
describe(\"test suite\", () => {
|
||||
beforeEach(\"a\", () => {});
|
||||
describe(\"b\", () => {});
|
||||
test(\"c\", () => {})
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
("describe(\"test suite\", () => { beforeAll(\"my beforeAll\") });", None),
|
||||
("describe(\"test suite\", () => { afterEach(\"my afterEach\") });", None),
|
||||
("describe(\"test suite\", () => { afterAll(\"my afterAll\") });", None),
|
||||
(
|
||||
"
|
||||
describe(\"test suite\", () => {
|
||||
it(\"my test\", () => {})
|
||||
describe(\"another test suite\", () => {});
|
||||
test(\"my other test\", () => {})
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
("foo()", None),
|
||||
("describe.each([1, true])(\"trues\", value => { it(\"an it\", () => expect(value).toBe(true) ); });", None),
|
||||
(
|
||||
"
|
||||
describe('%s', () => {
|
||||
it('is fine', () => {
|
||||
//
|
||||
});
|
||||
});
|
||||
|
||||
describe.each('world')('%s', () => {
|
||||
it.each([1, 2, 3])('%n', () => {
|
||||
//
|
||||
});
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
describe.each('hello')('%s', () => {
|
||||
it('is fine', () => {
|
||||
//
|
||||
});
|
||||
});
|
||||
|
||||
describe.each('world')('%s', () => {
|
||||
it.each([1, 2, 3])('%n', () => {
|
||||
//
|
||||
});
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
jest.doMock('my-module');
|
||||
",
|
||||
None,
|
||||
),
|
||||
("jest.doMock(\"my-module\")", None),
|
||||
("describe(\"test suite\", () => { test(\"my test\") });", None),
|
||||
("foo()", None),
|
||||
("describe.each([1, true])(\"trues\", value => { it(\"an it\", () => expect(value).toBe(true) ); });", None),
|
||||
(
|
||||
"
|
||||
describe('one', () => {});
|
||||
describe('two', () => {});
|
||||
describe('three', () => {});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('one', () => {
|
||||
describe('two', () => {});
|
||||
describe('three', () => {});
|
||||
});
|
||||
",
|
||||
Some(serde_json::json!({ "maxNumberOfTopLevelDescribes": 1 })),
|
||||
),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
("beforeEach(\"my test\", () => {})", None),
|
||||
(
|
||||
"
|
||||
test(\"my test\", () => {})
|
||||
describe(\"test suite\", () => {});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
test(\"my test\", () => {})
|
||||
describe(\"test suite\", () => {
|
||||
it(\"test\", () => {})
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
describe(\"test suite\", () => {});
|
||||
afterAll(\"my test\", () => {})
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
import { describe, afterAll as onceEverythingIsDone } from '@jest/globals';
|
||||
|
||||
describe(\"test suite\", () => {});
|
||||
onceEverythingIsDone(\"my test\", () => {})
|
||||
",
|
||||
None,
|
||||
),
|
||||
("it.skip('test', () => {});", None),
|
||||
("it.each([1, 2, 3])('%n', () => {});", None),
|
||||
("it.skip.each([1, 2, 3])('%n', () => {});", None),
|
||||
("it.skip.each``('%n', () => {});", None),
|
||||
("it.each``('%n', () => {});", None),
|
||||
(
|
||||
"
|
||||
describe(\"one\", () => {});
|
||||
describe(\"two\", () => {});
|
||||
describe(\"three\", () => {});
|
||||
",
|
||||
Some(serde_json::json!([{ "maxNumberOfTopLevelDescribes": 2 }])),
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('one', () => {
|
||||
describe('one (nested)', () => {});
|
||||
describe('two (nested)', () => {});
|
||||
});
|
||||
describe('two', () => {
|
||||
describe('one (nested)', () => {});
|
||||
describe('two (nested)', () => {});
|
||||
describe('three (nested)', () => {});
|
||||
});
|
||||
describe('three', () => {
|
||||
describe('one (nested)', () => {});
|
||||
describe('two (nested)', () => {});
|
||||
describe('three (nested)', () => {});
|
||||
});
|
||||
",
|
||||
Some(serde_json::json!([{ "maxNumberOfTopLevelDescribes": 2 }])),
|
||||
),
|
||||
(
|
||||
"
|
||||
import {
|
||||
describe as describe1,
|
||||
describe as describe2,
|
||||
describe as describe3,
|
||||
} from '@jest/globals';
|
||||
|
||||
describe1('one', () => {
|
||||
describe('one (nested)', () => {});
|
||||
describe('two (nested)', () => {});
|
||||
});
|
||||
describe2('two', () => {
|
||||
describe('one (nested)', () => {});
|
||||
describe('two (nested)', () => {});
|
||||
describe('three (nested)', () => {});
|
||||
});
|
||||
describe3('three', () => {
|
||||
describe('one (nested)', () => {});
|
||||
describe('two (nested)', () => {});
|
||||
describe('three (nested)', () => {});
|
||||
});
|
||||
",
|
||||
Some(serde_json::json!([{ "maxNumberOfTopLevelDescribes": 2 }])),
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('one', () => {});
|
||||
describe('two', () => {});
|
||||
describe('three', () => {});
|
||||
",
|
||||
Some(serde_json::json!([{ "maxNumberOfTopLevelDescribes": 1 }])),
|
||||
),
|
||||
];
|
||||
|
||||
Tester::new(RequireTopLevelDescribe::NAME, pass, fail)
|
||||
.with_jest_plugin(true)
|
||||
.test_and_snapshot();
|
||||
}
|
||||
132
crates/oxc_linter/src/snapshots/require_top_level_describe.snap
Normal file
132
crates/oxc_linter/src/snapshots/require_top_level_describe.snap
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
expression: require_top_level_describe
|
||||
---
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:1:1]
|
||||
1 │ beforeEach("my test", () => {})
|
||||
· ───────────────────────────────
|
||||
╰────
|
||||
help: All hooks must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:2:17]
|
||||
1 │
|
||||
2 │ test("my test", () => {})
|
||||
· ─────────────────────────
|
||||
3 │ describe("test suite", () => {});
|
||||
╰────
|
||||
help: All test cases must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:2:17]
|
||||
1 │
|
||||
2 │ test("my test", () => {})
|
||||
· ─────────────────────────
|
||||
3 │ describe("test suite", () => {
|
||||
╰────
|
||||
help: All test cases must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:3:17]
|
||||
2 │ describe("test suite", () => {});
|
||||
3 │ afterAll("my test", () => {})
|
||||
· ─────────────────────────────
|
||||
4 │
|
||||
╰────
|
||||
help: All hooks must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:5:17]
|
||||
4 │ describe("test suite", () => {});
|
||||
5 │ onceEverythingIsDone("my test", () => {})
|
||||
· ─────────────────────────────────────────
|
||||
6 │
|
||||
╰────
|
||||
help: All hooks must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:1:1]
|
||||
1 │ it.skip('test', () => {});
|
||||
· ─────────────────────────
|
||||
╰────
|
||||
help: All test cases must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:1:1]
|
||||
1 │ it.each([1, 2, 3])('%n', () => {});
|
||||
· ──────────────────────────────────
|
||||
╰────
|
||||
help: All test cases must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:1:1]
|
||||
1 │ it.skip.each([1, 2, 3])('%n', () => {});
|
||||
· ───────────────────────────────────────
|
||||
╰────
|
||||
help: All test cases must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:1:1]
|
||||
1 │ it.skip.each``('%n', () => {});
|
||||
· ──────────────────────────────
|
||||
╰────
|
||||
help: All test cases must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:1:1]
|
||||
1 │ it.each``('%n', () => {});
|
||||
· ─────────────────────────
|
||||
╰────
|
||||
help: All test cases must be wrapped in a describe block.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:4:17]
|
||||
3 │ describe("two", () => {});
|
||||
4 │ describe("three", () => {});
|
||||
· ───────────────────────────
|
||||
5 │
|
||||
╰────
|
||||
help: There should not be more than 2 describes at the top level.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:11:17]
|
||||
10 │ });
|
||||
11 │ ╭─▶ describe('three', () => {
|
||||
12 │ │ describe('one (nested)', () => {});
|
||||
13 │ │ describe('two (nested)', () => {});
|
||||
14 │ │ describe('three (nested)', () => {});
|
||||
15 │ ╰─▶ });
|
||||
16 │
|
||||
╰────
|
||||
help: There should not be more than 2 describes at the top level.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:17:17]
|
||||
16 │ });
|
||||
17 │ ╭─▶ describe3('three', () => {
|
||||
18 │ │ describe('one (nested)', () => {});
|
||||
19 │ │ describe('two (nested)', () => {});
|
||||
20 │ │ describe('three (nested)', () => {});
|
||||
21 │ ╰─▶ });
|
||||
22 │
|
||||
╰────
|
||||
help: There should not be more than 2 describes at the top level.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:3:17]
|
||||
2 │ describe('one', () => {});
|
||||
3 │ describe('two', () => {});
|
||||
· ─────────────────────────
|
||||
4 │ describe('three', () => {});
|
||||
╰────
|
||||
help: There should not be more than 1 describe at the top level.
|
||||
|
||||
⚠ eslint-plugin-jest(require-top-level-describe): Require test cases and hooks to be inside a `describe` block
|
||||
╭─[require_top_level_describe.tsx:4:17]
|
||||
3 │ describe('two', () => {});
|
||||
4 │ describe('three', () => {});
|
||||
· ───────────────────────────
|
||||
5 │
|
||||
╰────
|
||||
help: There should not be more than 1 describe at the top level.
|
||||
Loading…
Reference in a new issue