mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 20:28:58 +00:00
feat(linter): eslint-plugin-jest/max-nested-describes (#3585)
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/max-nested-describe.ts)
This commit is contained in:
parent
1959930ee7
commit
85c3b83f5f
3 changed files with 519 additions and 0 deletions
|
|
@ -149,6 +149,7 @@ mod typescript {
|
|||
mod jest {
|
||||
pub mod expect_expect;
|
||||
pub mod max_expects;
|
||||
pub mod max_nested_describe;
|
||||
pub mod no_alias_methods;
|
||||
pub mod no_commented_out_tests;
|
||||
pub mod no_conditional_expect;
|
||||
|
|
@ -529,6 +530,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
typescript::prefer_literal_enum_member,
|
||||
jest::expect_expect,
|
||||
jest::max_expects,
|
||||
jest::max_nested_describe,
|
||||
jest::no_alias_methods,
|
||||
jest::no_commented_out_tests,
|
||||
jest::no_conditional_expect,
|
||||
|
|
|
|||
397
crates/oxc_linter/src/rules/jest/max_nested_describe.rs
Normal file
397
crates/oxc_linter/src/rules/jest/max_nested_describe.rs
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
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, is_type_of_jest_fn_call, JestFnKind, JestGeneralFnKind,
|
||||
PossibleJestNode,
|
||||
},
|
||||
};
|
||||
|
||||
fn exceeded_max_depth(current: usize, max: usize, span0: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn(
|
||||
"eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.",
|
||||
)
|
||||
.with_help(format!("Too many nested describe calls ({current}) - maximum allowed is {max}"))
|
||||
.with_labels([span0.into()])
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MaxNestedDescribe {
|
||||
pub max: usize,
|
||||
}
|
||||
|
||||
impl Default for MaxNestedDescribe {
|
||||
fn default() -> Self {
|
||||
Self { max: 5 }
|
||||
}
|
||||
}
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// This rule enforces a maximum depth to nested `describe()` calls to improve code
|
||||
/// clarity in your tests.
|
||||
///
|
||||
/// The following patterns are considered warnings (with the default option of
|
||||
/// `{ "max": 5 } `):
|
||||
///
|
||||
/// ### Example
|
||||
///
|
||||
/// ```javascript
|
||||
///
|
||||
/// // invalid
|
||||
/// describe('foo', () => {
|
||||
/// describe('bar', () => {
|
||||
/// describe('baz', () => {
|
||||
/// describe('qux', () => {
|
||||
/// describe('quxx', () => {
|
||||
/// describe('too many', () => {
|
||||
/// it('should get something', () => {
|
||||
/// expect(getSomething()).toBe('Something');
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
///
|
||||
/// describe('foo', function () {
|
||||
/// describe('bar', function () {
|
||||
/// describe('baz', function () {
|
||||
/// describe('qux', function () {
|
||||
/// describe('quxx', function () {
|
||||
/// describe('too many', function () {
|
||||
/// it('should get something', () => {
|
||||
/// expect(getSomething()).toBe('Something');
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
///
|
||||
/// // valid
|
||||
/// describe('foo', () => {
|
||||
/// describe('bar', () => {
|
||||
/// it('should get something', () => {
|
||||
/// expect(getSomething()).toBe('Something');
|
||||
/// });
|
||||
/// });
|
||||
/// describe('qux', () => {
|
||||
/// it('should get something', () => {
|
||||
/// expect(getSomething()).toBe('Something');
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
///
|
||||
/// describe('foo2', function () {
|
||||
/// it('should get something', () => {
|
||||
/// expect(getSomething()).toBe('Something');
|
||||
/// });
|
||||
/// });
|
||||
///
|
||||
/// describe('foo', function () {
|
||||
/// describe('bar', function () {
|
||||
/// describe('baz', function () {
|
||||
/// describe('qux', function () {
|
||||
/// describe('this is the limit', function () {
|
||||
/// it('should get something', () => {
|
||||
/// expect(getSomething()).toBe('Something');
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
MaxNestedDescribe,
|
||||
style,
|
||||
);
|
||||
|
||||
impl Rule for MaxNestedDescribe {
|
||||
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 describes_hooks_depth: Vec<ScopeId> = vec![];
|
||||
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 describes_hooks_depth, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MaxNestedDescribe {
|
||||
fn run<'a>(
|
||||
&self,
|
||||
possible_jest_node: &PossibleJestNode<'a, '_>,
|
||||
describes_hooks_depth: &mut Vec<ScopeId>,
|
||||
ctx: &LintContext<'a>,
|
||||
) {
|
||||
let node = possible_jest_node.node;
|
||||
let scope_id = node.scope_id();
|
||||
let AstKind::CallExpression(call_expr) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
let is_describe_call = is_type_of_jest_fn_call(
|
||||
call_expr,
|
||||
possible_jest_node,
|
||||
ctx,
|
||||
&[JestFnKind::General(JestGeneralFnKind::Describe)],
|
||||
);
|
||||
|
||||
if is_describe_call && !describes_hooks_depth.contains(&scope_id) {
|
||||
describes_hooks_depth.push(scope_id);
|
||||
}
|
||||
|
||||
if is_describe_call && describes_hooks_depth.len() > self.max {
|
||||
ctx.diagnostic(exceeded_max_depth(
|
||||
describes_hooks_depth.len(),
|
||||
self.max,
|
||||
call_expr.span,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
(
|
||||
"
|
||||
describe('foo', function() {
|
||||
describe('bar', function () {
|
||||
describe('baz', function () {
|
||||
describe('qux', function () {
|
||||
describe('qux', function () {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('foo', function() {
|
||||
describe('bar', function () {
|
||||
describe('baz', function () {
|
||||
describe('qux', function () {
|
||||
describe('qux', function () {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
});
|
||||
|
||||
fdescribe('qux', () => {
|
||||
it('something', async () => {
|
||||
expect('something').toBe('something');
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('foo', () => {
|
||||
describe('bar', () => {
|
||||
it('hello', async () => {
|
||||
expect('hello').toBe('hello');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('foo', function() {
|
||||
describe('bar', function() {
|
||||
it('something', async () => {
|
||||
expect('something').toBe('something');
|
||||
});
|
||||
});
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('foo', () => {
|
||||
describe.only('bar', () => {
|
||||
describe.skip('baz', () => {
|
||||
it('something', async () => {
|
||||
expect('something').toBe('something');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
",
|
||||
Some(serde_json::json!([{ "max": 3 }])),
|
||||
),
|
||||
(
|
||||
"
|
||||
it('something', async () => {
|
||||
expect('something').toBe('something');
|
||||
});
|
||||
",
|
||||
Some(serde_json::json!([{ "max": 0 }])),
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('foo', () => {
|
||||
describe.each(['hello', 'world'])(\"%s\", (a) => {});
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('foo', () => {
|
||||
describe.each`
|
||||
foo | bar
|
||||
${'1'} | ${'2'}
|
||||
`('$foo $bar', ({ foo, bar }) => {});
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
(
|
||||
"
|
||||
describe('foo', function() {
|
||||
describe('bar', function () {
|
||||
describe('baz', function () {
|
||||
describe('qux', function () {
|
||||
describe('quxx', function () {
|
||||
describe('over limit', function () {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('foo', () => {
|
||||
describe('bar', () => {
|
||||
describe('baz', () => {
|
||||
describe('baz1', () => {
|
||||
describe('baz2', () => {
|
||||
describe('baz3', () => {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
});
|
||||
|
||||
describe('baz4', () => {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('qux', function () {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
fdescribe('foo', () => {
|
||||
describe.only('bar', () => {
|
||||
describe.skip('baz', () => {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
});
|
||||
|
||||
describe('baz', () => {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('qux', () => {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
});
|
||||
",
|
||||
Some(serde_json::json!([{ "max": 2 }])),
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('qux', () => {
|
||||
it('should get something', () => {
|
||||
expect(getSomething()).toBe('Something');
|
||||
});
|
||||
});
|
||||
",
|
||||
Some(serde_json::json!([{ "max": 0 }])),
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('foo', () => {
|
||||
describe.each(['hello', 'world'])(\"%s\", (a) => {});
|
||||
});
|
||||
",
|
||||
Some(serde_json::json!([{ "max": 1 }])),
|
||||
),
|
||||
(
|
||||
"
|
||||
describe('foo', () => {
|
||||
describe.each`
|
||||
foo | bar
|
||||
${'1'} | ${'2'}
|
||||
`('$foo $bar', ({ foo, bar }) => {});
|
||||
});
|
||||
",
|
||||
Some(serde_json::json!([{ "max": 1 }])),
|
||||
),
|
||||
];
|
||||
|
||||
Tester::new(MaxNestedDescribe::NAME, pass, fail).with_jest_plugin(true).test_and_snapshot();
|
||||
}
|
||||
120
crates/oxc_linter/src/snapshots/max_nested_describe.snap
Normal file
120
crates/oxc_linter/src/snapshots/max_nested_describe.snap
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
assertion_line: 203
|
||||
expression: max_nested_describe
|
||||
---
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:7:37]
|
||||
6 │ describe('quxx', function () {
|
||||
7 │ ╭─▶ describe('over limit', function () {
|
||||
8 │ │ it('should get something', () => {
|
||||
9 │ │ expect(getSomething()).toBe('Something');
|
||||
10 │ │ });
|
||||
11 │ ╰─▶ });
|
||||
12 │ });
|
||||
╰────
|
||||
help: Too many nested describe calls (6) - maximum allowed is 5
|
||||
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:7:37]
|
||||
6 │ describe('baz2', () => {
|
||||
7 │ ╭─▶ describe('baz3', () => {
|
||||
8 │ │ it('should get something', () => {
|
||||
9 │ │ expect(getSomething()).toBe('Something');
|
||||
10 │ │ });
|
||||
11 │ ╰─▶ });
|
||||
12 │
|
||||
╰────
|
||||
help: Too many nested describe calls (6) - maximum allowed is 5
|
||||
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:13:37]
|
||||
12 │
|
||||
13 │ ╭─▶ describe('baz4', () => {
|
||||
14 │ │ it('should get something', () => {
|
||||
15 │ │ expect(getSomething()).toBe('Something');
|
||||
16 │ │ });
|
||||
17 │ ╰─▶ });
|
||||
18 │ });
|
||||
╰────
|
||||
help: Too many nested describe calls (6) - maximum allowed is 5
|
||||
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:22:25]
|
||||
21 │
|
||||
22 │ ╭─▶ describe('qux', function () {
|
||||
23 │ │ it('should get something', () => {
|
||||
24 │ │ expect(getSomething()).toBe('Something');
|
||||
25 │ │ });
|
||||
26 │ ╰─▶ });
|
||||
27 │ })
|
||||
╰────
|
||||
help: Too many nested describe calls (6) - maximum allowed is 5
|
||||
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:4:25]
|
||||
3 │ describe.only('bar', () => {
|
||||
4 │ ╭─▶ describe.skip('baz', () => {
|
||||
5 │ │ it('should get something', () => {
|
||||
6 │ │ expect(getSomething()).toBe('Something');
|
||||
7 │ │ });
|
||||
8 │ ╰─▶ });
|
||||
9 │
|
||||
╰────
|
||||
help: Too many nested describe calls (3) - maximum allowed is 2
|
||||
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:10:25]
|
||||
9 │
|
||||
10 │ ╭─▶ describe('baz', () => {
|
||||
11 │ │ it('should get something', () => {
|
||||
12 │ │ expect(getSomething()).toBe('Something');
|
||||
13 │ │ });
|
||||
14 │ ╰─▶ });
|
||||
15 │ });
|
||||
╰────
|
||||
help: Too many nested describe calls (3) - maximum allowed is 2
|
||||
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:18:17]
|
||||
17 │
|
||||
18 │ ╭─▶ xdescribe('qux', () => {
|
||||
19 │ │ it('should get something', () => {
|
||||
20 │ │ expect(getSomething()).toBe('Something');
|
||||
21 │ │ });
|
||||
22 │ ╰─▶ });
|
||||
23 │
|
||||
╰────
|
||||
help: Too many nested describe calls (3) - maximum allowed is 2
|
||||
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:2:17]
|
||||
1 │
|
||||
2 │ ╭─▶ describe('qux', () => {
|
||||
3 │ │ it('should get something', () => {
|
||||
4 │ │ expect(getSomething()).toBe('Something');
|
||||
5 │ │ });
|
||||
6 │ ╰─▶ });
|
||||
7 │
|
||||
╰────
|
||||
help: Too many nested describe calls (1) - maximum allowed is 0
|
||||
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:3:21]
|
||||
2 │ describe('foo', () => {
|
||||
3 │ describe.each(['hello', 'world'])("%s", (a) => {});
|
||||
· ──────────────────────────────────────────────────
|
||||
4 │ });
|
||||
╰────
|
||||
help: Too many nested describe calls (2) - maximum allowed is 1
|
||||
|
||||
⚠ eslint-plugin-jest(max-nested-describe): Enforces a maximum depth to nested describe calls.
|
||||
╭─[max_nested_describe.tsx:3:21]
|
||||
2 │ describe('foo', () => {
|
||||
3 │ ╭─▶ describe.each`
|
||||
4 │ │ foo | bar
|
||||
5 │ │ ${'1'} | ${'2'}
|
||||
6 │ ╰─▶ `('$foo $bar', ({ foo, bar }) => {});
|
||||
7 │ });
|
||||
╰────
|
||||
help: Too many nested describe calls (2) - maximum allowed is 1
|
||||
Loading…
Reference in a new issue