diff --git a/apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq --print-config@oxlint.snap b/apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq --print-config@oxlint.snap index a19f93408dc7e..780b9380322a0 100644 --- a/apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq --print-config@oxlint.snap +++ b/apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq --print-config@oxlint.snap @@ -42,6 +42,9 @@ working directory: "implementsReplacesDocs": false, "exemptDestructuredRootsFromChecks": false, "tagNamePreference": {} + }, + "vitest": { + "typecheck": false } }, "env": { diff --git a/apps/oxlint/src/snapshots/fixtures_-A all --print-config@oxlint.snap b/apps/oxlint/src/snapshots/fixtures_-A all --print-config@oxlint.snap index bcec74b42ed09..f41f2fb609c49 100644 --- a/apps/oxlint/src/snapshots/fixtures_-A all --print-config@oxlint.snap +++ b/apps/oxlint/src/snapshots/fixtures_-A all --print-config@oxlint.snap @@ -35,6 +35,9 @@ working directory: fixtures "implementsReplacesDocs": false, "exemptDestructuredRootsFromChecks": false, "tagNamePreference": {} + }, + "vitest": { + "typecheck": false } }, "env": { diff --git a/crates/oxc_linter/src/config/settings/mod.rs b/crates/oxc_linter/src/config/settings/mod.rs index b986b479382f2..a0061f6b2d6c0 100644 --- a/crates/oxc_linter/src/config/settings/mod.rs +++ b/crates/oxc_linter/src/config/settings/mod.rs @@ -2,13 +2,14 @@ pub mod jsdoc; mod jsx_a11y; mod next; mod react; +pub mod vitest; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use self::{ jsdoc::JSDocPluginSettings, jsx_a11y::JSXA11yPluginSettings, next::NextPluginSettings, - react::ReactPluginSettings, + react::ReactPluginSettings, vitest::VitestPluginSettings, }; /// # Oxlint Plugin Settings @@ -52,6 +53,9 @@ pub struct OxlintSettings { #[serde(default)] pub jsdoc: JSDocPluginSettings, + + #[serde(default)] + pub vitest: VitestPluginSettings, } #[cfg(test)] diff --git a/crates/oxc_linter/src/config/settings/vitest.rs b/crates/oxc_linter/src/config/settings/vitest.rs new file mode 100644 index 0000000000000..648c34749d6ea --- /dev/null +++ b/crates/oxc_linter/src/config/settings/vitest.rs @@ -0,0 +1,16 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Configure Vitest plugin rules. +/// +/// See [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)'s +/// configuration for a full reference. +#[derive(Debug, Clone, Deserialize, Serialize, Default, JsonSchema)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub struct VitestPluginSettings { + /// Whether to enable typecheck mode for Vitest rules. + /// When enabled, some rules will skip certain checks for describe blocks + /// to accommodate TypeScript type checking scenarios. + #[serde(default)] + pub typecheck: bool, +} diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 7d47c7775c5c5..1786163960f24 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -1258,12 +1258,12 @@ oxc_macros::declare_all_lint_rules! { vitest::prefer_to_be_object, vitest::prefer_to_be_truthy, vitest::require_local_test_context_for_concurrent_snapshots, - vue::define_props_destructuring, vue::define_emits_declaration, vue::define_props_declaration, + vue::define_props_destructuring, vue::max_props, - vue::no_import_compiler_macros, vue::no_export_in_script_setup, + vue::no_import_compiler_macros, vue::no_multiple_slot_args, vue::no_required_prop_with_default, vue::prefer_import_from_vue, diff --git a/crates/oxc_linter/src/rules/jest/valid_title.rs b/crates/oxc_linter/src/rules/jest/valid_title.rs index c46a4e445556d..f44b05c87879b 100644 --- a/crates/oxc_linter/src/rules/jest/valid_title.rs +++ b/crates/oxc_linter/src/rules/jest/valid_title.rs @@ -28,6 +28,7 @@ pub struct ValidTitle(Box); pub struct ValidTitleConfig { ignore_type_of_test_name: bool, ignore_type_of_describe_name: bool, + allow_arguments: bool, disallowed_words: Vec, ignore_space: bool, must_not_match_patterns: FxHashMap, @@ -45,7 +46,7 @@ impl std::ops::Deref for ValidTitle { declare_oxc_lint!( /// ### What it does /// - /// Checks that the titles of Jest blocks are valid. + /// Checks that the titles of Jest and Vitest blocks are valid. /// /// Titles must be: /// - not empty, @@ -84,6 +85,7 @@ declare_oxc_lint!( /// ignoreSpaces?: boolean; /// ignoreTypeOfTestName?: boolean; /// ignoreTypeOfDescribeName?: boolean; + /// allowArguments?: boolean; /// disallowedWords?: string[]; /// mustNotMatch?: Partial> | string; /// mustMatch?: Partial> | string; @@ -108,6 +110,7 @@ impl Rule for ValidTitle { let ignore_type_of_test_name = get_as_bool("ignoreTypeOfTestName"); let ignore_type_of_describe_name = get_as_bool("ignoreTypeOfDescribeName"); + let allow_arguments = get_as_bool("allowArguments"); let ignore_space = get_as_bool("ignoreSpaces"); let disallowed_words = config .and_then(|v| v.get("disallowedWords")) @@ -125,6 +128,7 @@ impl Rule for ValidTitle { Self(Box::new(ValidTitleConfig { ignore_type_of_test_name, ignore_type_of_describe_name, + allow_arguments, disallowed_words, ignore_space, must_not_match_patterns, @@ -159,10 +163,29 @@ impl ValidTitle { return; } + // Check if extend keyword has been used (vitest feature) + if let Some(member) = jest_fn_call.members.first() + && member.is_name_equal("extend") + { + return; + } + let Some(arg) = call_expr.arguments.first() else { return; }; + // Handle typecheck settings - skip for describe when enabled (vitest feature) + if ctx.settings().vitest.typecheck + && matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Describe)) + { + return; + } + + // Handle allowArguments option (vitest feature) + if self.allow_arguments && matches!(arg, Argument::Identifier(_)) { + return; + } + let need_report_name = match jest_fn_call.kind { JestFnKind::General(JestGeneralFnKind::Test) => !self.ignore_type_of_test_name, JestFnKind::General(JestGeneralFnKind::Describe) => !self.ignore_type_of_describe_name, @@ -284,15 +307,39 @@ fn compile_matcher_patterns( fn compile_matcher_pattern(pattern: MatcherPattern) -> Option { match pattern { MatcherPattern::String(pattern) => { - let reg_str = format!("(?u){}", pattern.as_str()?); + let pattern_str = pattern.as_str()?; + + // Check for JS regex literal: /pattern/flags + if let Some(stripped) = pattern_str.strip_prefix('/') + && let Some(end) = stripped.rfind('/') + { + let (pat, _flags) = stripped.split_at(end); + // For now, ignore flags and just use the pattern + let regex = Regex::new(pat).ok()?; + return Some((regex, None)); + } + + // Fallback: treat as a normal Rust regex with Unicode support + let reg_str = format!("(?u){pattern_str}"); let reg = Regex::new(®_str).ok()?; Some((reg, None)) } MatcherPattern::Vec(pattern) => { - let reg_str = pattern.first().and_then(|v| v.as_str()).map(|v| format!("(?u){v}"))?; - let reg = Regex::new(®_str).ok()?; + let pattern_str = pattern.first().and_then(|v| v.as_str())?; + + // Check for JS regex literal: /pattern/flags + let regex = if let Some(stripped) = pattern_str.strip_prefix('/') + && let Some(end) = stripped.rfind('/') + { + let (pat, _flags) = stripped.split_at(end); + Regex::new(pat).ok()? + } else { + let reg_str = format!("(?u){pattern_str}"); + Regex::new(®_str).ok()? + }; + let message = pattern.get(1).and_then(serde_json::Value::as_str).map(CompactStr::from); - Some((reg, message)) + Some((regex, message)) } } } @@ -306,6 +353,7 @@ fn validate_title( ) { if title.is_empty() { Message::EmptyTitle.diagnostic(ctx, span); + return; } if !valid_title.disallowed_words.is_empty() { @@ -585,6 +633,35 @@ fn test() { None, ), ("it(abc, function () {})", Some(serde_json::json!([{ "ignoreTypeOfTestName": true }]))), + // Vitest-specific tests with allowArguments option + ("it(foo, () => {});", Some(serde_json::json!([{ "allowArguments": true }]))), + ("describe(bar, () => {});", Some(serde_json::json!([{ "allowArguments": true }]))), + ("test(baz, () => {});", Some(serde_json::json!([{ "allowArguments": true }]))), + // Vitest-specific tests with .extend() + ( + "export const myTest = test.extend({ + archive: [] + })", + None, + ), + ("const localTest = test.extend({})", None), + ( + "import { it } from 'vitest' + + const test = it.extend({ + fixture: [ + async ({}, use) => { + setup() + await use() + teardown() + }, + { auto: true } + ], + }) + + test('', () => {})", + None, + ), ]; let fail = vec![ @@ -927,6 +1004,8 @@ fn test() { None, ), ("it(abc, function () {})", None), + // Vitest-specific fail test with allowArguments: false + ("test(bar, () => {});", Some(serde_json::json!([{ "allowArguments": false }]))), ]; let fix = vec![ diff --git a/crates/oxc_linter/src/snapshots/jest_valid_title.snap b/crates/oxc_linter/src/snapshots/jest_valid_title.snap index c397c3e8aec11..e752f233f61ff 100644 --- a/crates/oxc_linter/src/snapshots/jest_valid_title.snap +++ b/crates/oxc_linter/src/snapshots/jest_valid_title.snap @@ -574,3 +574,10 @@ source: crates/oxc_linter/src/tester.rs · ─── ╰──── help: "Replace your title with a string" + + ⚠ eslint-plugin-jest(valid-title): "Title must be a string" + ╭─[valid_title.tsx:1:6] + 1 │ test(bar, () => {}); + · ─── + ╰──── + help: "Replace your title with a string" diff --git a/crates/oxc_linter/src/snapshots/schema_json.snap b/crates/oxc_linter/src/snapshots/schema_json.snap index e86a54aa78dc2..4585b4884d04e 100644 --- a/crates/oxc_linter/src/snapshots/schema_json.snap +++ b/crates/oxc_linter/src/snapshots/schema_json.snap @@ -113,6 +113,9 @@ expression: json "implementsReplacesDocs": false, "exemptDestructuredRootsFromChecks": false, "tagNamePreference": {} + }, + "vitest": { + "typecheck": false } }, "allOf": [ @@ -538,6 +541,16 @@ expression: json "$ref": "#/definitions/ReactPluginSettings" } ] + }, + "vitest": { + "default": { + "typecheck": false + }, + "allOf": [ + { + "$ref": "#/definitions/VitestPluginSettings" + } + ] } } }, @@ -598,6 +611,17 @@ expression: json "type": "boolean" } ] + }, + "VitestPluginSettings": { + "description": "Configure Vitest plugin rules.\n\nSee [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)'s\nconfiguration for a full reference.", + "type": "object", + "properties": { + "typecheck": { + "description": "Whether to enable typecheck mode for Vitest rules.\nWhen enabled, some rules will skip certain checks for describe blocks\nto accommodate TypeScript type checking scenarios.", + "default": false, + "type": "boolean" + } + } } } } diff --git a/npm/oxlint/configuration_schema.json b/npm/oxlint/configuration_schema.json index c534a3e1eea09..1ca90ad338b23 100644 --- a/npm/oxlint/configuration_schema.json +++ b/npm/oxlint/configuration_schema.json @@ -109,6 +109,9 @@ "implementsReplacesDocs": false, "exemptDestructuredRootsFromChecks": false, "tagNamePreference": {} + }, + "vitest": { + "typecheck": false } }, "allOf": [ @@ -534,6 +537,16 @@ "$ref": "#/definitions/ReactPluginSettings" } ] + }, + "vitest": { + "default": { + "typecheck": false + }, + "allOf": [ + { + "$ref": "#/definitions/VitestPluginSettings" + } + ] } } }, @@ -594,6 +607,17 @@ "type": "boolean" } ] + }, + "VitestPluginSettings": { + "description": "Configure Vitest plugin rules.\n\nSee [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)'s\nconfiguration for a full reference.", + "type": "object", + "properties": { + "typecheck": { + "description": "Whether to enable typecheck mode for Vitest rules.\nWhen enabled, some rules will skip certain checks for describe blocks\nto accommodate TypeScript type checking scenarios.", + "default": false, + "type": "boolean" + } + } } } } \ No newline at end of file diff --git a/tasks/website/src/linter/snapshots/schema_markdown.snap b/tasks/website/src/linter/snapshots/schema_markdown.snap index 9ad8abced7cb5..8a2689187de75 100644 --- a/tasks/website/src/linter/snapshots/schema_markdown.snap +++ b/tasks/website/src/linter/snapshots/schema_markdown.snap @@ -540,3 +540,29 @@ Example: ##### settings.react.linkComponents[n] + + + + + + +### settings.vitest + +type: `object` + + +Configure Vitest plugin rules. + +See [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)'s +configuration for a full reference. + + +#### settings.vitest.typecheck + +type: `boolean` + +default: `false` + +Whether to enable typecheck mode for Vitest rules. +When enabled, some rules will skip certain checks for describe blocks +to accommodate TypeScript type checking scenarios.