From 6b9e610ac3277a05c0105e135fb19fcc8c33a590 Mon Sep 17 00:00:00 2001 From: shulaoda Date: Mon, 26 Aug 2024 00:48:17 +0800 Subject: [PATCH] feat(linter/eslint-plugin-vitest): implement prefer-each --- .../src/rules/vitest/prefer_each.rs | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 crates/oxc_linter/src/rules/vitest/prefer_each.rs diff --git a/crates/oxc_linter/src/rules/vitest/prefer_each.rs b/crates/oxc_linter/src/rules/vitest/prefer_each.rs new file mode 100644 index 00000000000000..e0662c50c213b7 --- /dev/null +++ b/crates/oxc_linter/src/rules/vitest/prefer_each.rs @@ -0,0 +1,257 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_semantic::{AstNode, AstNodeId}; +use oxc_span::{GetSpan, Span}; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{parse_jest_fn_call, JestFnKind, JestGeneralFnKind, PossibleJestNode}, +}; + +fn use_prefer_each(span0: Span, fn_name: &str) -> OxcDiagnostic { + OxcDiagnostic::warn("Enforce using `each` rather than manual loops") + .with_help(format!("Prefer using `{fn_name}.each` rather than a manual loop.")) + .with_label(span0) +} + +#[inline] +fn is_in_test(ctx: &LintContext<'_>, id: AstNodeId) -> bool { + ctx.nodes().iter_parents(id).any(|node| { + if let AstKind::CallExpression(ancestor_call_expr) = node.kind() { + if let Some(ancestor_member_expr) = ancestor_call_expr.callee.as_member_expression() { + if let Some(id) = ancestor_member_expr.object().get_identifier_reference() { + return matches!( + JestFnKind::from(id.name.as_str()), + JestFnKind::General(JestGeneralFnKind::Test) + ); + } + return false; + } + } + false + }) +} + +#[derive(Debug, Default, Clone)] +pub struct PreferEach; + +declare_oxc_lint!( + /// ### What it does + /// This rule enforces using `each` rather than manual loops. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// for (const item of items) { + /// describe(item, () => { + /// expect(item).toBe('foo') + /// }) + /// } + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// describe.each(items)('item', (item) => { + /// expect(item).toBe('foo') + /// }) + /// ``` + PreferEach, + style, +); + +impl Rule for PreferEach { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let kind = node.kind(); + + if let AstKind::CallExpression(call_expr) = kind { + let Some(vitest_fn_call) = + parse_jest_fn_call(call_expr, &PossibleJestNode { node, original: None }, ctx) + else { + return; + }; + + if matches!( + vitest_fn_call.kind(), + JestFnKind::General( + JestGeneralFnKind::Describe | JestGeneralFnKind::Hook | JestGeneralFnKind::Test + ) + ) { + for parent_node in ctx.nodes().iter_parents(node.id()).skip(1) { + match parent_node.kind() { + AstKind::CallExpression(_) => { + return; + } + AstKind::ForStatement(_) + | AstKind::ForInStatement(_) + | AstKind::ForOfStatement(_) => { + if !is_in_test(ctx, parent_node.id()) { + let fn_name = if matches!( + vitest_fn_call.kind(), + JestFnKind::General(JestGeneralFnKind::Test) + ) { + "it" + } else { + "describe" + }; + + ctx.diagnostic(use_prefer_each(parent_node.span(), fn_name)); + } + + break; + } + _ => (), + } + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r#"it("is true", () => { expect(true).toBe(false) });"#, + r#"it.each(getNumbers())("only returns numbers that are greater than seven", number => { + expect(number).toBeGreaterThan(7); + });"#, + r#"it("returns numbers that are greater than five", function () { + for (const number of getNumbers()) { + expect(number).toBeGreaterThan(5); + } + });"#, + r#"it("returns things that are less than ten", function () { + for (const thing in things) { + expect(thing).toBeLessThan(10); + } + });"#, + r#"it("only returns numbers that are greater than seven", function () { + const numbers = getNumbers(); + + for (let i = 0; i < numbers.length; i++) { + expect(numbers[i]).toBeGreaterThan(7); + } + });"#, + ]; + + let fail = vec![ + " for (const [input, expected] of data) { + it(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + }", + " for (const [input, expected] of data) { + describe(`when the input is ${input}`, () => { + it(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + }); + }", + "for (const [input, expected] of data) { + describe(`when the input is ${input}`, () => { + it(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + }); + } + + for (const [input, expected] of data) { + it.skip(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + }", + "for (const [input, expected] of data) { + it.skip(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + }", + "it('is true', () => { + expect(true).toBe(false); + }); + + for (const [input, expected] of data) { + it.skip(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + }", + " for (const [input, expected] of data) { + it.skip(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + } + + it('is true', () => { + expect(true).toBe(false); + });", + " it('is true', () => { + expect(true).toBe(false); + }); + + for (const [input, expected] of data) { + it.skip(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + } + + it('is true', () => { + expect(true).toBe(false); + });", + "for (const [input, expected] of data) { + it(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + + it(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + }", + "for (const [input, expected] of data) { + it(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + } + + for (const [input, expected] of data) { + it(`results in ${expected}`, () => { + expect(fn(input)).toBe(expected) + }); + }", + "for (const [input, expected] of data) { + beforeEach(() => setupSomething(input)); + + test(`results in ${expected}`, () => { + expect(doSomething()).toBe(expected) + }); + }", + r#" + for (const [input, expected] of data) { + it("only returns numbers that are greater than seven", function () { + const numbers = getNumbers(input); + + for (let i = 0; i < numbers.length; i++) { + expect(numbers[i]).toBeGreaterThan(7); + } + }); + } + "#, + r#" + for (const [input, expected] of data) { + beforeEach(() => setupSomething(input)); + + it("only returns numbers that are greater than seven", function () { + const numbers = getNumbers(); + + for (let i = 0; i < numbers.length; i++) { + expect(numbers[i]).toBeGreaterThan(7); + } + }); + } + "#, + ]; + + Tester::new(PreferEach::NAME, pass, fail).test_and_snapshot(); +}