mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21: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 {
|
mod jest {
|
||||||
pub mod expect_expect;
|
pub mod expect_expect;
|
||||||
pub mod max_expects;
|
pub mod max_expects;
|
||||||
|
pub mod max_nested_describe;
|
||||||
pub mod no_alias_methods;
|
pub mod no_alias_methods;
|
||||||
pub mod no_commented_out_tests;
|
pub mod no_commented_out_tests;
|
||||||
pub mod no_conditional_expect;
|
pub mod no_conditional_expect;
|
||||||
|
|
@ -529,6 +530,7 @@ oxc_macros::declare_all_lint_rules! {
|
||||||
typescript::prefer_literal_enum_member,
|
typescript::prefer_literal_enum_member,
|
||||||
jest::expect_expect,
|
jest::expect_expect,
|
||||||
jest::max_expects,
|
jest::max_expects,
|
||||||
|
jest::max_nested_describe,
|
||||||
jest::no_alias_methods,
|
jest::no_alias_methods,
|
||||||
jest::no_commented_out_tests,
|
jest::no_commented_out_tests,
|
||||||
jest::no_conditional_expect,
|
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