Skip to content

Commit

Permalink
feat(linter/eslint-plugin-vitest): implement no-restricted-vi-methods
Browse files Browse the repository at this point in the history
  • Loading branch information
shulaoda committed Aug 19, 2024
1 parent 247120f commit f805797
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 114 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ mod promise {
mod vitest {
pub mod no_conditional_tests;
pub mod no_import_node_test;
pub mod no_restricted_vi_methods;
pub mod prefer_to_be_falsy;
pub mod prefer_to_be_truthy;
}
Expand Down Expand Up @@ -870,4 +871,5 @@ oxc_macros::declare_all_lint_rules! {
vitest::prefer_to_be_falsy,
vitest::prefer_to_be_truthy,
vitest::no_conditional_tests,
vitest::no_restricted_vi_methods,
}
120 changes: 11 additions & 109 deletions crates/oxc_linter/src/rules/jest/no_restricted_jest_methods.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,16 @@
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;
use rustc_hash::FxHashMap;

use crate::{
context::LintContext,
rule::Rule,
utils::{
collect_possible_jest_call_node, is_type_of_jest_fn_call, JestFnKind, JestGeneralFnKind,
PossibleJestNode,
collect_possible_jest_call_node, NoRestrictedTestMethods, NoRestrictedTestMethodsConfig,
},
};

fn restricted_jest_method(x0: &str, span1: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Disallow specific `jest.` methods")
.with_help(format!("Use of `{x0:?}` is disallowed"))
.with_label(span1)
}

fn restricted_jest_method_with_message(x0: &str, span1: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Disallow specific `jest.` methods")
.with_help(format!("{x0:?}"))
.with_label(span1)
}

#[derive(Debug, Default, Clone)]
pub struct NoRestrictedJestMethods(Box<NoRestrictedJestMethodsConfig>);

#[derive(Debug, Default, Clone)]
pub struct NoRestrictedJestMethodsConfig {
restricted_jest_methods: FxHashMap<String, String>,
}

impl std::ops::Deref for NoRestrictedJestMethods {
type Target = NoRestrictedJestMethodsConfig;

fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct NoRestrictedJestMethods(Box<NoRestrictedTestMethodsConfig>);

declare_oxc_lint!(
/// ### What it does
Expand Down Expand Up @@ -67,91 +38,24 @@ declare_oxc_lint!(
style,
);

impl NoRestrictedTestMethods for NoRestrictedJestMethods {
fn restricted_test_methods(&self) -> &FxHashMap<String, String> {
&self.0.restricted_test_methods
}
}

impl Rule for NoRestrictedJestMethods {
fn from_configuration(value: serde_json::Value) -> Self {
let restricted_jest_methods = &value
.get(0)
.and_then(serde_json::Value::as_object)
.and_then(Self::compile_restricted_jest_methods)
.unwrap_or_default();

Self(Box::new(NoRestrictedJestMethodsConfig {
restricted_jest_methods: restricted_jest_methods.clone(),
}))
Self(Box::new(Self::get_configuration(&value)))
}

fn run_once(&self, ctx: &LintContext) {
for possible_jest_node in &collect_possible_jest_call_node(ctx) {
self.run(possible_jest_node, ctx);
self.run_test(possible_jest_node, ctx, true);
}
}
}

impl NoRestrictedJestMethods {
fn contains(&self, key: &str) -> bool {
self.restricted_jest_methods.contains_key(key)
}

fn get_message(&self, name: &str) -> Option<String> {
self.restricted_jest_methods.get(name).cloned()
}

fn run<'a>(&self, possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) {
let node = possible_jest_node.node;
let AstKind::CallExpression(call_expr) = node.kind() else {
return;
};

if !is_type_of_jest_fn_call(
call_expr,
possible_jest_node,
ctx,
&[JestFnKind::General(JestGeneralFnKind::Jest)],
) {
return;
}

let Some(mem_expr) = call_expr.callee.as_member_expression() else {
return;
};
let Some(property_name) = mem_expr.static_property_name() else {
return;
};
let Some((span, _)) = mem_expr.static_property_info() else {
return;
};

if self.contains(property_name) {
self.get_message(property_name).map_or_else(
|| {
ctx.diagnostic(restricted_jest_method(property_name, span));
},
|message| {
if message.trim() == "" {
ctx.diagnostic(restricted_jest_method(property_name, span));
} else {
ctx.diagnostic(restricted_jest_method_with_message(&message, span));
}
},
);
}
}

#[allow(clippy::unnecessary_wraps)]
pub fn compile_restricted_jest_methods(
matchers: &serde_json::Map<String, serde_json::Value>,
) -> Option<FxHashMap<String, String>> {
Some(
matchers
.iter()
.map(|(key, value)| {
(String::from(key), String::from(value.as_str().unwrap_or_default()))
})
.collect(),
)
}
}

#[test]
fn test() {
use crate::tester::Tester;
Expand Down Expand Up @@ -186,7 +90,5 @@ fn test() {
),
];

Tester::new(NoRestrictedJestMethods::NAME, pass, fail)
.with_jest_plugin(true)
.test_and_snapshot();
Tester::new(NoRestrictedJestMethods::NAME, pass, fail).test_and_snapshot();
}
92 changes: 92 additions & 0 deletions crates/oxc_linter/src/rules/vitest/no_restricted_vi_methods.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use oxc_macros::declare_oxc_lint;
use rustc_hash::FxHashMap;

use crate::{
context::LintContext,
rule::Rule,
utils::{
collect_possible_jest_call_node, NoRestrictedTestMethods, NoRestrictedTestMethodsConfig,
},
};

#[derive(Debug, Default, Clone)]
pub struct NoRestrictedViMethods(Box<NoRestrictedTestMethodsConfig>);

declare_oxc_lint!(
/// ### What it does
///
/// Restrict the use of specific `vi` methods.
///
/// ### Example
/// ```javascript
/// vi.useFakeTimers();
/// it('calls the callback after 1 second via advanceTimersByTime', () => {
/// // ...
///
/// vi.advanceTimersByTime(1000);
///
/// // ...
/// });
///
/// test('plays video', () => {
/// const spy = vi.spyOn(video, 'play');
///
/// // ...
/// });
///
NoRestrictedViMethods,
style,
);

impl NoRestrictedTestMethods for NoRestrictedViMethods {
fn restricted_test_methods(&self) -> &FxHashMap<String, String> {
&self.0.restricted_test_methods
}
}

impl Rule for NoRestrictedViMethods {
fn from_configuration(value: serde_json::Value) -> Self {
Self(Box::new(Self::get_configuration(&value)))
}

fn run_once(&self, ctx: &LintContext) {
for possible_vitest_node in &collect_possible_jest_call_node(ctx) {
self.run_test(possible_vitest_node, ctx, false);
}
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
("vi", None),
("vi()", None),
("vi.mock()", None),
("expect(a).rejects;", None),
("expect(a);", None),
(
"
import { vi } from 'vitest';
vi;
",
None,
), // { "parserOptions": { "sourceType": "module" } }
];

let fail = vec![
("vi.fn()", Some(serde_json::json!([{ "fn": null }]))),
("vi.mock()", Some(serde_json::json!([{ "mock": "Do not use mocks" }]))),
(
"
import { vi } from 'vitest';
vi.advanceTimersByTime();
",
Some(serde_json::json!([{ "advanceTimersByTime": null }])),
), // { "parserOptions": { "sourceType": "module" } },
(r#"vi["fn"]()"#, Some(serde_json::json!([{ "fn": null }]))),
];

Tester::new(NoRestrictedViMethods::NAME, pass, fail).test_and_snapshot();
}
32 changes: 32 additions & 0 deletions crates/oxc_linter/src/snapshots/no_restricted_vi_methods.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
source: crates/oxc_linter/src/tester.rs
---
eslint-plugin-vitest(no-restricted-vi-methods): Disallow specific `vi.` methods
╭─[no_restricted_vi_methods.tsx:1:4]
1vi.fn()
· ──
╰────
help: Use of `"fn"` is disallowed

eslint-plugin-vitest(no-restricted-vi-methods): Disallow specific `vi.` methods
╭─[no_restricted_vi_methods.tsx:1:4]
1vi.mock()
· ────
╰────
help: "Do not use mocks"

eslint-plugin-vitest(no-restricted-vi-methods): Disallow specific `vi.` methods
╭─[no_restricted_vi_methods.tsx:3:12]
2import { vi } from 'vitest';
3vi.advanceTimersByTime();
· ───────────────────
4
╰────
help: Use of `"advanceTimersByTime"` is disallowed

eslint-plugin-vitest(no-restricted-vi-methods): Disallow specific `vi.` methods
╭─[no_restricted_vi_methods.tsx:1:4]
1vi["fn"]()
· ────
╰────
help: Use of `"fn"` is disallowed
3 changes: 3 additions & 0 deletions crates/oxc_linter/src/utils/jest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub const JEST_METHOD_NAMES: phf::Set<&'static str> = phf_set![
"fit",
"it",
"jest",
"vi",
"test",
"xdescribe",
"xit",
Expand All @@ -51,6 +52,7 @@ impl JestFnKind {
match name {
"expect" => Self::Expect,
"expectTypeOf" => Self::ExpectTypeOf,
"vi" => Self::General(JestGeneralFnKind::Vitest),
"jest" => Self::General(JestGeneralFnKind::Jest),
"describe" | "fdescribe" | "xdescribe" => Self::General(JestGeneralFnKind::Describe),
"fit" | "it" | "test" | "xit" | "xtest" => Self::General(JestGeneralFnKind::Test),
Expand All @@ -75,6 +77,7 @@ pub enum JestGeneralFnKind {
Describe,
Test,
Jest,
Vitest,
}

/// <https://jestjs.io/docs/configuration#testmatch-arraystring>
Expand Down
12 changes: 9 additions & 3 deletions crates/oxc_linter/src/utils/jest/parse_jest_fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ pub fn parse_jest_fn_call<'a>(
return None;
}

if matches!(kind, JestFnKind::General(JestGeneralFnKind::Jest)) {
if matches!(kind, JestFnKind::General(JestGeneralFnKind::Jest | JestGeneralFnKind::Vitest))
{
return parse_jest_jest_fn_call(members, name, resolved.local);
}

Expand Down Expand Up @@ -244,12 +245,17 @@ fn parse_jest_jest_fn_call<'a>(
name: &'a str,
local: &'a str,
) -> Option<ParsedJestFnCall<'a>> {
if !name.to_ascii_lowercase().eq_ignore_ascii_case("jest") {
let lowercase_name = name.to_ascii_lowercase();

if !(lowercase_name == "jest" || lowercase_name == "vi") {
return None;
}

let kind =
if lowercase_name == "jest" { JestGeneralFnKind::Jest } else { JestGeneralFnKind::Vitest };

return Some(ParsedJestFnCall::GeneralJest(ParsedGeneralJestFnCall {
kind: JestFnKind::General(JestGeneralFnKind::Jest),
kind: JestFnKind::General(kind),
members,
name: Cow::Borrowed(name),
local: Cow::Borrowed(local),
Expand Down
5 changes: 3 additions & 2 deletions crates/oxc_linter/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ mod nextjs;
mod promise;
mod react;
mod react_perf;
mod test;
mod tree_shaking;
mod unicorn;
mod vitest;

pub use self::{
config::*, jest::*, jsdoc::*, nextjs::*, promise::*, react::*, react_perf::*, tree_shaking::*,
unicorn::*, vitest::*,
config::*, jest::*, jsdoc::*, nextjs::*, promise::*, react::*, react_perf::*, test::*,
tree_shaking::*, unicorn::*, vitest::*,
};

/// Check if the Jest rule is adapted to Vitest.
Expand Down
Loading

0 comments on commit f805797

Please sign in to comment.