Skip to content

Commit bd74603

Browse files
taearlsautofix-ci[bot]claude
authored
feat(linter): add support for vitest/valid-title rule (#12085)
I noticed that there's already a fully working implementation of `jest/valid-title`, and the logic is very similar. However, the vitest rule has slightly different configuration options, so it's not quite the same. I made the `vitest/valid-title` rule work with their specific configuration options and unit tests. As part of this, I added support for the vitest typecheck option so that all the vitest-specific test scenarios would pass for this rule. This meant adding a `VitestPluginSettings` module with `typecheck` available as a boolean, defaulting to false. I considered trying to reuse some of the logic from the jest rule equivalent, but since there are subtle differences I didn't think the refactor was worth it on a first pass. The only tradeoff is that there's some duplicate logic between the jest / vitest rules for this. Future consideration: The only other rule that uses `typecheck` as a config setting in `eslint-plugin-vitest` is the `expect-expect` rule, which we currently have listed in the [VITEST_COMPATIBLE_JEST_RULES](https://github.com/oxc-project/oxc/blob/main/crates/oxc_linter/src/utils/mod.rs#L34). It might be worth adding support for `typecheck` in that rule for 100% compatibility. ---- Reference: Vitest Config Options from `vitest/valid_title.rs`): ```ts interface Options { ignoreTypeOfDescribeName?: boolean; allowArguments?: boolean; disallowedWords?: string[]; mustNotMatch?: Partial<Record<'describe' | 'test' | 'it', string>> | string; mustMatch?: Partial<Record<'describe' | 'test' | 'it', string>> | string; } ``` Jest Config Options (from `jest/valid_title.rs`): ```ts interface Options { ignoreSpaces?: boolean; ignoreTypeOfTestName?: boolean; ignoreTypeOfDescribeName?: boolean; disallowedWords?: string[]; mustNotMatch?: Partial<Record<'describe' | 'test' | 'it', string>> | string; mustMatch?: Partial<Record<'describe' | 'test' | 'it', string>> | string; } ``` [Vitest valid-title Rule Docs](https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-title.md) [Jest valid-title Rule Docs](https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/valid-title.md) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude <[email protected]>
1 parent 40eb394 commit bd74603

File tree

10 files changed

+194
-8
lines changed

10 files changed

+194
-8
lines changed

apps/oxlint/src/snapshots/_-c fixtures__print_config__ban_rules__eslintrc.json -A all -D eqeqeq [email protected]

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ working directory:
4242
"implementsReplacesDocs": false,
4343
"exemptDestructuredRootsFromChecks": false,
4444
"tagNamePreference": {}
45+
},
46+
"vitest": {
47+
"typecheck": false
4548
}
4649
},
4750
"env": {

apps/oxlint/src/snapshots/fixtures_-A all [email protected]

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ working directory: fixtures
3535
"implementsReplacesDocs": false,
3636
"exemptDestructuredRootsFromChecks": false,
3737
"tagNamePreference": {}
38+
},
39+
"vitest": {
40+
"typecheck": false
3841
}
3942
},
4043
"env": {

crates/oxc_linter/src/config/settings/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ pub mod jsdoc;
22
mod jsx_a11y;
33
mod next;
44
mod react;
5+
pub mod vitest;
56

67
use schemars::JsonSchema;
78
use serde::{Deserialize, Serialize};
89

910
use self::{
1011
jsdoc::JSDocPluginSettings, jsx_a11y::JSXA11yPluginSettings, next::NextPluginSettings,
11-
react::ReactPluginSettings,
12+
react::ReactPluginSettings, vitest::VitestPluginSettings,
1213
};
1314

1415
/// # Oxlint Plugin Settings
@@ -52,6 +53,9 @@ pub struct OxlintSettings {
5253

5354
#[serde(default)]
5455
pub jsdoc: JSDocPluginSettings,
56+
57+
#[serde(default)]
58+
pub vitest: VitestPluginSettings,
5559
}
5660

5761
#[cfg(test)]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
use schemars::JsonSchema;
2+
use serde::{Deserialize, Serialize};
3+
4+
/// Configure Vitest plugin rules.
5+
///
6+
/// See [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)'s
7+
/// configuration for a full reference.
8+
#[derive(Debug, Clone, Deserialize, Serialize, Default, JsonSchema)]
9+
#[cfg_attr(test, derive(PartialEq, Eq))]
10+
pub struct VitestPluginSettings {
11+
/// Whether to enable typecheck mode for Vitest rules.
12+
/// When enabled, some rules will skip certain checks for describe blocks
13+
/// to accommodate TypeScript type checking scenarios.
14+
#[serde(default)]
15+
pub typecheck: bool,
16+
}

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,12 +1260,12 @@ oxc_macros::declare_all_lint_rules! {
12601260
vitest::prefer_to_be_object,
12611261
vitest::prefer_to_be_truthy,
12621262
vitest::require_local_test_context_for_concurrent_snapshots,
1263-
vue::define_props_destructuring,
12641263
vue::define_emits_declaration,
12651264
vue::define_props_declaration,
1265+
vue::define_props_destructuring,
12661266
vue::max_props,
1267-
vue::no_import_compiler_macros,
12681267
vue::no_export_in_script_setup,
1268+
vue::no_import_compiler_macros,
12691269
vue::no_multiple_slot_args,
12701270
vue::no_required_prop_with_default,
12711271
vue::prefer_import_from_vue,

crates/oxc_linter/src/rules/jest/valid_title.rs

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ pub struct ValidTitle(Box<ValidTitleConfig>);
2828
pub struct ValidTitleConfig {
2929
ignore_type_of_test_name: bool,
3030
ignore_type_of_describe_name: bool,
31+
allow_arguments: bool,
3132
disallowed_words: Vec<CompactStr>,
3233
ignore_space: bool,
3334
must_not_match_patterns: FxHashMap<MatchKind, CompiledMatcherAndMessage>,
@@ -45,7 +46,7 @@ impl std::ops::Deref for ValidTitle {
4546
declare_oxc_lint!(
4647
/// ### What it does
4748
///
48-
/// Checks that the titles of Jest blocks are valid.
49+
/// Checks that the titles of Jest and Vitest blocks are valid.
4950
///
5051
/// Titles must be:
5152
/// - not empty,
@@ -84,6 +85,7 @@ declare_oxc_lint!(
8485
/// ignoreSpaces?: boolean;
8586
/// ignoreTypeOfTestName?: boolean;
8687
/// ignoreTypeOfDescribeName?: boolean;
88+
/// allowArguments?: boolean;
8789
/// disallowedWords?: string[];
8890
/// mustNotMatch?: Partial<Record<'describe' | 'test' | 'it', string>> | string;
8991
/// mustMatch?: Partial<Record<'describe' | 'test' | 'it', string>> | string;
@@ -108,6 +110,7 @@ impl Rule for ValidTitle {
108110

109111
let ignore_type_of_test_name = get_as_bool("ignoreTypeOfTestName");
110112
let ignore_type_of_describe_name = get_as_bool("ignoreTypeOfDescribeName");
113+
let allow_arguments = get_as_bool("allowArguments");
111114
let ignore_space = get_as_bool("ignoreSpaces");
112115
let disallowed_words = config
113116
.and_then(|v| v.get("disallowedWords"))
@@ -125,6 +128,7 @@ impl Rule for ValidTitle {
125128
Self(Box::new(ValidTitleConfig {
126129
ignore_type_of_test_name,
127130
ignore_type_of_describe_name,
131+
allow_arguments,
128132
disallowed_words,
129133
ignore_space,
130134
must_not_match_patterns,
@@ -159,10 +163,29 @@ impl ValidTitle {
159163
return;
160164
}
161165

166+
// Check if extend keyword has been used (vitest feature)
167+
if let Some(member) = jest_fn_call.members.first()
168+
&& member.is_name_equal("extend")
169+
{
170+
return;
171+
}
172+
162173
let Some(arg) = call_expr.arguments.first() else {
163174
return;
164175
};
165176

177+
// Handle typecheck settings - skip for describe when enabled (vitest feature)
178+
if ctx.settings().vitest.typecheck
179+
&& matches!(jest_fn_call.kind, JestFnKind::General(JestGeneralFnKind::Describe))
180+
{
181+
return;
182+
}
183+
184+
// Handle allowArguments option (vitest feature)
185+
if self.allow_arguments && matches!(arg, Argument::Identifier(_)) {
186+
return;
187+
}
188+
166189
let need_report_name = match jest_fn_call.kind {
167190
JestFnKind::General(JestGeneralFnKind::Test) => !self.ignore_type_of_test_name,
168191
JestFnKind::General(JestGeneralFnKind::Describe) => !self.ignore_type_of_describe_name,
@@ -284,15 +307,39 @@ fn compile_matcher_patterns(
284307
fn compile_matcher_pattern(pattern: MatcherPattern) -> Option<CompiledMatcherAndMessage> {
285308
match pattern {
286309
MatcherPattern::String(pattern) => {
287-
let reg_str = format!("(?u){}", pattern.as_str()?);
310+
let pattern_str = pattern.as_str()?;
311+
312+
// Check for JS regex literal: /pattern/flags
313+
if let Some(stripped) = pattern_str.strip_prefix('/')
314+
&& let Some(end) = stripped.rfind('/')
315+
{
316+
let (pat, _flags) = stripped.split_at(end);
317+
// For now, ignore flags and just use the pattern
318+
let regex = Regex::new(pat).ok()?;
319+
return Some((regex, None));
320+
}
321+
322+
// Fallback: treat as a normal Rust regex with Unicode support
323+
let reg_str = format!("(?u){pattern_str}");
288324
let reg = Regex::new(&reg_str).ok()?;
289325
Some((reg, None))
290326
}
291327
MatcherPattern::Vec(pattern) => {
292-
let reg_str = pattern.first().and_then(|v| v.as_str()).map(|v| format!("(?u){v}"))?;
293-
let reg = Regex::new(&reg_str).ok()?;
328+
let pattern_str = pattern.first().and_then(|v| v.as_str())?;
329+
330+
// Check for JS regex literal: /pattern/flags
331+
let regex = if let Some(stripped) = pattern_str.strip_prefix('/')
332+
&& let Some(end) = stripped.rfind('/')
333+
{
334+
let (pat, _flags) = stripped.split_at(end);
335+
Regex::new(pat).ok()?
336+
} else {
337+
let reg_str = format!("(?u){pattern_str}");
338+
Regex::new(&reg_str).ok()?
339+
};
340+
294341
let message = pattern.get(1).and_then(serde_json::Value::as_str).map(CompactStr::from);
295-
Some((reg, message))
342+
Some((regex, message))
296343
}
297344
}
298345
}
@@ -306,6 +353,7 @@ fn validate_title(
306353
) {
307354
if title.is_empty() {
308355
Message::EmptyTitle.diagnostic(ctx, span);
356+
return;
309357
}
310358

311359
if !valid_title.disallowed_words.is_empty() {
@@ -585,6 +633,35 @@ fn test() {
585633
None,
586634
),
587635
("it(abc, function () {})", Some(serde_json::json!([{ "ignoreTypeOfTestName": true }]))),
636+
// Vitest-specific tests with allowArguments option
637+
("it(foo, () => {});", Some(serde_json::json!([{ "allowArguments": true }]))),
638+
("describe(bar, () => {});", Some(serde_json::json!([{ "allowArguments": true }]))),
639+
("test(baz, () => {});", Some(serde_json::json!([{ "allowArguments": true }]))),
640+
// Vitest-specific tests with .extend()
641+
(
642+
"export const myTest = test.extend({
643+
archive: []
644+
})",
645+
None,
646+
),
647+
("const localTest = test.extend({})", None),
648+
(
649+
"import { it } from 'vitest'
650+
651+
const test = it.extend({
652+
fixture: [
653+
async ({}, use) => {
654+
setup()
655+
await use()
656+
teardown()
657+
},
658+
{ auto: true }
659+
],
660+
})
661+
662+
test('', () => {})",
663+
None,
664+
),
588665
];
589666

590667
let fail = vec![
@@ -927,6 +1004,8 @@ fn test() {
9271004
None,
9281005
),
9291006
("it(abc, function () {})", None),
1007+
// Vitest-specific fail test with allowArguments: false
1008+
("test(bar, () => {});", Some(serde_json::json!([{ "allowArguments": false }]))),
9301009
];
9311010

9321011
let fix = vec![

crates/oxc_linter/src/snapshots/jest_valid_title.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,3 +574,10 @@ source: crates/oxc_linter/src/tester.rs
574574
· ───
575575
╰────
576576
help: "Replace your title with a string"
577+
578+
eslint-plugin-jest(valid-title): "Title must be a string"
579+
╭─[valid_title.tsx:1:6]
580+
1test(bar, () => {});
581+
· ───
582+
╰────
583+
help: "Replace your title with a string"

crates/oxc_linter/src/snapshots/schema_json.snap

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ expression: json
113113
"implementsReplacesDocs": false,
114114
"exemptDestructuredRootsFromChecks": false,
115115
"tagNamePreference": {}
116+
},
117+
"vitest": {
118+
"typecheck": false
116119
}
117120
},
118121
"allOf": [
@@ -538,6 +541,16 @@ expression: json
538541
"$ref": "#/definitions/ReactPluginSettings"
539542
}
540543
]
544+
},
545+
"vitest": {
546+
"default": {
547+
"typecheck": false
548+
},
549+
"allOf": [
550+
{
551+
"$ref": "#/definitions/VitestPluginSettings"
552+
}
553+
]
541554
}
542555
}
543556
},
@@ -598,6 +611,17 @@ expression: json
598611
"type": "boolean"
599612
}
600613
]
614+
},
615+
"VitestPluginSettings": {
616+
"description": "Configure Vitest plugin rules.\n\nSee [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)'s\nconfiguration for a full reference.",
617+
"type": "object",
618+
"properties": {
619+
"typecheck": {
620+
"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.",
621+
"default": false,
622+
"type": "boolean"
623+
}
624+
}
601625
}
602626
}
603627
}

npm/oxlint/configuration_schema.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@
109109
"implementsReplacesDocs": false,
110110
"exemptDestructuredRootsFromChecks": false,
111111
"tagNamePreference": {}
112+
},
113+
"vitest": {
114+
"typecheck": false
112115
}
113116
},
114117
"allOf": [
@@ -534,6 +537,16 @@
534537
"$ref": "#/definitions/ReactPluginSettings"
535538
}
536539
]
540+
},
541+
"vitest": {
542+
"default": {
543+
"typecheck": false
544+
},
545+
"allOf": [
546+
{
547+
"$ref": "#/definitions/VitestPluginSettings"
548+
}
549+
]
537550
}
538551
}
539552
},
@@ -594,6 +607,17 @@
594607
"type": "boolean"
595608
}
596609
]
610+
},
611+
"VitestPluginSettings": {
612+
"description": "Configure Vitest plugin rules.\n\nSee [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)'s\nconfiguration for a full reference.",
613+
"type": "object",
614+
"properties": {
615+
"typecheck": {
616+
"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.",
617+
"default": false,
618+
"type": "boolean"
619+
}
620+
}
597621
}
598622
}
599623
}

tasks/website/src/linter/snapshots/schema_markdown.snap

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,3 +540,29 @@ Example:
540540

541541

542542
##### settings.react.linkComponents[n]
543+
544+
545+
546+
547+
548+
549+
### settings.vitest
550+
551+
type: `object`
552+
553+
554+
Configure Vitest plugin rules.
555+
556+
See [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest)'s
557+
configuration for a full reference.
558+
559+
560+
#### settings.vitest.typecheck
561+
562+
type: `boolean`
563+
564+
default: `false`
565+
566+
Whether to enable typecheck mode for Vitest rules.
567+
When enabled, some rules will skip certain checks for describe blocks
568+
to accommodate TypeScript type checking scenarios.

0 commit comments

Comments
 (0)