From fb549e1288738691d7517dd9b07a89b73b512db6 Mon Sep 17 00:00:00 2001 From: mysteryven <33973865+mysteryven@users.noreply.github.com> Date: Thu, 11 Jul 2024 05:37:04 +0000 Subject: [PATCH] feat(linter): add vitest/no-focused-tests rule (#4178) Rule detail: https://github.com/veritem/eslint-plugin-vitest/blob/main/docs/rules/no-focused-tests.md --- .../src/rules/jest/consistent_test_it.rs | 13 +- .../src/rules/jest/no_disabled_tests.rs | 11 +- .../src/rules/jest/no_focused_tests.rs | 112 ++++++++++++++---- .../src/rules/jest/prefer_hooks_in_order.rs | 4 +- .../src/snapshots/no_focused_tests.snap | 107 ++++++++++++----- crates/oxc_linter/src/utils/mod.rs | 25 +++- 6 files changed, 205 insertions(+), 67 deletions(-) diff --git a/crates/oxc_linter/src/rules/jest/consistent_test_it.rs b/crates/oxc_linter/src/rules/jest/consistent_test_it.rs index ec35b6ee5833..60c272676575 100644 --- a/crates/oxc_linter/src/rules/jest/consistent_test_it.rs +++ b/crates/oxc_linter/src/rules/jest/consistent_test_it.rs @@ -12,11 +12,11 @@ use crate::{ rule::Rule, utils::{ collect_possible_jest_call_node, get_test_plugin_name, parse_jest_fn_call, JestFnKind, - JestGeneralFnKind, ParsedJestFnCallNew, PossibleJestNode, + JestGeneralFnKind, ParsedJestFnCallNew, PossibleJestNode, TestPluginName, }, }; -fn consistent_method(x0: &str, x1: &str, x2: &str, span0: Span) -> OxcDiagnostic { +fn consistent_method(x0: TestPluginName, x1: &str, x2: &str, span0: Span) -> OxcDiagnostic { OxcDiagnostic::warn(format!( "{x0}(consistent-test-it): Enforce `test` and `it` usage conventions", )) @@ -24,7 +24,12 @@ fn consistent_method(x0: &str, x1: &str, x2: &str, span0: Span) -> OxcDiagnostic .with_label(span0) } -fn consistent_method_within_describe(x0: &str, x1: &str, x2: &str, span0: Span) -> OxcDiagnostic { +fn consistent_method_within_describe( + x0: TestPluginName, + x1: &str, + x2: &str, + span0: Span, +) -> OxcDiagnostic { OxcDiagnostic::warn(format!( "{x0}(consistent-test-it): Enforce `test` and `it` usage conventions", )) @@ -211,7 +216,7 @@ impl ConsistentTestIt { fn run<'a>( &self, describe_nesting_hash: &mut FxHashMap, - plugin_name: &str, + plugin_name: TestPluginName, possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>, ) { diff --git a/crates/oxc_linter/src/rules/jest/no_disabled_tests.rs b/crates/oxc_linter/src/rules/jest/no_disabled_tests.rs index d7dd3b49b44f..9aec9bb5e398 100644 --- a/crates/oxc_linter/src/rules/jest/no_disabled_tests.rs +++ b/crates/oxc_linter/src/rules/jest/no_disabled_tests.rs @@ -8,7 +8,7 @@ use crate::{ rule::Rule, utils::{ collect_possible_jest_call_node, get_test_plugin_name, parse_general_jest_fn_call, - JestFnKind, JestGeneralFnKind, ParsedGeneralJestFnCall, PossibleJestNode, + JestFnKind, JestGeneralFnKind, ParsedGeneralJestFnCall, PossibleJestNode, TestPluginName, }, }; @@ -63,7 +63,12 @@ declare_oxc_lint!( correctness ); -fn no_disabled_tests_diagnostic(x0: &str, x1: &str, x2: &str, span3: Span) -> OxcDiagnostic { +fn no_disabled_tests_diagnostic( + x0: TestPluginName, + x1: &str, + x2: &str, + span3: Span, +) -> OxcDiagnostic { OxcDiagnostic::warn(format!("{x0}(no-disabled-tests): {x1:?}")) .with_help(format!("{x2:?}")) .with_label(span3) @@ -103,7 +108,7 @@ impl Rule for NoDisabledTests { fn run<'a>( possible_jest_node: &PossibleJestNode<'a, '_>, - plugin_name: &str, + plugin_name: TestPluginName, ctx: &LintContext<'a>, ) { let node = possible_jest_node.node; diff --git a/crates/oxc_linter/src/rules/jest/no_focused_tests.rs b/crates/oxc_linter/src/rules/jest/no_focused_tests.rs index a5ab63dae3bc..1acbc1ae7ff5 100644 --- a/crates/oxc_linter/src/rules/jest/no_focused_tests.rs +++ b/crates/oxc_linter/src/rules/jest/no_focused_tests.rs @@ -7,15 +7,24 @@ use crate::{ context::LintContext, rule::Rule, utils::{ - collect_possible_jest_call_node, parse_general_jest_fn_call, JestFnKind, JestGeneralFnKind, - MemberExpressionElement, ParsedGeneralJestFnCall, PossibleJestNode, + collect_possible_jest_call_node, get_test_plugin_name, parse_general_jest_fn_call, + JestFnKind, JestGeneralFnKind, MemberExpressionElement, ParsedGeneralJestFnCall, + PossibleJestNode, TestPluginName, }, }; -fn no_focused_tests_diagnostic(span0: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("eslint-plugin-jest(no-focused-tests): Unexpected focused test.") - .with_help("Remove focus from test.") - .with_label(span0) +fn no_focused_tests_diagnostic(span0: Span, x1: TestPluginName) -> OxcDiagnostic { + match x1 { + TestPluginName::Jest => { + OxcDiagnostic::warn(format!("{x1}(no-focused-tests): Unexpected focused test.")) + .with_help("Remove focus from test.") + .with_label(span0) + } + TestPluginName::Vitest => { + OxcDiagnostic::warn(format!("{x1}(no-focused-tests): Focused tests are not allowed.")) + .with_label(span0) + } + } } #[derive(Debug, Default, Clone)] @@ -49,19 +58,35 @@ declare_oxc_lint!( /// table /// `(); /// ``` + /// + /// This rule is compatible with [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest/blob/main/docs/rules/no-focused-tests.md), + /// to use it, add the following configuration to your `.eslintrc.json`: + /// + /// ```json + /// { + /// "rules": { + /// "vitest/no-focused-tests": "error" + /// } + /// } + /// ``` NoFocusedTests, correctness ); impl Rule for NoFocusedTests { fn run_once(&self, ctx: &LintContext) { + let plugin_name = get_test_plugin_name(ctx); for node in &collect_possible_jest_call_node(ctx) { - run(node, ctx); + run(node, plugin_name, ctx); } } } -fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) { +fn run<'a>( + possible_jest_node: &PossibleJestNode<'a, '_>, + plugin_name: TestPluginName, + ctx: &LintContext<'a>, +) { let node = possible_jest_node.node; let AstKind::CallExpression(call_expr) = node.kind() else { return; @@ -75,22 +100,32 @@ fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) } if name.starts_with('f') { - ctx.diagnostic_with_fix(no_focused_tests_diagnostic(call_expr.span), |fixer| { - fixer.delete_range(Span::sized(call_expr.span.start, 1)) - }); + ctx.diagnostic_with_fix( + no_focused_tests_diagnostic( + Span::new( + call_expr.span.start, + call_expr.span.start + u32::try_from(name.len()).unwrap_or(1), + ), + plugin_name, + ), + |fixer| fixer.delete_range(Span::sized(call_expr.span.start, 1)), + ); return; } let only_node = members.iter().find(|member| member.is_name_equal("only")); if let Some(only_node) = only_node { - ctx.diagnostic_with_fix(no_focused_tests_diagnostic(call_expr.span), |fixer| { - let mut span = only_node.span.expand_left(1); - if !matches!(only_node.element, MemberExpressionElement::IdentName(_)) { - span = span.expand_right(1); - } - fixer.delete_range(span) - }); + ctx.diagnostic_with_fix( + no_focused_tests_diagnostic(only_node.span, plugin_name), + |fixer| { + let mut span = only_node.span.expand_left(1); + if !matches!(only_node.element, MemberExpressionElement::IdentName(_)) { + span = span.expand_right(1); + } + fixer.delete_range(span) + }, + ); } } @@ -98,7 +133,7 @@ fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) fn test() { use crate::tester::Tester; - let pass = vec![ + let mut pass = vec![ ("describe()", None), ("it()", None), ("describe.skip()", None), @@ -114,7 +149,7 @@ fn test() { ("test.concurrent()", None), ]; - let fail = vec![ + let mut fail = vec![ ("describe.only()", None), // TODO: this need set setting like `settings: { jest: { globalAliases: { describe: ['context'] } } },` // ("context.only()", None), @@ -137,12 +172,47 @@ fn test() { ("fit.each`table`()", None), ]; - let fix = vec![ + let mut fix = vec![ ("describe.only('foo', () => {})", "describe('foo', () => {})", None), ("describe['only']('foo', () => {})", "describe('foo', () => {})", None), ("fdescribe('foo', () => {})", "describe('foo', () => {})", None), ]; + let pass_vitest = vec![ + (r#"it("test", () => {});"#, None), + (r#"describe("test group", () => {});"#, None), + (r#"it("test", () => {});"#, None), + (r#"describe("test group", () => {});"#, None), + ]; + + let fail_vitest = vec![ + ( + r#" + import { it } from 'vitest'; + it.only("test", () => {}); + "#, + None, + ), + (r#"describe.only("test", () => {});"#, None), + (r#"test.only("test", () => {});"#, None), + (r#"it.only.each([])("test", () => {});"#, None), + (r#"test.only.each``("test", () => {});"#, None), + (r#"it.only.each``("test", () => {});"#, None), + ]; + + let fix_vitest = vec![ + (r#"it.only("test", () => {});"#, r#"it("test", () => {});"#, None), + (r#"describe.only("test", () => {});"#, r#"describe("test", () => {});"#, None), + (r#"test.only("test", () => {});"#, r#"test("test", () => {});"#, None), + (r#"it.only.each([])("test", () => {});"#, r#"it.each([])("test", () => {});"#, None), + (r#"test.only.each``("test", () => {});"#, r#"test.each``("test", () => {});"#, None), + (r#"it.only.each``("test", () => {});"#, r#"it.each``("test", () => {});"#, None), + ]; + + pass.extend(pass_vitest); + fail.extend(fail_vitest); + fix.extend(fix_vitest); + Tester::new(NoFocusedTests::NAME, pass, fail) .with_jest_plugin(true) .expect_fix(fix) diff --git a/crates/oxc_linter/src/rules/jest/prefer_hooks_in_order.rs b/crates/oxc_linter/src/rules/jest/prefer_hooks_in_order.rs index 70bd6b730ce1..a983a2f79559 100644 --- a/crates/oxc_linter/src/rules/jest/prefer_hooks_in_order.rs +++ b/crates/oxc_linter/src/rules/jest/prefer_hooks_in_order.rs @@ -10,11 +10,11 @@ use crate::{ rule::Rule, utils::{ get_test_plugin_name, parse_jest_fn_call, JestFnKind, JestGeneralFnKind, - ParsedJestFnCallNew, PossibleJestNode, + ParsedJestFnCallNew, PossibleJestNode, TestPluginName, }, }; -fn reorder_hooks(x0: &str, x1: &str, x2: &str, span0: Span) -> OxcDiagnostic { +fn reorder_hooks(x0: TestPluginName, x1: &str, x2: &str, span0: Span) -> OxcDiagnostic { OxcDiagnostic::warn(format!( "{x0}(prefer-hooks-in-order): Prefer having hooks in a consistent order.", )) diff --git a/crates/oxc_linter/src/snapshots/no_focused_tests.snap b/crates/oxc_linter/src/snapshots/no_focused_tests.snap index 885f25c0b9e2..92a25a26cbfc 100644 --- a/crates/oxc_linter/src/snapshots/no_focused_tests.snap +++ b/crates/oxc_linter/src/snapshots/no_focused_tests.snap @@ -2,127 +2,170 @@ source: crates/oxc_linter/src/tester.rs --- ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:10] 1 │ describe.only() - · ─────────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:10] 1 │ describe.only.each()() - · ────────────────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:10] 1 │ describe.only.each`table`() - · ─────────────────────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:10] 1 │ describe["only"]() - · ────────────────── + · ────── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:4] 1 │ it.only() - · ───────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:15] 1 │ it.concurrent.only.each``() - · ─────────────────────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:4] 1 │ it.only.each()() - · ──────────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:4] 1 │ it.only.each`table`() - · ───────────────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:4] 1 │ it["only"]() - · ──────────── + · ────── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:6] 1 │ test.only() - · ─────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:17] 1 │ test.concurrent.only.each()() - · ───────────────────────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:6] 1 │ test.only.each()() - · ────────────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:6] 1 │ test.only.each`table`() - · ─────────────────────── + · ──── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. - ╭─[no_focused_tests.tsx:1:1] + ╭─[no_focused_tests.tsx:1:6] 1 │ test["only"]() - · ────────────── + · ────── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. ╭─[no_focused_tests.tsx:1:1] 1 │ fdescribe() - · ─────────── + · ───────── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. ╭─[no_focused_tests.tsx:1:1] 1 │ fit() - · ───── + · ─── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. ╭─[no_focused_tests.tsx:1:1] 1 │ fit.each()() - · ──────────── + · ─── ╰──── help: Remove focus from test. ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. ╭─[no_focused_tests.tsx:1:1] 1 │ fit.each`table`() - · ───────────────── + · ─── + ╰──── + help: Remove focus from test. + + ⚠ eslint-plugin-vitest(no-focused-tests): Focused tests are not allowed. + ╭─[no_focused_tests.tsx:3:16] + 2 │ import { it } from 'vitest'; + 3 │ it.only("test", () => {}); + · ──── + 4 │ + ╰──── + + ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. + ╭─[no_focused_tests.tsx:1:10] + 1 │ describe.only("test", () => {}); + · ──── + ╰──── + help: Remove focus from test. + + ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. + ╭─[no_focused_tests.tsx:1:6] + 1 │ test.only("test", () => {}); + · ──── + ╰──── + help: Remove focus from test. + + ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. + ╭─[no_focused_tests.tsx:1:4] + 1 │ it.only.each([])("test", () => {}); + · ──── + ╰──── + help: Remove focus from test. + + ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. + ╭─[no_focused_tests.tsx:1:6] + 1 │ test.only.each``("test", () => {}); + · ──── + ╰──── + help: Remove focus from test. + + ⚠ eslint-plugin-jest(no-focused-tests): Unexpected focused test. + ╭─[no_focused_tests.tsx:1:4] + 1 │ it.only.each``("test", () => {}); + · ──── ╰──── help: Remove focus from test. diff --git a/crates/oxc_linter/src/utils/mod.rs b/crates/oxc_linter/src/utils/mod.rs index e61e78a72df6..3ebd2f7c8b73 100644 --- a/crates/oxc_linter/src/utils/mod.rs +++ b/crates/oxc_linter/src/utils/mod.rs @@ -16,17 +16,32 @@ pub use self::{ /// Many Vitest rule are essentially ports of Jest plugin rules with minor modifications. /// For these rules, we use the corresponding jest rules with some adjustments for compatibility. pub fn is_jest_rule_adapted_to_vitest(rule_name: &str) -> bool { - let jest_rules: [&str; 3] = - ["consistent_test_it", "no-disabled-tests", "prefer-hooks-in-order"]; + let jest_rules: &[&str] = + &["consistent-test-it", "no-disabled-tests", "no-focused-tests", "prefer-hooks-in-order"]; jest_rules.contains(&rule_name) } -pub fn get_test_plugin_name(ctx: &LintContext) -> &'static str { +#[derive(Clone, Copy)] +pub enum TestPluginName { + Jest, + Vitest, +} + +impl std::fmt::Display for TestPluginName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TestPluginName::Jest => write!(f, "eslint-plugin-jest"), + TestPluginName::Vitest => write!(f, "eslint-plugin-vitest"), + } + } +} + +pub fn get_test_plugin_name(ctx: &LintContext) -> TestPluginName { if is_using_vitest(ctx) { - "eslint-plugin-vitest" + TestPluginName::Vitest } else { - "eslint-plugin-jest" + TestPluginName::Jest } }