diff --git a/apps/oxlint/fixtures/disable_vitest_rules/.oxlintrc-vitest.json b/apps/oxlint/fixtures/disable_vitest_rules/.oxlintrc-vitest.json new file mode 100644 index 0000000000000..9d1b7bb79a2c1 --- /dev/null +++ b/apps/oxlint/fixtures/disable_vitest_rules/.oxlintrc-vitest.json @@ -0,0 +1,15 @@ +{ + "plugins": ["vitest"], + "categories": { + "correctness": "off" + }, + "rules": { + "vitest/no-restricted-vi-methods": [ + "error", + { + "advanceTimersByTime": null, + "spyOn": "Don't use spies" + } + ] + } +} diff --git a/apps/oxlint/fixtures/disable_vitest_rules/test.js b/apps/oxlint/fixtures/disable_vitest_rules/test.js new file mode 100644 index 0000000000000..b56ea64353508 --- /dev/null +++ b/apps/oxlint/fixtures/disable_vitest_rules/test.js @@ -0,0 +1,24 @@ +// File should have a total of 1 error from the vitest rule, and 1 warning about an unnecessary disable. +import { vi } from 'vitest'; + +vi.useFakeTimers(); + +it('calls the callback after 1 second via advanceTimersByTime', () => { + vi.advanceTimersByTime(1000); +}) + +test('plays video', () => { + // oxlint-disable vitest/no-restricted-vi-methods + vi.spyOn(audio, 'play'); // this is disabled by the block above, should be no error. + // oxlint-enable vitest/no-restricted-vi-methods + + // oxlint-disable-next-line vitest/no-restricted-vi-methods + const spy = vi.spyOn(video, 'play'); // this is disabled by the line above, should be no error. + + // This one should trigger a warning about an unnecessary disable: + // oxlint-disable-next-line vitest/no-restricted-vi-methods + video.play(); + + // Next line should not have an error, as we disable the rule. + vi.spyOn(audio, 'play'); // oxlint-disable-line vitest/no-restricted-vi-methods +}) diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index f82610a83ac21..31fb353f1e7e6 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -1152,6 +1152,16 @@ mod test { .test_and_snapshot_multiple(&[args_1, args_2]); } + #[test] + // Test to ensure that a vitest rule based on the jest rule is + // handled correctly when it has a different name. + // e.g. `vitest/no-restricted-vi-methods` vs `jest/no-restricted-jest-methods` + fn test_disable_vitest_rules() { + let args = + &["-c", ".oxlintrc-vitest.json", "--report-unused-disable-directives", "test.js"]; + Tester::new().with_cwd("fixtures/disable_vitest_rules".into()).test_and_snapshot(args); + } + #[test] fn test_two_rules_with_same_rule_name_from_different_plugins() { // Issue: diff --git a/apps/oxlint/src/snapshots/fixtures__disable_vitest_rules_-c .oxlintrc-vitest.json --report-unused-disable-directives test.js@oxlint.snap b/apps/oxlint/src/snapshots/fixtures__disable_vitest_rules_-c .oxlintrc-vitest.json --report-unused-disable-directives test.js@oxlint.snap new file mode 100644 index 0000000000000..38be2e6ff2fee --- /dev/null +++ b/apps/oxlint/src/snapshots/fixtures__disable_vitest_rules_-c .oxlintrc-vitest.json --report-unused-disable-directives test.js@oxlint.snap @@ -0,0 +1,29 @@ +--- +source: apps/oxlint/src/tester.rs +--- +########## +arguments: -c .oxlintrc-vitest.json --report-unused-disable-directives test.js +working directory: fixtures/disable_vitest_rules +---------- + + x ]8;;https://oxc.rs/docs/guide/usage/linter/rules/jest/no-restricted-jest-methods.html\eslint-plugin-jest(no-restricted-jest-methods)]8;;\: Use of `advanceTimersByTime` is not allowed + ,-[test.js:7:6] + 6 | it('calls the callback after 1 second via advanceTimersByTime', () => { + 7 | vi.advanceTimersByTime(1000); + : ^^^^^^^^^^^^^^^^^^^ + 8 | }) + `---- + + ! Unused eslint-disable directive (no problems were reported). + ,-[test.js:19:5] + 18 | // This one should trigger a warning about an unnecessary disable: + 19 | // oxlint-disable-next-line vitest/no-restricted-vi-methods + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 20 | video.play(); + `---- + +Found 1 warning and 1 error. +Finished in ms on 1 file with 1 rules using 1 threads. +---------- +CLI result: LintFoundErrors +---------- diff --git a/crates/oxc_linter/src/config/rules.rs b/crates/oxc_linter/src/config/rules.rs index 70a34b08adf09..3190ff87a3cc4 100644 --- a/crates/oxc_linter/src/config/rules.rs +++ b/crates/oxc_linter/src/config/rules.rs @@ -147,6 +147,11 @@ fn transform_rule_and_plugin_name<'a>( rule_name: &'a str, plugin_name: &'a str, ) -> (&'a str, &'a str) { + // Special case: vitest/no-restricted-vi-methods is implemented by jest/no-restricted-jest-methods + if plugin_name == "vitest" && rule_name == "no-restricted-vi-methods" { + return ("no-restricted-jest-methods", "jest"); + } + let plugin_name = match plugin_name { "vitest" if is_jest_rule_adapted_to_vitest(rule_name) => "jest", "unicorn" if rule_name == "no-negated-condition" => "eslint", diff --git a/crates/oxc_linter/src/disable_directives.rs b/crates/oxc_linter/src/disable_directives.rs index 72e0e4cb98b51..04b89d9782026 100644 --- a/crates/oxc_linter/src/disable_directives.rs +++ b/crates/oxc_linter/src/disable_directives.rs @@ -196,10 +196,21 @@ impl DisableDirectives { // Check if this rule should be disabled let rule_matches = match &interval.val { DisabledRule::All { .. } => true, - // Our rule name currently does not contain the prefix. - // For example, this will match `@typescript-eslint/no-var-requires` given - // our rule_name is `no-var-requires`. - DisabledRule::Single { rule_name: name, .. } => name.contains(rule_name), + // `rule_name` does not contain the prefix. + // - `vitest/foobar` will be just `foobar`. + // - `@typescript-eslint/no-var-requires` will be just `no-var-requires` + // + // This enables matching rules across different plugins that share the same + // rule name, such as jest<->vitest rules and eslint<->typescript rules. + DisabledRule::Single { rule_name: name, .. } => { + if name.contains(rule_name) { + return true; + } + + // Special-case mapping: `vitest/no-restricted-vi-methods` is implemented by `jest/no-restricted-jest-methods`. + return name == "vitest/no-restricted-vi-methods" + && rule_name == "no-restricted-jest-methods"; + } }; if !rule_matches { @@ -360,7 +371,7 @@ impl DisableDirectivesBuilder { if let Some(text) = text.strip_prefix("eslint-disable").or_else(|| text.strip_prefix("oxlint-disable")) { - rule_name_start += 14; // eslint-disable is 14 bytes + rule_name_start += 14; // eslint-disable and oxlint-disable are each 14 bytes // `eslint-disable` if text.trim().is_empty() { if self.disable_all_start.is_none() { diff --git a/crates/oxc_linter/src/rules/jest/no_restricted_jest_methods.rs b/crates/oxc_linter/src/rules/jest/no_restricted_jest_methods.rs index c09ff9e1f42c4..595423e65b73f 100644 --- a/crates/oxc_linter/src/rules/jest/no_restricted_jest_methods.rs +++ b/crates/oxc_linter/src/rules/jest/no_restricted_jest_methods.rs @@ -26,7 +26,8 @@ pub struct NoRestrictedJestMethods(Box); #[derive(Debug, Default, Clone, JsonSchema, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct NoRestrictedJestMethodsConfig { - /// A mapping of restricted Jest method names to custom messages. + /// A mapping of restricted Jest method names to custom messages - or + /// `null`, for a generic message. restricted_jest_methods: FxHashMap, } @@ -45,10 +46,13 @@ declare_oxc_lint!( /// /// ### Why is this bad? /// - /// Certain Jest methods may be deprecated, discouraged in specific + /// Certain Jest or Vitest methods may be deprecated, discouraged in specific /// contexts, or incompatible with your testing environment. Restricting /// them helps maintain consistent and reliable test practices. /// + /// By default, no methods are restricted by this rule. + /// You must configure the rule for it to disable anything. + /// /// ### Examples /// /// Examples of **incorrect** code for this rule: @@ -75,7 +79,7 @@ declare_oxc_lint!( /// ```json /// { /// "rules": { - /// "vitest/no-restricted-vi-methods": "error" + /// "vitest/no-restricted-vi-methods": ["error", { "badFunction": "Don't use `badFunction`, it is bad." }] /// } /// } /// ```