Skip to content

Commit

Permalink
feat(linter): support vitest/no-disabled-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mysteryven committed Jun 17, 2024
1 parent d65c652 commit 329d875
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 19 deletions.
4 changes: 4 additions & 0 deletions apps/oxlint/src/command/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,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,
Expand Down
1 change: 1 addition & 0 deletions apps/oxlint/src/lint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions crates/oxc_linter/fixtures/eslint_config_vitest_replace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"vitest/no-disabled-tests": "error"
}
}
36 changes: 34 additions & 2 deletions crates/oxc_linter/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,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 => {
Expand Down Expand Up @@ -167,9 +169,26 @@ impl OxlintConfig {
}
}

fn transform_rule_and_plugin_name<'a>(
rule_name: &'a str,
plugin_name: &'a str,
) -> (&'a str, &'a str) {
// For the rules in eslint-plugin-vitest, if it has already existed in eslint-plugin-jest,
// we will redirect it to corresponding rule in eslint-plugin-jest.
// Not all rules has been adopted now.
if plugin_name == "vitest" && matches!(rule_name, "no-disabled-tests") {
return (rule_name, "jest");
}

(rule_name, plugin_name)
}

#[cfg(test)]
mod test {
use crate::rules::RULES;

use super::OxlintConfig;
use rustc_hash::FxHashSet;
use serde::Deserialize;
use std::env;

Expand Down Expand Up @@ -218,4 +237,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");
}
}
5 changes: 5 additions & 0 deletions crates/oxc_linter/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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,
Expand Down Expand Up @@ -113,6 +114,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"]);
Expand Down
12 changes: 11 additions & 1 deletion crates/oxc_linter/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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,
Expand All @@ -40,6 +41,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,
Expand Down Expand Up @@ -110,6 +112,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;
Expand Down Expand Up @@ -278,7 +286,9 @@ impl LintOptions {
"typescript" => self.typescript_plugin,
"import" => self.import_plugin,
"jsdoc" => self.jsdoc_plugin,
"jest" => self.jest_plugin,
// Some jest rules are also used in vitest
// go to `normalize_rule_and_plugin_name` to see the details.
"jest" => self.jest_plugin || self.vitest_plugin,
"jsx_a11y" => self.jsx_a11y_plugin,
"nextjs" => self.nextjs_plugin,
"react_perf" => self.react_perf_plugin,
Expand Down
110 changes: 95 additions & 15 deletions crates/oxc_linter/src/rules/jest/no_disabled_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,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,
},
};

Expand Down Expand Up @@ -53,10 +53,10 @@ declare_oxc_lint!(
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 {
Expand All @@ -83,13 +83,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) {
Expand All @@ -104,7 +110,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;
}

Expand All @@ -116,7 +127,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;
}

Expand All @@ -128,15 +144,25 @@ 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"
&& ctx.semantic().is_reference_to_global_variable(ident)
{
// `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,
));
}
}
}
Expand All @@ -146,7 +172,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),
Expand All @@ -173,7 +199,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),
Expand Down Expand Up @@ -202,5 +228,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();
}
Loading

0 comments on commit 329d875

Please sign in to comment.