feat(linter): support vitest/no-disabled-tests (#3717)

This commit is contained in:
mysteryven 2024-06-24 15:16:31 +00:00
parent ef82c78a72
commit 328445b4ca
15 changed files with 323 additions and 21 deletions

View file

@ -0,0 +1,5 @@
{
"rules": {
"vitest/no-disabled-tests": "error"
}
}

View file

@ -0,0 +1,3 @@
test.skip('foo', () => {
// ...
})

View file

@ -208,6 +208,10 @@ pub struct EnablePlugins {
#[bpaf(switch, hide_usage)]
pub jest_plugin: bool,
/// Enable the Vitest plugin and detect test problems
#[bpaf(switch, hide_usage)]
pub vitest_plugin: bool,
/// Enable the JSX-a11y plugin and detect accessibility problems
#[bpaf(switch, hide_usage)]
pub jsx_a11y_plugin: bool,

View file

@ -101,6 +101,7 @@ impl Runner for LintRunner {
.with_import_plugin(enable_plugins.import_plugin)
.with_jsdoc_plugin(enable_plugins.jsdoc_plugin)
.with_jest_plugin(enable_plugins.jest_plugin)
.with_vitest_plugin(enable_plugins.vitest_plugin)
.with_jsx_a11y_plugin(enable_plugins.jsx_a11y_plugin)
.with_nextjs_plugin(enable_plugins.nextjs_plugin)
.with_react_perf_plugin(enable_plugins.react_perf_plugin);
@ -488,4 +489,26 @@ mod test {
assert!(test_invalid_options(&["--tsconfig", "oxc/tsconfig.json"])
.contains("oxc/tsconfig.json\" does not exist, Please provide a valid tsconfig file."));
}
#[test]
fn test_enable_vitest_plugin() {
let args = &[
"-c",
"fixtures/eslintrc_vitest_replace/eslintrc.json",
"fixtures/eslintrc_vitest_replace/foo.js",
];
let result = test(args);
assert_eq!(result.number_of_files, 1);
assert_eq!(result.number_of_errors, 0);
let args = &[
"--vitest-plugin",
"-c",
"fixtures/eslintrc_vitest_replace/eslintrc.json",
"fixtures/eslintrc_vitest_replace/foo.js",
];
let result = test(args);
assert_eq!(result.number_of_files, 1);
assert_eq!(result.number_of_errors, 1);
}
}

View file

@ -0,0 +1,5 @@
{
"rules": {
"vitest/no-disabled-tests": "error"
}
}

View file

@ -16,7 +16,9 @@ pub use self::{
rules::OxlintRules,
settings::{jsdoc::JSDocPluginSettings, OxlintSettings},
};
use crate::{rules::RuleEnum, AllowWarnDeny, RuleWithSeverity};
use crate::{
rules::RuleEnum, utils::is_jest_rule_adapted_to_vitest, AllowWarnDeny, RuleWithSeverity,
};
/// Oxlint Configuration File
///
@ -113,8 +115,10 @@ impl OxlintConfig {
0 => unreachable!(),
1 => {
let rule_config = &rule_configs[0];
let rule_name = &rule_config.rule_name;
let plugin_name = &rule_config.plugin_name;
let (rule_name, plugin_name) = transform_rule_and_plugin_name(
&rule_config.rule_name,
&rule_config.plugin_name,
);
let severity = rule_config.severity;
match severity {
AllowWarnDeny::Warn | AllowWarnDeny::Deny => {
@ -168,12 +172,26 @@ impl OxlintConfig {
}
}
fn transform_rule_and_plugin_name<'a>(
rule_name: &'a str,
plugin_name: &'a str,
) -> (&'a str, &'a str) {
if plugin_name == "vitest" && is_jest_rule_adapted_to_vitest(rule_name) {
return (rule_name, "jest");
}
(rule_name, plugin_name)
}
#[cfg(test)]
mod test {
use std::env;
use rustc_hash::FxHashSet;
use serde::Deserialize;
use crate::rules::RULES;
use super::OxlintConfig;
#[test]
@ -221,4 +239,17 @@ mod test {
assert_eq!(env.iter().count(), 1);
assert!(globals.is_enabled("foo"));
}
#[test]
fn test_vitest_rule_replace() {
let fixture_path: std::path::PathBuf =
env::current_dir().unwrap().join("fixtures/eslint_config_vitest_replace.json");
let config = OxlintConfig::from_file(&fixture_path).unwrap();
let mut set = FxHashSet::default();
config.override_rules(&mut set, &RULES);
let rule = set.into_iter().next().unwrap();
assert_eq!(rule.name(), "no-disabled-tests");
assert_eq!(rule.plugin_name(), "jest");
}
}

View file

@ -7,6 +7,7 @@ use oxc_span::{SourceType, Span};
use oxc_syntax::module_record::ModuleRecord;
use crate::{
config::OxlintRules,
disable_directives::{DisableDirectives, DisableDirectivesBuilder},
fixer::{Fix, Message, RuleFixer},
javascript_globals::GLOBALS,
@ -131,6 +132,10 @@ impl<'a> LintContext<'a> {
&self.eslint_config.env
}
pub fn rules(&self) -> &OxlintRules {
&self.eslint_config.rules
}
pub fn env_contains_var(&self, var: &str) -> bool {
for env in self.env().iter() {
let env = GLOBALS.get(env).unwrap_or(&GLOBALS["builtin"]);

View file

@ -4,7 +4,10 @@ use oxc_diagnostics::{Error, OxcDiagnostic, Severity};
use rustc_hash::FxHashSet;
use serde_json::{Number, Value};
use crate::{config::OxlintConfig, rules::RULES, RuleCategory, RuleEnum, RuleWithSeverity};
use crate::{
config::OxlintConfig, rules::RULES, utils::is_jest_rule_adapted_to_vitest, RuleCategory,
RuleEnum, RuleWithSeverity,
};
#[derive(Debug)]
pub struct LintOptions {
@ -21,6 +24,7 @@ pub struct LintOptions {
pub import_plugin: bool,
pub jsdoc_plugin: bool,
pub jest_plugin: bool,
pub vitest_plugin: bool,
pub jsx_a11y_plugin: bool,
pub nextjs_plugin: bool,
pub react_perf_plugin: bool,
@ -39,6 +43,7 @@ impl Default for LintOptions {
import_plugin: false,
jsdoc_plugin: false,
jest_plugin: false,
vitest_plugin: false,
jsx_a11y_plugin: false,
nextjs_plugin: false,
react_perf_plugin: false,
@ -109,6 +114,12 @@ impl LintOptions {
self
}
#[must_use]
pub fn with_vitest_plugin(mut self, yes: bool) -> Self {
self.vitest_plugin = yes;
self
}
#[must_use]
pub fn with_jsx_a11y_plugin(mut self, yes: bool) -> Self {
self.jsx_a11y_plugin = yes;
@ -277,7 +288,15 @@ impl LintOptions {
"typescript" => self.typescript_plugin,
"import" => self.import_plugin,
"jsdoc" => self.jsdoc_plugin,
"jest" => self.jest_plugin,
"jest" => {
if self.jest_plugin {
return true;
}
if self.vitest_plugin && is_jest_rule_adapted_to_vitest(rule.name()) {
return true;
}
false
}
"jsx_a11y" => self.jsx_a11y_plugin,
"nextjs" => self.nextjs_plugin,
"react_perf" => self.react_perf_plugin,

View file

@ -7,8 +7,8 @@ use crate::{
context::LintContext,
rule::Rule,
utils::{
collect_possible_jest_call_node, parse_general_jest_fn_call, JestFnKind, JestGeneralFnKind,
ParsedGeneralJestFnCall, PossibleJestNode,
collect_possible_jest_call_node, get_test_plugin_name, parse_general_jest_fn_call,
JestFnKind, JestGeneralFnKind, ParsedGeneralJestFnCall, PossibleJestNode,
},
};
@ -48,14 +48,25 @@ declare_oxc_lint!(
/// pending();
/// });
/// ```
///
/// This rule is compatible with [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest/blob/main/docs/rules/no-disabled-tests.md),
/// to use it, add the following configuration to your `.eslintrc.json`:
///
/// ```json
/// {
/// "rules": {
/// "vitest/no-disabled-tests": "error"
/// }
/// }
/// ```
NoDisabledTests,
correctness
);
fn no_disabled_tests_diagnostic(x0: &str, x1: &str, span2: Span) -> OxcDiagnostic {
OxcDiagnostic::warn(format!("eslint-plugin-jest(no-disabled-tests): {x0:?}"))
.with_help(format!("{x1:?}"))
.with_labels([span2.into()])
fn no_disabled_tests_diagnostic(x0: &str, x1: &str, x2: &str, span3: Span) -> OxcDiagnostic {
OxcDiagnostic::warn(format!("{x0}(no-disabled-tests): {x1:?}"))
.with_help(format!("{x2:?}"))
.with_labels([span3.into()])
}
enum Message {
@ -82,13 +93,19 @@ impl Message {
impl Rule for NoDisabledTests {
fn run_once(&self, ctx: &LintContext) {
let plugin_name = get_test_plugin_name(ctx);
for possible_jest_node in &collect_possible_jest_call_node(ctx) {
run(possible_jest_node, ctx);
run(possible_jest_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: &str,
ctx: &LintContext<'a>,
) {
let node = possible_jest_node.node;
if let AstKind::CallExpression(call_expr) = node.kind() {
if let Some(jest_fn_call) = parse_general_jest_fn_call(call_expr, possible_jest_node, ctx) {
@ -103,7 +120,12 @@ fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>)
&& members.iter().all(|member| member.is_name_unequal("todo"))
{
let (error, help) = Message::MissingFunction.details();
ctx.diagnostic(no_disabled_tests_diagnostic(error, help, call_expr.span));
ctx.diagnostic(no_disabled_tests_diagnostic(
plugin_name,
error,
help,
call_expr.span,
));
return;
}
@ -115,7 +137,12 @@ fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>)
} else {
Message::DisabledTestWithX.details()
};
ctx.diagnostic(no_disabled_tests_diagnostic(error, help, call_expr.callee.span()));
ctx.diagnostic(no_disabled_tests_diagnostic(
plugin_name,
error,
help,
call_expr.callee.span(),
));
return;
}
@ -127,7 +154,12 @@ fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>)
} else {
Message::DisabledTestWithSkip.details()
};
ctx.diagnostic(no_disabled_tests_diagnostic(error, help, call_expr.callee.span()));
ctx.diagnostic(no_disabled_tests_diagnostic(
plugin_name,
error,
help,
call_expr.callee.span(),
));
}
} else if let Expression::Identifier(ident) = &call_expr.callee {
if ident.name.as_str() == "pending"
@ -135,7 +167,12 @@ fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>)
{
// `describe('foo', function () { pending() })`
let (error, help) = Message::Pending.details();
ctx.diagnostic(no_disabled_tests_diagnostic(error, help, call_expr.span));
ctx.diagnostic(no_disabled_tests_diagnostic(
plugin_name,
error,
help,
call_expr.span,
));
}
}
}
@ -145,7 +182,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('foo', function () {})", None),
("it('foo', function () {})", None),
("describe.only('foo', function () {})", None),
@ -184,7 +221,7 @@ fn test() {
("import { test } from './test-utils'; test('something');", None),
];
let fail = vec![
let mut fail = vec![
("describe.skip('foo', function () {})", None),
("describe.skip.each([1, 2, 3])('%s', (a, b) => {});", None),
("xdescribe.each([1, 2, 3])('%s', (a, b) => {});", None),
@ -213,5 +250,59 @@ fn test() {
("import { test } from '@jest/globals';test('something');", None),
];
Tester::new(NoDisabledTests::NAME, pass, fail).with_jest_plugin(true).test_and_snapshot();
let pass_vitest = vec![
r#"describe("foo", function () {})"#,
r#"it("foo", function () {})"#,
r#"describe.only("foo", function () {})"#,
r#"it.only("foo", function () {})"#,
r#"it.each("foo", () => {})"#,
r#"it.concurrent("foo", function () {})"#,
r#"test("foo", function () {})"#,
r#"test.only("foo", function () {})"#,
r#"test.concurrent("foo", function () {})"#,
r#"describe[`${"skip"}`]("foo", function () {})"#,
r#"it.todo("fill this later")"#,
"var appliedSkip = describe.skip; appliedSkip.apply(describe)",
"var calledSkip = it.skip; calledSkip.call(it)",
"({ f: function () {} }).f()",
"(a || b).f()",
"itHappensToStartWithIt()",
"testSomething()",
"xitSomethingElse()",
"xitiViewMap()",
r#"
import { pending } from "actions"
test("foo", () => {
expect(pending()).toEqual({})
})
"#,
"
import { test } from './test-utils';
test('something');
",
];
let fail_vitest = vec![
r#"describe.skip("foo", function () {})"#,
r#"xtest("foo", function () {})"#,
r#"xit.each``("foo", function () {})"#,
r#"xtest.each``("foo", function () {})"#,
r#"xit.each([])("foo", function () {})"#,
r#"it("has title but no callback")"#,
r#"test("has title but no callback")"#,
r#"it("contains a call to pending", function () { pending() })"#,
"pending();",
r#"
import { describe } from 'vitest';
describe.skip("foo", function () {})
"#,
];
pass.extend(pass_vitest.into_iter().map(|x| (x, None)));
fail.extend(fail_vitest.into_iter().map(|x| (x, None)));
Tester::new(NoDisabledTests::NAME, pass, fail)
.with_jest_plugin(true)
.with_vitest_plugin(true)
.test_and_snapshot();
}

View file

@ -182,3 +182,75 @@ source: crates/oxc_linter/src/tester.rs
· ─────────────────
╰────
help: "Add function argument"
⚠ eslint-plugin-jest(no-disabled-tests): "Disabled test suite"
╭─[no_disabled_tests.tsx:1:1]
1 │ describe.skip("foo", function () {})
· ─────────────
╰────
help: "Remove the appending `.skip`"
⚠ eslint-plugin-jest(no-disabled-tests): "Disabled test"
╭─[no_disabled_tests.tsx:1:1]
1 │ xtest("foo", function () {})
· ─────
╰────
help: "Remove x prefix"
⚠ eslint-plugin-jest(no-disabled-tests): "Disabled test"
╭─[no_disabled_tests.tsx:1:1]
1 │ xit.each``("foo", function () {})
· ──────────
╰────
help: "Remove x prefix"
⚠ eslint-plugin-jest(no-disabled-tests): "Disabled test"
╭─[no_disabled_tests.tsx:1:1]
1 │ xtest.each``("foo", function () {})
· ────────────
╰────
help: "Remove x prefix"
⚠ eslint-plugin-jest(no-disabled-tests): "Disabled test"
╭─[no_disabled_tests.tsx:1:1]
1 │ xit.each([])("foo", function () {})
· ────────────
╰────
help: "Remove x prefix"
⚠ eslint-plugin-jest(no-disabled-tests): "Test is missing function argument"
╭─[no_disabled_tests.tsx:1:1]
1 │ it("has title but no callback")
· ───────────────────────────────
╰────
help: "Add function argument"
⚠ eslint-plugin-jest(no-disabled-tests): "Test is missing function argument"
╭─[no_disabled_tests.tsx:1:1]
1 │ test("has title but no callback")
· ─────────────────────────────────
╰────
help: "Add function argument"
⚠ eslint-plugin-jest(no-disabled-tests): "Call to pending()"
╭─[no_disabled_tests.tsx:1:48]
1 │ it("contains a call to pending", function () { pending() })
· ─────────
╰────
help: "Remove pending() call"
⚠ eslint-plugin-jest(no-disabled-tests): "Call to pending()"
╭─[no_disabled_tests.tsx:1:1]
1 │ pending();
· ─────────
╰────
help: "Remove pending() call"
⚠ eslint-plugin-vitest(no-disabled-tests): "Disabled test suite"
╭─[no_disabled_tests.tsx:3:13]
2 │ import { describe } from 'vitest';
3 │ describe.skip("foo", function () {})
· ─────────────
4 │
╰────
help: "Remove the appending `.skip`"

View file

@ -96,6 +96,7 @@ pub struct Tester {
current_working_directory: Box<Path>,
import_plugin: bool,
jest_plugin: bool,
vitest_plugin: bool,
jsx_a11y_plugin: bool,
nextjs_plugin: bool,
react_perf_plugin: bool,
@ -125,6 +126,7 @@ impl Tester {
jsx_a11y_plugin: false,
nextjs_plugin: false,
react_perf_plugin: false,
vitest_plugin: false,
}
}
@ -150,6 +152,11 @@ impl Tester {
self
}
pub fn with_vitest_plugin(mut self, yes: bool) -> Self {
self.vitest_plugin = yes;
self
}
pub fn with_jsx_a11y_plugin(mut self, yes: bool) -> Self {
self.jsx_a11y_plugin = yes;
self
@ -255,6 +262,7 @@ impl Tester {
.with_fix(is_fix)
.with_import_plugin(self.import_plugin)
.with_jest_plugin(self.jest_plugin)
.with_vitest_plugin(self.vitest_plugin)
.with_jsx_a11y_plugin(self.jsx_a11y_plugin)
.with_nextjs_plugin(self.nextjs_plugin)
.with_react_perf_plugin(self.react_perf_plugin);

View file

@ -206,7 +206,7 @@ fn collect_ids_referenced_to_import<'a>(
};
let name = ctx.symbols().get_name(symbol_id);
if import_decl.source.value == "@jest/globals" {
if matches!(import_decl.source.value.as_str(), "@jest/globals" | "vitest") {
let original = find_original_name(import_decl, name);
let mut ret = vec![];
for reference_id in reference_ids {

View file

@ -6,6 +6,39 @@ mod react_perf;
mod tree_shaking;
mod unicorn;
use crate::LintContext;
pub use self::{
jest::*, jsdoc::*, nextjs::*, react::*, react_perf::*, tree_shaking::*, unicorn::*,
};
/// Check if the Jest rule is adapted to Vitest.
/// 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 {
matches!(rule_name, "no-disabled-tests")
}
pub fn get_test_plugin_name(ctx: &LintContext) -> &'static str {
if is_using_vitest(ctx) {
"eslint-plugin-vitest"
} else {
"eslint-plugin-jest"
}
}
fn is_using_vitest(ctx: &LintContext) -> bool {
// If import 'vitest' is found, we assume the user is using vitest.
if ctx
.semantic()
.module_record()
.import_entries
.iter()
.any(|entry| entry.module_request.name().as_str() == "vitest")
{
return true;
}
// Or, find the eslint config file
ctx.rules().iter().any(|rule| rule.plugin_name == "vitest")
}

View file

@ -58,6 +58,8 @@ Arguments:
Enable the experimental jsdoc plugin and detect JSDoc problems
- **` --jest-plugin`** &mdash;
Enable the Jest plugin and detect test problems
- **` --vitest-plugin`** &mdash;
Enable the Vitest plugin and detect test problems
- **` --jsx-a11y-plugin`** &mdash;
Enable the JSX-a11y plugin and detect accessibility problems
- **` --nextjs-plugin`** &mdash;

View file

@ -35,6 +35,7 @@ Enable Plugins
recommended to use along side with the `--tsconfig` option.
--jsdoc-plugin Enable the experimental jsdoc plugin and detect JSDoc problems
--jest-plugin Enable the Jest plugin and detect test problems
--vitest-plugin Enable the Vitest plugin and detect test problems
--jsx-a11y-plugin Enable the JSX-a11y plugin and detect accessibility problems
--nextjs-plugin Enable the Next.js plugin and detect Next.js problems
--react-perf-plugin Enable the React performance plugin and detect rendering performance