diff --git a/crates/oxc_linter/data/vitest_compatible_jest_rules.json b/crates/oxc_linter/data/vitest_compatible_jest_rules.json index d88d35eed5ca6..f448e4631c845 100644 --- a/crates/oxc_linter/data/vitest_compatible_jest_rules.json +++ b/crates/oxc_linter/data/vitest_compatible_jest_rules.json @@ -1,5 +1,4 @@ [ - "consistent-test-it", "expect-expect", "max-expects", "max-nested-describe", diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 4809ca2b483f9..eb3531753e997 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -4223,6 +4223,11 @@ impl RuleRunner for crate::rules::vitest::consistent_test_filename::ConsistentTe const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::RunOnce; } +impl RuleRunner for crate::rules::vitest::consistent_test_it::ConsistentTestIt { + const NODE_TYPES: Option<&AstTypesBitset> = None; + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::RunOnce; +} + impl RuleRunner for crate::rules::vitest::consistent_vitest_vi::ConsistentVitestVi { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[AstType::CallExpression, AstType::ImportDeclaration])); diff --git a/crates/oxc_linter/src/generated/rules_enum.rs b/crates/oxc_linter/src/generated/rules_enum.rs index c13dc07e17eff..2d4d5547465a7 100644 --- a/crates/oxc_linter/src/generated/rules_enum.rs +++ b/crates/oxc_linter/src/generated/rules_enum.rs @@ -675,6 +675,7 @@ pub use crate::rules::unicorn::text_encoding_identifier_case::TextEncodingIdenti pub use crate::rules::unicorn::throw_new_error::ThrowNewError as UnicornThrowNewError; pub use crate::rules::vitest::consistent_each_for::ConsistentEachFor as VitestConsistentEachFor; pub use crate::rules::vitest::consistent_test_filename::ConsistentTestFilename as VitestConsistentTestFilename; +pub use crate::rules::vitest::consistent_test_it::ConsistentTestIt as VitestConsistentTestIt; pub use crate::rules::vitest::consistent_vitest_vi::ConsistentVitestVi as VitestConsistentVitestVi; pub use crate::rules::vitest::hoisted_apis_on_top::HoistedApisOnTop as VitestHoistedApisOnTop; pub use crate::rules::vitest::no_conditional_tests::NoConditionalTests as VitestNoConditionalTests; @@ -1380,6 +1381,7 @@ pub enum RuleEnum { PromiseValidParams(PromiseValidParams), VitestConsistentEachFor(VitestConsistentEachFor), VitestConsistentTestFilename(VitestConsistentTestFilename), + VitestConsistentTestIt(VitestConsistentTestIt), VitestConsistentVitestVi(VitestConsistentVitestVi), VitestHoistedApisOnTop(VitestHoistedApisOnTop), VitestNoConditionalTests(VitestNoConditionalTests), @@ -2164,7 +2166,8 @@ const PROMISE_SPEC_ONLY_ID: usize = PROMISE_PREFER_CATCH_ID + 1usize; const PROMISE_VALID_PARAMS_ID: usize = PROMISE_SPEC_ONLY_ID + 1usize; const VITEST_CONSISTENT_EACH_FOR_ID: usize = PROMISE_VALID_PARAMS_ID + 1usize; const VITEST_CONSISTENT_TEST_FILENAME_ID: usize = VITEST_CONSISTENT_EACH_FOR_ID + 1usize; -const VITEST_CONSISTENT_VITEST_VI_ID: usize = VITEST_CONSISTENT_TEST_FILENAME_ID + 1usize; +const VITEST_CONSISTENT_TEST_IT_ID: usize = VITEST_CONSISTENT_TEST_FILENAME_ID + 1usize; +const VITEST_CONSISTENT_VITEST_VI_ID: usize = VITEST_CONSISTENT_TEST_IT_ID + 1usize; const VITEST_HOISTED_APIS_ON_TOP_ID: usize = VITEST_CONSISTENT_VITEST_VI_ID + 1usize; const VITEST_NO_CONDITIONAL_TESTS_ID: usize = VITEST_HOISTED_APIS_ON_TOP_ID + 1usize; const VITEST_NO_IMPORT_NODE_TEST_ID: usize = VITEST_NO_CONDITIONAL_TESTS_ID + 1usize; @@ -2975,6 +2978,7 @@ impl RuleEnum { Self::PromiseValidParams(_) => PROMISE_VALID_PARAMS_ID, Self::VitestConsistentEachFor(_) => VITEST_CONSISTENT_EACH_FOR_ID, Self::VitestConsistentTestFilename(_) => VITEST_CONSISTENT_TEST_FILENAME_ID, + Self::VitestConsistentTestIt(_) => VITEST_CONSISTENT_TEST_IT_ID, Self::VitestConsistentVitestVi(_) => VITEST_CONSISTENT_VITEST_VI_ID, Self::VitestHoistedApisOnTop(_) => VITEST_HOISTED_APIS_ON_TOP_ID, Self::VitestNoConditionalTests(_) => VITEST_NO_CONDITIONAL_TESTS_ID, @@ -3775,6 +3779,7 @@ impl RuleEnum { Self::PromiseValidParams(_) => PromiseValidParams::NAME, Self::VitestConsistentEachFor(_) => VitestConsistentEachFor::NAME, Self::VitestConsistentTestFilename(_) => VitestConsistentTestFilename::NAME, + Self::VitestConsistentTestIt(_) => VitestConsistentTestIt::NAME, Self::VitestConsistentVitestVi(_) => VitestConsistentVitestVi::NAME, Self::VitestHoistedApisOnTop(_) => VitestHoistedApisOnTop::NAME, Self::VitestNoConditionalTests(_) => VitestNoConditionalTests::NAME, @@ -4617,6 +4622,7 @@ impl RuleEnum { Self::PromiseValidParams(_) => PromiseValidParams::CATEGORY, Self::VitestConsistentEachFor(_) => VitestConsistentEachFor::CATEGORY, Self::VitestConsistentTestFilename(_) => VitestConsistentTestFilename::CATEGORY, + Self::VitestConsistentTestIt(_) => VitestConsistentTestIt::CATEGORY, Self::VitestConsistentVitestVi(_) => VitestConsistentVitestVi::CATEGORY, Self::VitestHoistedApisOnTop(_) => VitestHoistedApisOnTop::CATEGORY, Self::VitestNoConditionalTests(_) => VitestNoConditionalTests::CATEGORY, @@ -5422,6 +5428,7 @@ impl RuleEnum { Self::PromiseValidParams(_) => PromiseValidParams::FIX, Self::VitestConsistentEachFor(_) => VitestConsistentEachFor::FIX, Self::VitestConsistentTestFilename(_) => VitestConsistentTestFilename::FIX, + Self::VitestConsistentTestIt(_) => VitestConsistentTestIt::FIX, Self::VitestConsistentVitestVi(_) => VitestConsistentVitestVi::FIX, Self::VitestHoistedApisOnTop(_) => VitestHoistedApisOnTop::FIX, Self::VitestNoConditionalTests(_) => VitestNoConditionalTests::FIX, @@ -6415,6 +6422,7 @@ impl RuleEnum { Self::PromiseValidParams(_) => PromiseValidParams::documentation(), Self::VitestConsistentEachFor(_) => VitestConsistentEachFor::documentation(), Self::VitestConsistentTestFilename(_) => VitestConsistentTestFilename::documentation(), + Self::VitestConsistentTestIt(_) => VitestConsistentTestIt::documentation(), Self::VitestConsistentVitestVi(_) => VitestConsistentVitestVi::documentation(), Self::VitestHoistedApisOnTop(_) => VitestHoistedApisOnTop::documentation(), Self::VitestNoConditionalTests(_) => VitestNoConditionalTests::documentation(), @@ -8348,6 +8356,8 @@ impl RuleEnum { VitestConsistentTestFilename::config_schema(generator) .or_else(|| VitestConsistentTestFilename::schema(generator)) } + Self::VitestConsistentTestIt(_) => VitestConsistentTestIt::config_schema(generator) + .or_else(|| VitestConsistentTestIt::schema(generator)), Self::VitestConsistentVitestVi(_) => VitestConsistentVitestVi::config_schema(generator) .or_else(|| VitestConsistentVitestVi::schema(generator)), Self::VitestHoistedApisOnTop(_) => VitestHoistedApisOnTop::config_schema(generator) @@ -9116,6 +9126,7 @@ impl RuleEnum { Self::PromiseValidParams(_) => "promise", Self::VitestConsistentEachFor(_) => "vitest", Self::VitestConsistentTestFilename(_) => "vitest", + Self::VitestConsistentTestIt(_) => "vitest", Self::VitestConsistentVitestVi(_) => "vitest", Self::VitestHoistedApisOnTop(_) => "vitest", Self::VitestNoConditionalTests(_) => "vitest", @@ -11285,6 +11296,9 @@ impl RuleEnum { Self::VitestConsistentTestFilename(_) => Ok(Self::VitestConsistentTestFilename( VitestConsistentTestFilename::from_configuration(value)?, )), + Self::VitestConsistentTestIt(_) => { + Ok(Self::VitestConsistentTestIt(VitestConsistentTestIt::from_configuration(value)?)) + } Self::VitestConsistentVitestVi(_) => Ok(Self::VitestConsistentVitestVi( VitestConsistentVitestVi::from_configuration(value)?, )), @@ -12073,6 +12087,7 @@ impl RuleEnum { Self::PromiseValidParams(rule) => rule.to_configuration(), Self::VitestConsistentEachFor(rule) => rule.to_configuration(), Self::VitestConsistentTestFilename(rule) => rule.to_configuration(), + Self::VitestConsistentTestIt(rule) => rule.to_configuration(), Self::VitestConsistentVitestVi(rule) => rule.to_configuration(), Self::VitestHoistedApisOnTop(rule) => rule.to_configuration(), Self::VitestNoConditionalTests(rule) => rule.to_configuration(), @@ -12779,6 +12794,7 @@ impl RuleEnum { Self::PromiseValidParams(rule) => rule.run(node, ctx), Self::VitestConsistentEachFor(rule) => rule.run(node, ctx), Self::VitestConsistentTestFilename(rule) => rule.run(node, ctx), + Self::VitestConsistentTestIt(rule) => rule.run(node, ctx), Self::VitestConsistentVitestVi(rule) => rule.run(node, ctx), Self::VitestHoistedApisOnTop(rule) => rule.run(node, ctx), Self::VitestNoConditionalTests(rule) => rule.run(node, ctx), @@ -13483,6 +13499,7 @@ impl RuleEnum { Self::PromiseValidParams(rule) => rule.run_once(ctx), Self::VitestConsistentEachFor(rule) => rule.run_once(ctx), Self::VitestConsistentTestFilename(rule) => rule.run_once(ctx), + Self::VitestConsistentTestIt(rule) => rule.run_once(ctx), Self::VitestConsistentVitestVi(rule) => rule.run_once(ctx), Self::VitestHoistedApisOnTop(rule) => rule.run_once(ctx), Self::VitestNoConditionalTests(rule) => rule.run_once(ctx), @@ -14285,6 +14302,7 @@ impl RuleEnum { Self::PromiseValidParams(rule) => rule.run_on_jest_node(jest_node, ctx), Self::VitestConsistentEachFor(rule) => rule.run_on_jest_node(jest_node, ctx), Self::VitestConsistentTestFilename(rule) => rule.run_on_jest_node(jest_node, ctx), + Self::VitestConsistentTestIt(rule) => rule.run_on_jest_node(jest_node, ctx), Self::VitestConsistentVitestVi(rule) => rule.run_on_jest_node(jest_node, ctx), Self::VitestHoistedApisOnTop(rule) => rule.run_on_jest_node(jest_node, ctx), Self::VitestNoConditionalTests(rule) => rule.run_on_jest_node(jest_node, ctx), @@ -14991,6 +15009,7 @@ impl RuleEnum { Self::PromiseValidParams(rule) => rule.should_run(ctx), Self::VitestConsistentEachFor(rule) => rule.should_run(ctx), Self::VitestConsistentTestFilename(rule) => rule.should_run(ctx), + Self::VitestConsistentTestIt(rule) => rule.should_run(ctx), Self::VitestConsistentVitestVi(rule) => rule.should_run(ctx), Self::VitestHoistedApisOnTop(rule) => rule.should_run(ctx), Self::VitestNoConditionalTests(rule) => rule.should_run(ctx), @@ -15981,6 +16000,7 @@ impl RuleEnum { Self::PromiseValidParams(_) => PromiseValidParams::IS_TSGOLINT_RULE, Self::VitestConsistentEachFor(_) => VitestConsistentEachFor::IS_TSGOLINT_RULE, Self::VitestConsistentTestFilename(_) => VitestConsistentTestFilename::IS_TSGOLINT_RULE, + Self::VitestConsistentTestIt(_) => VitestConsistentTestIt::IS_TSGOLINT_RULE, Self::VitestConsistentVitestVi(_) => VitestConsistentVitestVi::IS_TSGOLINT_RULE, Self::VitestHoistedApisOnTop(_) => VitestHoistedApisOnTop::IS_TSGOLINT_RULE, Self::VitestNoConditionalTests(_) => VitestNoConditionalTests::IS_TSGOLINT_RULE, @@ -16856,6 +16876,7 @@ impl RuleEnum { Self::PromiseValidParams(_) => PromiseValidParams::HAS_CONFIG, Self::VitestConsistentEachFor(_) => VitestConsistentEachFor::HAS_CONFIG, Self::VitestConsistentTestFilename(_) => VitestConsistentTestFilename::HAS_CONFIG, + Self::VitestConsistentTestIt(_) => VitestConsistentTestIt::HAS_CONFIG, Self::VitestConsistentVitestVi(_) => VitestConsistentVitestVi::HAS_CONFIG, Self::VitestHoistedApisOnTop(_) => VitestHoistedApisOnTop::HAS_CONFIG, Self::VitestNoConditionalTests(_) => VitestNoConditionalTests::HAS_CONFIG, @@ -17566,6 +17587,7 @@ impl RuleEnum { Self::PromiseValidParams(rule) => rule.types_info(), Self::VitestConsistentEachFor(rule) => rule.types_info(), Self::VitestConsistentTestFilename(rule) => rule.types_info(), + Self::VitestConsistentTestIt(rule) => rule.types_info(), Self::VitestConsistentVitestVi(rule) => rule.types_info(), Self::VitestHoistedApisOnTop(rule) => rule.types_info(), Self::VitestNoConditionalTests(rule) => rule.types_info(), @@ -18270,6 +18292,7 @@ impl RuleEnum { Self::PromiseValidParams(rule) => rule.run_info(), Self::VitestConsistentEachFor(rule) => rule.run_info(), Self::VitestConsistentTestFilename(rule) => rule.run_info(), + Self::VitestConsistentTestIt(rule) => rule.run_info(), Self::VitestConsistentVitestVi(rule) => rule.run_info(), Self::VitestHoistedApisOnTop(rule) => rule.run_info(), Self::VitestNoConditionalTests(rule) => rule.run_info(), @@ -19090,6 +19113,7 @@ pub static RULES: std::sync::LazyLock> = std::sync::LazyLock::new( RuleEnum::PromiseValidParams(PromiseValidParams::default()), RuleEnum::VitestConsistentEachFor(VitestConsistentEachFor::default()), RuleEnum::VitestConsistentTestFilename(VitestConsistentTestFilename::default()), + RuleEnum::VitestConsistentTestIt(VitestConsistentTestIt::default()), RuleEnum::VitestConsistentVitestVi(VitestConsistentVitestVi::default()), RuleEnum::VitestHoistedApisOnTop(VitestHoistedApisOnTop::default()), RuleEnum::VitestNoConditionalTests(VitestNoConditionalTests::default()), diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 8dad91d025bbc..04eead47bf005 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -703,6 +703,7 @@ pub(crate) mod promise { pub(crate) mod vitest { pub mod consistent_each_for; pub mod consistent_test_filename; + pub mod consistent_test_it; pub mod consistent_vitest_vi; pub mod hoisted_apis_on_top; pub mod no_conditional_tests; @@ -751,6 +752,7 @@ pub(crate) mod vue { } pub(crate) mod shared { + pub mod consistent_test_it; pub mod valid_title; } 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 b41e0c6191ec5..eee04339a4f94 100644 --- a/crates/oxc_linter/src/rules/jest/consistent_test_it.rs +++ b/crates/oxc_linter/src/rules/jest/consistent_test_it.rs @@ -1,274 +1,31 @@ -use std::borrow::Cow; - -use oxc_ast::{AstKind, ast::Expression}; -use oxc_diagnostics::OxcDiagnostic; use oxc_macros::declare_oxc_lint; -use oxc_semantic::ScopeId; -use oxc_span::{GetSpan, Span}; -use rustc_hash::FxHashMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use crate::{ context::LintContext, rule::Rule, - utils::{ - JestFnKind, JestGeneralFnKind, ParsedJestFnCallNew, PossibleJestNode, - collect_possible_jest_call_node, parse_jest_fn_call, - }, + rules::shared::consistent_test_it::{ConsistentTestItConfig, DOCUMENTATION}, }; -fn consistent_method(preferred_method: &str, other_method: &str, span: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("Enforce `test` and `it` usage conventions") - .with_help(format!("Prefer using {preferred_method:?} instead of {other_method:?}")) - .with_label(span) -} - -fn consistent_method_within_describe( - preferred_method: &str, - other_method: &str, - span: Span, -) -> OxcDiagnostic { - OxcDiagnostic::warn("Enforce `test` and `it` usage conventions") - .with_help(format!( - "Prefer using {preferred_method:?} instead of {other_method:?} within describe" - )) - .with_label(span) -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, JsonSchema, Serialize)] -#[serde(rename_all = "lowercase")] -enum TestCaseName { - IT, - Test, -} - -impl TestCaseName { - pub fn as_str(self) -> &'static str { - match self { - TestCaseName::IT => "it", - TestCaseName::Test => "test", - } - } -} - #[derive(Debug, Default, Clone, Deserialize)] pub struct ConsistentTestIt(Box); -#[derive(Debug, Clone, Copy, Deserialize, JsonSchema, Serialize)] -#[serde(rename_all = "camelCase", default)] -pub struct ConsistentTestItConfig { - /// Decides whether to use `test` or `it` within a `describe` scope. - /// If only `fn` is provided, this will default to the value of `fn`. - within_describe: TestCaseName, - /// Decides whether to use `test` or `it`. - r#fn: TestCaseName, -} - -impl std::ops::Deref for ConsistentTestIt { - type Target = ConsistentTestItConfig; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Default for ConsistentTestItConfig { - fn default() -> Self { - Self { within_describe: TestCaseName::IT, r#fn: TestCaseName::Test } - } -} - declare_oxc_lint!( - /// ### What it does - /// - /// Jest allows you to choose how you want to define your tests, using the `it` or - /// the `test` keywords, with multiple permutations for each: - /// - /// - **it:** `it`, `xit`, `fit`, `it.only`, `it.skip`. - /// - **test:** `test`, `xtest`, `test.only`, `test.skip`. - /// - /// ### Why is this bad? - /// - /// It's a good practice to be consistent in your test suite, so that all tests are written in the same way. - /// - /// ### Examples - /// - /// ```javascript - /// /* jest/consistent-test-it: ["error", {"fn": "test"}] */ - /// test('foo'); // valid - /// test.only('foo'); // valid - /// - /// it('foo'); // invalid - /// it.only('foo'); // invalid - /// ``` - /// - /// ```javascript - /// /* jest/consistent-test-it: ["error", {"fn": "it"}] */ - /// it('foo'); // valid - /// it.only('foo'); // valid - /// test('foo'); // invalid - /// test.only('foo'); // invalid - /// ``` - /// - /// ```javascript - /// /* jest/consistent-test-it: ["error", {"fn": "it", "withinDescribe": "test"}] */ - /// it('foo'); // valid - /// describe('foo', function () { - /// test('bar'); // valid - /// }); - /// - /// test('foo'); // invalid - /// describe('foo', function () { - /// it('bar'); // invalid - /// }); - /// ``` - /// - /// This rule is compatible with [eslint-plugin-vitest](https://github.com/vitest-dev/eslint-plugin-vitest/blob/v1.1.9/docs/rules/consistent-test-it.md), - /// to use it, add the following configuration to your `.oxlintrc.json`: - /// - /// ```json - /// { - /// "rules": { - /// "vitest/consistent-test-it": "error" - /// } - /// } - /// ``` ConsistentTestIt, jest, style, fix, config = ConsistentTestItConfig, + docs = DOCUMENTATION, ); impl Rule for ConsistentTestIt { fn from_configuration(value: serde_json::Value) -> Result { - if value.is_null() { - return Ok(Self::default()); - } - - let config_value = value.get(0).unwrap_or(&value); - - let mut config: ConsistentTestItConfig = - serde_json::from_value(config_value.clone()).unwrap_or_default(); - - // If withinDescribe wasn't provided, default it to the value of `fn` only if fn was explicitly provided - if config_value.get("withinDescribe").is_none() && config_value.get("fn").is_some() { - config.within_describe = config.r#fn; - } - - Ok(Self(Box::new(config))) + ConsistentTestItConfig::from_configuration(&value).map(|config| Self(Box::new(config))) } fn run_once(&self, ctx: &LintContext) { - let mut describe_nesting_hash: FxHashMap = FxHashMap::default(); - let mut possible_jest_nodes = collect_possible_jest_call_node(ctx); - possible_jest_nodes.sort_unstable_by_key(|n| n.node.id()); - - for possible_jest_node in &possible_jest_nodes { - self.run(&mut describe_nesting_hash, possible_jest_node, ctx); - } - } -} - -impl ConsistentTestIt { - fn run<'a>( - &self, - describe_nesting_hash: &mut FxHashMap, - possible_jest_node: &PossibleJestNode<'a, '_>, - ctx: &LintContext<'a>, - ) { - let node = possible_jest_node.node; - let AstKind::CallExpression(call_expr) = node.kind() else { - return; - }; - let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) = - parse_jest_fn_call(call_expr, possible_jest_node, ctx) - else { - return; - }; - - if matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Describe)) { - let scope_id = node.scope_id(); - let current_count = describe_nesting_hash.get(&scope_id).unwrap_or(&0); - describe_nesting_hash.insert(scope_id, *current_count + 1); - return; - } - - let is_test = matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Test)); - let fn_to_str = self.r#fn.as_str(); - - if is_test && describe_nesting_hash.is_empty() && !jest_fn_call.name.ends_with(&fn_to_str) { - let opposite_test_keyword = Self::get_opposite_test_case(self.r#fn); - if let Some((span, prefer_test_name)) = Self::get_prefer_test_name_and_span( - call_expr.callee.get_inner_expression(), - &jest_fn_call.name, - fn_to_str, - ) { - ctx.diagnostic_with_fix( - consistent_method(fn_to_str, opposite_test_keyword, span), - |fixer| fixer.replace(span, prefer_test_name), - ); - } - } - - let describe_to_str = self.within_describe.as_str(); - - if is_test - && !describe_nesting_hash.is_empty() - && !jest_fn_call.name.ends_with(&describe_to_str) - { - let opposite_test_keyword = Self::get_opposite_test_case(self.within_describe); - if let Some((span, prefer_test_name)) = Self::get_prefer_test_name_and_span( - call_expr.callee.get_inner_expression(), - &jest_fn_call.name, - describe_to_str, - ) { - ctx.diagnostic_with_fix( - consistent_method_within_describe(describe_to_str, opposite_test_keyword, span), - |fixer| fixer.replace(span, prefer_test_name), - ); - } - } - } - - fn get_opposite_test_case(test_case_name: TestCaseName) -> &'static str { - if matches!(test_case_name, TestCaseName::Test) { "it" } else { "test" } - } - - fn get_prefer_test_name_and_span<'s>( - expr: &Expression, - test_name: &str, - fix_jest_name: &'s str, - ) -> Option<(Span, Cow<'s, str>)> { - match expr { - Expression::Identifier(ident) => { - if ident.name.eq("fit") { - return Some((ident.span(), Cow::Borrowed("test.only"))); - } - - let prefer_test_name = match test_name.chars().next() { - Some('x') => Cow::Owned(format!("x{fix_jest_name}")), - Some('f') => Cow::Owned(format!("f{fix_jest_name}")), - _ => Cow::Borrowed(fix_jest_name), - }; - Some((ident.span(), prefer_test_name)) - } - Expression::StaticMemberExpression(expr) => { - Self::get_prefer_test_name_and_span(&expr.object, test_name, fix_jest_name) - } - Expression::CallExpression(call_expr) => Self::get_prefer_test_name_and_span( - call_expr.callee.get_inner_expression(), - test_name, - fix_jest_name, - ), - Expression::TaggedTemplateExpression(expr) => Self::get_prefer_test_name_and_span( - expr.tag.get_inner_expression(), - test_name, - fix_jest_name, - ), - _ => None, - } + self.0.run_once(ctx); } } @@ -276,7 +33,7 @@ impl ConsistentTestIt { fn test() { use crate::tester::Tester; - let mut pass = vec![ + let pass = vec![ // consistent-test-it with fn=test ("test(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), ("test.only(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), @@ -353,7 +110,7 @@ fn test() { ), ]; - let mut fail = vec![ + let fail = vec![ // consistent-test-it with fn=test ("it(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), ( @@ -523,7 +280,7 @@ fn test() { ), ]; - let mut fix = vec![ + let fix = vec![ // consistent-test-it with fn=test ("it(\"foo\")", "test(\"foo\")"), ( @@ -731,150 +488,8 @@ fn test() { // ), ]; - let pass_vitest = vec![ - ( - " - it(\"shows error\", () => { - expect(true).toBe(false); - }); - ", - Some(serde_json::json!([{ "fn": "it" }])), - ), - ( - " - it(\"foo\", function () { - expect(true).toBe(false); - }) - ", - Some(serde_json::json!([{ "fn": "it" }])), - ), - ( - " - it('foo', () => { - expect(true).toBe(false); - }); - function myTest() { if ('bar') {} } - ", - Some(serde_json::json!([{ "fn": "it" }])), - ), - ( - " - test(\"shows error\", () => { - expect(true).toBe(false); - }); - ", - Some(serde_json::json!([{ "fn": "test" }])), - ), - ("test.skip(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), - ("test.concurrent(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), - ("xtest(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), - ("test.each([])(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), - ("test.each``(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), - ( - "describe(\"suite\", () => { test(\"foo\") })", - Some(serde_json::json!([{ "fn": "test" }])), - ), - ( - "describe(\"suite\", () => { it(\"foo\") })", - Some(serde_json::json!([{ "fn": "it", "withinDescribe": "it" }])), - ), - ("it(\"foo\")", Some(serde_json::json!([{ "fn": "it", "withinDescribe": "it" }]))), - ("test(\"shows error\", () => {});", None), - ("test(\"foo\")", Some(serde_json::json!([{ "withinDescribe": "it" }]))), - ( - "describe(\"suite\", () => { it(\"foo\") })", - Some(serde_json::json!([{ "withinDescribe": "it" }])), - ), - ("test(\"foo\")", Some(serde_json::json!([{ "withinDescribe": "test" }]))), - ( - "describe(\"suite\", () => { test(\"foo\") })", - Some(serde_json::json!([{ "withinDescribe": "test" }])), - ), - ]; - - let fail_vitest = vec![ - ("test(\"shows error\", () => {});", Some(serde_json::json!([{ "fn": "it" }]))), - ("test.skip(\"shows error\");", Some(serde_json::json!([{ "fn": "it" }]))), - ("test.only('shows error');", Some(serde_json::json!([{ "fn": "it" }]))), - ( - "describe('foo', () => { it('bar', () => {}); });", - Some(serde_json::json!([{ "fn": "it", "withinDescribe": "test" }])), - ), - ( - "import { test } from \"vitest\"\ntest(\"shows error\", () => {});", - Some(serde_json::json!([{ "fn": "it" }])), - ), - ("it(\"shows error\", () => {});", Some(serde_json::json!([{ "fn": "test" }]))), - ("describe(\"suite\", () => { it(\"foo\") })", Some(serde_json::json!([{ "fn": "test" }]))), - ( - "describe(\"suite\", () => { test(\"foo\") })", - Some(serde_json::json!([{ "fn": "it", "withinDescribe": "it" }])), - ), - ("test(\"foo\")", Some(serde_json::json!([{ "fn": "it", "withinDescribe": "it" }]))), - ("describe(\"suite\", () => { test(\"foo\") })", None), - ("it(\"foo\")", Some(serde_json::json!([{ "withinDescribe": "it" }]))), - ( - "describe(\"suite\", () => { test(\"foo\") })", - Some(serde_json::json!([{ "withinDescribe": "it" }])), - ), - ("it(\"foo\")", Some(serde_json::json!([{ "withinDescribe": "test" }]))), - ( - "import { it } from \"vitest\"\nit(\"foo\")", - Some(serde_json::json!([{ "withinDescribe": "test" }])), - ), - ( - "describe(\"suite\", () => { it(\"foo\") })", - Some(serde_json::json!([{ "withinDescribe": "test" }])), - ), - ]; - - let fix_vitest = vec![ - // Note: couldn't fixed, because the fixer doesn't support to set the options for the fix cases. - // Todo: this need to fixer support option configuration. - // ("test(\"shows error\", () => {});", "it(\"shows error\", () => {});"), - // ("test.skip(\"shows error\");", "it.skip(\"shows error\");"), - // ("test.only('shows error');", "it.only('shows error');"), - // ( - // "describe('foo', () => { it('bar', () => {}); });", - // "describe('foo', () => { test('bar', () => {}); });" - // ), - // ( - // "import { test } from \"vitest\"\ntest(\"shows error\", () => {});", - // "import { it } from \"vitest\"\nit(\"shows error\", () => {});", - // ), - // ("describe(\"suite\", () => { it(\"foo\") })", "describe(\"suite\", () => { test(\"foo\") })"), - // ("test(\"foo\")", "it(\"foo\")"), - // ("describe(\"suite\", () => { it(\"foo\") })", "describe(\"suite\", () => { test(\"foo\") })"), - // - ("it(\"shows error\", () => {});", "test(\"shows error\", () => {});"), - ( - "describe(\"suite\", () => { test(\"foo\") })", - "describe(\"suite\", () => { it(\"foo\") })", - ), - ( - "describe(\"suite\", () => { test(\"foo\") })", - "describe(\"suite\", () => { it(\"foo\") })", - ), - ("it(\"foo\")", "test(\"foo\")"), - ( - "describe(\"suite\", () => { test(\"foo\") })", - "describe(\"suite\", () => { it(\"foo\") })", - ), - ("it(\"foo\")", "test(\"foo\")"), - // Todo: need to be fixed - // ( - // "import { it } from \"vitest\"\nit(\"foo\")", - // "import { test } from \"vitest\"\ntest(\"foo\")" - // ), - ]; - - pass.extend(pass_vitest); - fail.extend(fail_vitest); - fix.extend(fix_vitest); - Tester::new(ConsistentTestIt::NAME, ConsistentTestIt::PLUGIN, pass, fail) .with_jest_plugin(true) - .with_vitest_plugin(true) .expect_fix(fix) .test_and_snapshot(); } diff --git a/crates/oxc_linter/src/rules/shared/consistent_test_it.rs b/crates/oxc_linter/src/rules/shared/consistent_test_it.rs new file mode 100644 index 0000000000000..3ac8710feed88 --- /dev/null +++ b/crates/oxc_linter/src/rules/shared/consistent_test_it.rs @@ -0,0 +1,256 @@ +use std::borrow::Cow; + +use oxc_ast::{AstKind, ast::Expression}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_semantic::ScopeId; +use oxc_span::{GetSpan, Span}; +use rustc_hash::FxHashMap; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + context::LintContext, + utils::{ + JestFnKind, JestGeneralFnKind, ParsedJestFnCallNew, PossibleJestNode, + collect_possible_jest_call_node, parse_jest_fn_call, + }, +}; + +fn consistent_method(preferred_method: &str, other_method: &str, span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Enforce `test` and `it` usage conventions") + .with_help(format!("Prefer using {preferred_method:?} instead of {other_method:?}")) + .with_label(span) +} + +fn consistent_method_within_describe( + preferred_method: &str, + other_method: &str, + span: Span, +) -> OxcDiagnostic { + OxcDiagnostic::warn("Enforce `test` and `it` usage conventions") + .with_help(format!( + "Prefer using {preferred_method:?} instead of {other_method:?} within describe" + )) + .with_label(span) +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize, JsonSchema, Serialize)] +#[serde(rename_all = "lowercase")] +enum TestCaseName { + IT, + Test, +} + +impl TestCaseName { + pub fn as_str(self) -> &'static str { + match self { + TestCaseName::IT => "it", + TestCaseName::Test => "test", + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, JsonSchema, Serialize)] +#[serde(rename_all = "camelCase", default)] +pub struct ConsistentTestItConfig { + /// Decides whether to use `test` or `it` within a `describe` scope. + /// If only `fn` is provided, this will default to the value of `fn`. + within_describe: TestCaseName, + /// Decides whether to use `test` or `it`. + r#fn: TestCaseName, +} + +impl Default for ConsistentTestItConfig { + fn default() -> Self { + Self { within_describe: TestCaseName::IT, r#fn: TestCaseName::Test } + } +} + +pub const DOCUMENTATION: &str = r#" +### What it does + + Jest allows you to choose how you want to define your tests, using the `it` or + the `test` keywords, with multiple permutations for each: + + - **it:** `it`, `xit`, `fit`, `it.only`, `it.skip`. + - **test:** `test`, `xtest`, `test.only`, `test.skip`. + + ### Why is this bad? + + It's a good practice to be consistent in your test suite, so that all tests are written in the same way. + + ### Examples + + ```javascript + /* jest/consistent-test-it: ["error", {"fn": "test"}] */ + test('foo'); // valid + test.only('foo'); // valid + + it('foo'); // invalid + it.only('foo'); // invalid + ``` + + ```javascript + /* jest/consistent-test-it: ["error", {"fn": "it"}] */ + it('foo'); // valid + it.only('foo'); // valid + test('foo'); // invalid + test.only('foo'); // invalid + ``` + + ```javascript + /* jest/consistent-test-it: ["error", {"fn": "it", "withinDescribe": "test"}] */ + it('foo'); // valid + describe('foo', function () { + test('bar'); // valid + }); + + test('foo'); // invalid + describe('foo', function () { + it('bar'); // invalid + }); + ``` + + This rule is compatible with [eslint-plugin-vitest](https://github.com/vitest-dev/eslint-plugin-vitest/blob/v1.1.9/docs/rules/consistent-test-it.md), + to use it, add the following configuration to your `.oxlintrc.json`: + + ```json + { + "rules": { + "vitest/consistent-test-it": "error" + } + } + ``` +"#; + +impl ConsistentTestItConfig { + #[expect(clippy::unnecessary_wraps)] // TODO: fail on serde error + pub fn from_configuration(value: &serde_json::Value) -> Result { + if value.is_null() { + return Ok(Self::default()); + } + + let config_value = value.get(0).unwrap_or(value); + + let mut config: ConsistentTestItConfig = + serde_json::from_value(config_value.clone()).unwrap_or_default(); + + // If withinDescribe wasn't provided, default it to the value of `fn` only if fn was explicitly provided + if config_value.get("withinDescribe").is_none() && config_value.get("fn").is_some() { + config.within_describe = config.r#fn; + } + + Ok(config) + } + + pub fn run_once(self, ctx: &LintContext) { + let mut describe_nesting_hash: FxHashMap = FxHashMap::default(); + let mut possible_jest_nodes = collect_possible_jest_call_node(ctx); + possible_jest_nodes.sort_unstable_by_key(|n| n.node.id()); + + for possible_jest_node in &possible_jest_nodes { + self.run(&mut describe_nesting_hash, possible_jest_node, ctx); + } + } +} + +impl ConsistentTestItConfig { + fn run<'a>( + self, + describe_nesting_hash: &mut FxHashMap, + possible_jest_node: &PossibleJestNode<'a, '_>, + ctx: &LintContext<'a>, + ) { + let node = possible_jest_node.node; + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + let Some(ParsedJestFnCallNew::GeneralJest(jest_fn_call)) = + parse_jest_fn_call(call_expr, possible_jest_node, ctx) + else { + return; + }; + + if matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Describe)) { + let scope_id = node.scope_id(); + let current_count = describe_nesting_hash.get(&scope_id).unwrap_or(&0); + describe_nesting_hash.insert(scope_id, *current_count + 1); + return; + } + + let is_test = matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Test)); + let fn_to_str = self.r#fn.as_str(); + + if is_test && describe_nesting_hash.is_empty() && !jest_fn_call.name.ends_with(&fn_to_str) { + let opposite_test_keyword = Self::get_opposite_test_case(self.r#fn); + if let Some((span, prefer_test_name)) = Self::get_prefer_test_name_and_span( + call_expr.callee.get_inner_expression(), + &jest_fn_call.name, + fn_to_str, + ) { + ctx.diagnostic_with_fix( + consistent_method(fn_to_str, opposite_test_keyword, span), + |fixer| fixer.replace(span, prefer_test_name), + ); + } + } + + let describe_to_str = self.within_describe.as_str(); + + if is_test + && !describe_nesting_hash.is_empty() + && !jest_fn_call.name.ends_with(&describe_to_str) + { + let opposite_test_keyword = Self::get_opposite_test_case(self.within_describe); + if let Some((span, prefer_test_name)) = Self::get_prefer_test_name_and_span( + call_expr.callee.get_inner_expression(), + &jest_fn_call.name, + describe_to_str, + ) { + ctx.diagnostic_with_fix( + consistent_method_within_describe(describe_to_str, opposite_test_keyword, span), + |fixer| fixer.replace(span, prefer_test_name), + ); + } + } + } + + fn get_opposite_test_case(test_case_name: TestCaseName) -> &'static str { + if matches!(test_case_name, TestCaseName::Test) { "it" } else { "test" } + } + + fn get_prefer_test_name_and_span<'s>( + expr: &Expression, + test_name: &str, + fix_jest_name: &'s str, + ) -> Option<(Span, Cow<'s, str>)> { + match expr { + Expression::Identifier(ident) => { + if ident.name.eq("fit") { + return Some((ident.span(), Cow::Borrowed("test.only"))); + } + + let prefer_test_name = match test_name.chars().next() { + Some('x') => Cow::Owned(format!("x{fix_jest_name}")), + Some('f') => Cow::Owned(format!("f{fix_jest_name}")), + _ => Cow::Borrowed(fix_jest_name), + }; + Some((ident.span(), prefer_test_name)) + } + Expression::StaticMemberExpression(expr) => { + Self::get_prefer_test_name_and_span(&expr.object, test_name, fix_jest_name) + } + Expression::CallExpression(call_expr) => Self::get_prefer_test_name_and_span( + call_expr.callee.get_inner_expression(), + test_name, + fix_jest_name, + ), + Expression::TaggedTemplateExpression(expr) => Self::get_prefer_test_name_and_span( + expr.tag.get_inner_expression(), + test_name, + fix_jest_name, + ), + _ => None, + } + } +} diff --git a/crates/oxc_linter/src/rules/vitest/consistent_test_it.rs b/crates/oxc_linter/src/rules/vitest/consistent_test_it.rs new file mode 100644 index 0000000000000..4003b295d4164 --- /dev/null +++ b/crates/oxc_linter/src/rules/vitest/consistent_test_it.rs @@ -0,0 +1,177 @@ +use oxc_macros::declare_oxc_lint; +use serde::Deserialize; + +use crate::{ + context::LintContext, + rule::Rule, + rules::shared::consistent_test_it::{ConsistentTestItConfig, DOCUMENTATION}, +}; + +#[derive(Debug, Default, Clone, Deserialize)] +pub struct ConsistentTestIt(Box); + +declare_oxc_lint!( + ConsistentTestIt, + vitest, + style, + fix, + config = ConsistentTestItConfig, + docs = DOCUMENTATION, +); + +impl Rule for ConsistentTestIt { + fn from_configuration(value: serde_json::Value) -> Result { + ConsistentTestItConfig::from_configuration(&value).map(|config| Self(Box::new(config))) + } + + fn run_once(&self, ctx: &LintContext) { + self.0.run_once(ctx); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + " + it(\"shows error\", () => { + expect(true).toBe(false); + }); + ", + Some(serde_json::json!([{ "fn": "it" }])), + ), + ( + " + it(\"foo\", function () { + expect(true).toBe(false); + }) + ", + Some(serde_json::json!([{ "fn": "it" }])), + ), + ( + " + it('foo', () => { + expect(true).toBe(false); + }); + function myTest() { if ('bar') {} } + ", + Some(serde_json::json!([{ "fn": "it" }])), + ), + ( + " + test(\"shows error\", () => { + expect(true).toBe(false); + }); + ", + Some(serde_json::json!([{ "fn": "test" }])), + ), + ("test.skip(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), + ("test.concurrent(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), + ("xtest(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), + ("test.each([])(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), + ("test.each``(\"foo\")", Some(serde_json::json!([{ "fn": "test" }]))), + ( + "describe(\"suite\", () => { test(\"foo\") })", + Some(serde_json::json!([{ "fn": "test" }])), + ), + ( + "describe(\"suite\", () => { it(\"foo\") })", + Some(serde_json::json!([{ "fn": "it", "withinDescribe": "it" }])), + ), + ("it(\"foo\")", Some(serde_json::json!([{ "fn": "it", "withinDescribe": "it" }]))), + ("test(\"shows error\", () => {});", None), + ("test(\"foo\")", Some(serde_json::json!([{ "withinDescribe": "it" }]))), + ( + "describe(\"suite\", () => { it(\"foo\") })", + Some(serde_json::json!([{ "withinDescribe": "it" }])), + ), + ("test(\"foo\")", Some(serde_json::json!([{ "withinDescribe": "test" }]))), + ( + "describe(\"suite\", () => { test(\"foo\") })", + Some(serde_json::json!([{ "withinDescribe": "test" }])), + ), + ]; + + let fail = vec![ + ("test(\"shows error\", () => {});", Some(serde_json::json!([{ "fn": "it" }]))), + ("test.skip(\"shows error\");", Some(serde_json::json!([{ "fn": "it" }]))), + ("test.only('shows error');", Some(serde_json::json!([{ "fn": "it" }]))), + ( + "describe('foo', () => { it('bar', () => {}); });", + Some(serde_json::json!([{ "fn": "it", "withinDescribe": "test" }])), + ), + ( + "import { test } from \"vitest\"\ntest(\"shows error\", () => {});", + Some(serde_json::json!([{ "fn": "it" }])), + ), + ("it(\"shows error\", () => {});", Some(serde_json::json!([{ "fn": "test" }]))), + ("describe(\"suite\", () => { it(\"foo\") })", Some(serde_json::json!([{ "fn": "test" }]))), + ( + "describe(\"suite\", () => { test(\"foo\") })", + Some(serde_json::json!([{ "fn": "it", "withinDescribe": "it" }])), + ), + ("test(\"foo\")", Some(serde_json::json!([{ "fn": "it", "withinDescribe": "it" }]))), + ("describe(\"suite\", () => { test(\"foo\") })", None), + ("it(\"foo\")", Some(serde_json::json!([{ "withinDescribe": "it" }]))), + ( + "describe(\"suite\", () => { test(\"foo\") })", + Some(serde_json::json!([{ "withinDescribe": "it" }])), + ), + ("it(\"foo\")", Some(serde_json::json!([{ "withinDescribe": "test" }]))), + ( + "import { it } from \"vitest\"\nit(\"foo\")", + Some(serde_json::json!([{ "withinDescribe": "test" }])), + ), + ( + "describe(\"suite\", () => { it(\"foo\") })", + Some(serde_json::json!([{ "withinDescribe": "test" }])), + ), + ]; + + let fix = vec![ + // Note: couldn't fixed, because the fixer doesn't support to set the options for the fix cases. + // Todo: this need to fixer support option configuration. + // ("test(\"shows error\", () => {});", "it(\"shows error\", () => {});"), + // ("test.skip(\"shows error\");", "it.skip(\"shows error\");"), + // ("test.only('shows error');", "it.only('shows error');"), + // ( + // "describe('foo', () => { it('bar', () => {}); });", + // "describe('foo', () => { test('bar', () => {}); });" + // ), + // ( + // "import { test } from \"vitest\"\ntest(\"shows error\", () => {});", + // "import { it } from \"vitest\"\nit(\"shows error\", () => {});", + // ), + // ("describe(\"suite\", () => { it(\"foo\") })", "describe(\"suite\", () => { test(\"foo\") })"), + // ("test(\"foo\")", "it(\"foo\")"), + // ("describe(\"suite\", () => { it(\"foo\") })", "describe(\"suite\", () => { test(\"foo\") })"), + // + ("it(\"shows error\", () => {});", "test(\"shows error\", () => {});"), + ( + "describe(\"suite\", () => { test(\"foo\") })", + "describe(\"suite\", () => { it(\"foo\") })", + ), + ( + "describe(\"suite\", () => { test(\"foo\") })", + "describe(\"suite\", () => { it(\"foo\") })", + ), + ("it(\"foo\")", "test(\"foo\")"), + ( + "describe(\"suite\", () => { test(\"foo\") })", + "describe(\"suite\", () => { it(\"foo\") })", + ), + ("it(\"foo\")", "test(\"foo\")"), + // Todo: need to be fixed + // ( + // "import { it } from \"vitest\"\nit(\"foo\")", + // "import { test } from \"vitest\"\ntest(\"foo\")" + // ), + ]; + + Tester::new(ConsistentTestIt::NAME, ConsistentTestIt::PLUGIN, pass, fail) + .with_vitest_plugin(true) + .expect_fix(fix) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/jest_consistent_test_it.snap b/crates/oxc_linter/src/snapshots/jest_consistent_test_it.snap index ea2732e3b3484..42bb7ba62cb7f 100644 --- a/crates/oxc_linter/src/snapshots/jest_consistent_test_it.snap +++ b/crates/oxc_linter/src/snapshots/jest_consistent_test_it.snap @@ -346,110 +346,3 @@ source: crates/oxc_linter/src/tester.rs · ── ╰──── help: Prefer using "test" instead of "it" within describe - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:1] - 1 │ test("shows error", () => {}); - · ──── - ╰──── - help: Prefer using "it" instead of "test" - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:1] - 1 │ test.skip("shows error"); - · ──── - ╰──── - help: Prefer using "it" instead of "test" - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:1] - 1 │ test.only('shows error'); - · ──── - ╰──── - help: Prefer using "it" instead of "test" - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:25] - 1 │ describe('foo', () => { it('bar', () => {}); }); - · ── - ╰──── - help: Prefer using "test" instead of "it" within describe - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:2:1] - 1 │ import { test } from "vitest" - 2 │ test("shows error", () => {}); - · ──── - ╰──── - help: Prefer using "it" instead of "test" - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:1] - 1 │ it("shows error", () => {}); - · ── - ╰──── - help: Prefer using "test" instead of "it" - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:27] - 1 │ describe("suite", () => { it("foo") }) - · ── - ╰──── - help: Prefer using "test" instead of "it" within describe - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:27] - 1 │ describe("suite", () => { test("foo") }) - · ──── - ╰──── - help: Prefer using "it" instead of "test" within describe - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:1] - 1 │ test("foo") - · ──── - ╰──── - help: Prefer using "it" instead of "test" - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:27] - 1 │ describe("suite", () => { test("foo") }) - · ──── - ╰──── - help: Prefer using "it" instead of "test" within describe - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:1] - 1 │ it("foo") - · ── - ╰──── - help: Prefer using "test" instead of "it" - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:27] - 1 │ describe("suite", () => { test("foo") }) - · ──── - ╰──── - help: Prefer using "it" instead of "test" within describe - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:1] - 1 │ it("foo") - · ── - ╰──── - help: Prefer using "test" instead of "it" - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:2:1] - 1 │ import { it } from "vitest" - 2 │ it("foo") - · ── - ╰──── - help: Prefer using "test" instead of "it" - - ⚠ eslint-plugin-jest(consistent-test-it): Enforce `test` and `it` usage conventions - ╭─[consistent_test_it.tsx:1:27] - 1 │ describe("suite", () => { it("foo") }) - · ── - ╰──── - help: Prefer using "test" instead of "it" within describe diff --git a/crates/oxc_linter/src/snapshots/vitest_consistent_test_it.snap b/crates/oxc_linter/src/snapshots/vitest_consistent_test_it.snap new file mode 100644 index 0000000000000..83dd2e0d7eed0 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/vitest_consistent_test_it.snap @@ -0,0 +1,109 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:1] + 1 │ test("shows error", () => {}); + · ──── + ╰──── + help: Prefer using "it" instead of "test" + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:1] + 1 │ test.skip("shows error"); + · ──── + ╰──── + help: Prefer using "it" instead of "test" + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:1] + 1 │ test.only('shows error'); + · ──── + ╰──── + help: Prefer using "it" instead of "test" + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:25] + 1 │ describe('foo', () => { it('bar', () => {}); }); + · ── + ╰──── + help: Prefer using "test" instead of "it" within describe + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:2:1] + 1 │ import { test } from "vitest" + 2 │ test("shows error", () => {}); + · ──── + ╰──── + help: Prefer using "it" instead of "test" + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:1] + 1 │ it("shows error", () => {}); + · ── + ╰──── + help: Prefer using "test" instead of "it" + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:27] + 1 │ describe("suite", () => { it("foo") }) + · ── + ╰──── + help: Prefer using "test" instead of "it" within describe + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:27] + 1 │ describe("suite", () => { test("foo") }) + · ──── + ╰──── + help: Prefer using "it" instead of "test" within describe + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:1] + 1 │ test("foo") + · ──── + ╰──── + help: Prefer using "it" instead of "test" + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:27] + 1 │ describe("suite", () => { test("foo") }) + · ──── + ╰──── + help: Prefer using "it" instead of "test" within describe + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:1] + 1 │ it("foo") + · ── + ╰──── + help: Prefer using "test" instead of "it" + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:27] + 1 │ describe("suite", () => { test("foo") }) + · ──── + ╰──── + help: Prefer using "it" instead of "test" within describe + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:1] + 1 │ it("foo") + · ── + ╰──── + help: Prefer using "test" instead of "it" + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:2:1] + 1 │ import { it } from "vitest" + 2 │ it("foo") + · ── + ╰──── + help: Prefer using "test" instead of "it" + + ⚠ eslint-plugin-vitest(consistent-test-it): Enforce `test` and `it` usage conventions + ╭─[consistent_test_it.tsx:1:27] + 1 │ describe("suite", () => { it("foo") }) + · ── + ╰──── + help: Prefer using "test" instead of "it" within describe diff --git a/crates/oxc_linter/src/utils/mod.rs b/crates/oxc_linter/src/utils/mod.rs index 31d995ccc9d3a..b581c483579a2 100644 --- a/crates/oxc_linter/src/utils/mod.rs +++ b/crates/oxc_linter/src/utils/mod.rs @@ -37,8 +37,7 @@ pub use self::{ // the crates/oxc_linter/data/vitest_compatible_jest_rules.json // file is also updated. The JSON file is used by the oxlint-migrate // and eslint-plugin-oxlint repos to keep everything synced. -const VITEST_COMPATIBLE_JEST_RULES: [&str; 43] = [ - "consistent-test-it", +const VITEST_COMPATIBLE_JEST_RULES: [&str; 42] = [ "expect-expect", "max-expects", "max-nested-describe",