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:
cinchen 2024-06-11 20:13:31 +08:00 committed by GitHub
parent 1959930ee7
commit 85c3b83f5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 519 additions and 0 deletions

View file

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

View 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();
}

View 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