Skip to content

Commit

Permalink
feat(linter/eslint-plugin-vitest): implement require-local-test-conte…
Browse files Browse the repository at this point in the history
…xt-for-concurrent-snapshots (#4951)

Related to #4656

---------

Co-authored-by: Wang Wenzhe <[email protected]>
  • Loading branch information
shulaoda and mysteryven authored Aug 20, 2024
1 parent 8d3f61b commit ed9a1c4
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 0 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 @@ -457,6 +457,7 @@ mod vitest {
pub mod no_import_node_test;
pub mod prefer_to_be_falsy;
pub mod prefer_to_be_truthy;
pub mod require_local_test_context_for_concurrent_snapshots;
}

oxc_macros::declare_all_lint_rules! {
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::require_local_test_context_for_concurrent_snapshots,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use oxc_ast::{ast::MemberExpression, AstKind};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};

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

#[inline]
fn is_snapshot_method(property_name: &str) -> bool {
matches!(
property_name,
"toMatchSnapshot"
| "toMatchInlineSnapshot"
| "toMatchFileSnapshot"
| "toThrowErrorMatchingSnapshot"
| "toThrowErrorMatchingInlineSnapshot"
)
}

#[inline]
fn is_test_or_describe_node(member_expr: &MemberExpression) -> bool {
if let Some(id) = member_expr.object().get_identifier_reference() {
if matches!(
JestFnKind::from(id.name.as_str()),
JestFnKind::General(JestGeneralFnKind::Describe | JestGeneralFnKind::Test)
) {
if let Some(property_name) = member_expr.static_property_name() {
return property_name == "concurrent";
}
}
}
false
}

fn require_local_test_context_for_concurrent_snapshots_diagnostic(span0: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("Require local Test Context for concurrent snapshot tests")
.with_help("Use local Test Context instead")
.with_label(span0)
}

#[derive(Debug, Default, Clone)]
pub struct RequireLocalTestContextForConcurrentSnapshots;

declare_oxc_lint!(
/// ### What it does
/// The rule is intended to ensure that concurrent snapshot tests are executed within a properly configured local test context.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```js
/// test.concurrent('myLogic', () => {
/// expect(true).toMatchSnapshot();
/// })
///
/// describe.concurrent('something', () => {
/// test('myLogic', () => {
/// expect(true).toMatchInlineSnapshot();
/// })
/// })
///
/// ```
///
/// Examples of **correct** code for this rule:
/// ```js
/// test.concurrent('myLogic', ({ expect }) => {
/// expect(true).toMatchSnapshot();
/// })
///
/// test.concurrent('myLogic', (context) => {
/// context.expect(true).toMatchSnapshot();
/// })
/// ```
RequireLocalTestContextForConcurrentSnapshots,
correctness
);

impl Rule for RequireLocalTestContextForConcurrentSnapshots {
fn run_once(&self, ctx: &LintContext) {
for possible_jest_node in &collect_possible_jest_call_node(ctx) {
Self::run(possible_jest_node, ctx);
}
}
}

impl RequireLocalTestContextForConcurrentSnapshots {
fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) {
let node = possible_jest_node.node;
if let AstKind::CallExpression(call_expr) = node.kind() {
if !is_type_of_jest_fn_call(call_expr, possible_jest_node, ctx, &[JestFnKind::Expect]) {
return;
}

let Some(member_expr) = call_expr.callee.as_member_expression() else { return };

let Some(property_name) = member_expr.static_property_name() else { return };

if !is_snapshot_method(property_name) {
return;
}

let test_or_describe_node_found =
ctx.nodes().iter_parents(possible_jest_node.node.id()).any(|node| {
if let AstKind::CallExpression(ancestor_call_expr) = node.kind() {
if let Some(ancestor_member_expr) =
ancestor_call_expr.callee.as_member_expression()
{
return is_test_or_describe_node(ancestor_member_expr);
}
}

false
});

if test_or_describe_node_found {
ctx.diagnostic(require_local_test_context_for_concurrent_snapshots_diagnostic(
call_expr.span(),
));
}
}
}
}

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

let pass = vec![
r#"it("something", () => { expect(true).toBe(true) })"#,
r#"it.concurrent("something", () => { expect(true).toBe(true) })"#,
r#"it("something", () => { expect(1).toMatchSnapshot() })"#,
r#"it.concurrent("something", ({ expect }) => { expect(1).toMatchSnapshot() })"#,
r#"it.concurrent("something", ({ expect }) => { expect(1).toMatchInlineSnapshot("1") })"#,
r#"describe.concurrent("something", () => { it("something", () => { expect(true).toBe(true) }) })"#,
r#"describe.concurrent("something", () => { it("something", ({ expect }) => { expect(1).toMatchSnapshot() }) })"#,
r#"describe.concurrent("something", () => { it("something", ({ expect }) => { expect(1).toMatchInlineSnapshot() }) })"#,
r#"describe("something", () => { it("something", (context) => { context.expect(1).toMatchInlineSnapshot() }) })"#,
r#"describe("something", () => { it("something", (context) => { expect(1).toMatchInlineSnapshot() }) })"#,
r#"it.concurrent("something", (context) => { context.expect(1).toMatchSnapshot() })"#,
];

let fail = vec![
r#"it.concurrent("should fail", () => { expect(true).toMatchSnapshot() })"#,
r#"it.concurrent("should fail", () => { expect(true).toMatchInlineSnapshot("true") })"#,
r#"describe.concurrent("failing", () => { it("should fail", () => { expect(true).toMatchSnapshot() }) })"#,
r#"describe.concurrent("failing", () => { it("should fail", () => { expect(true).toMatchInlineSnapshot("true") }) })"#,
r#"it.concurrent("something", (context) => { expect(true).toMatchSnapshot() })"#,
r#"it.concurrent("something", () => {
expect(true).toMatchSnapshot();
expect(true).toMatchSnapshot();
})"#,
r#"it.concurrent("something", () => {
expect(true).toBe(true);
expect(true).toMatchSnapshot();
})"#,
r#"it.concurrent("should fail", () => { expect(true).toMatchFileSnapshot("./test/basic.output.html") })"#,
r#"it.concurrent("should fail", () => { expect(foo()).toThrowErrorMatchingSnapshot() })"#,
r#"it.concurrent("should fail", () => { expect(foo()).toThrowErrorMatchingInlineSnapshot("bar") })"#,
];

Tester::new(RequireLocalTestContextForConcurrentSnapshots::NAME, pass, fail)
.test_and_snapshot();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
source: crates/oxc_linter/src/tester.rs
---
eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:1:38]
1it.concurrent("should fail", () => { expect(true).toMatchSnapshot() })
· ──────────────────────────────
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:1:38]
1it.concurrent("should fail", () => { expect(true).toMatchInlineSnapshot("true") })
· ──────────────────────────────────────────
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:1:66]
1describe.concurrent("failing", () => { it("should fail", () => { expect(true).toMatchSnapshot() }) })
· ──────────────────────────────
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:1:66]
1describe.concurrent("failing", () => { it("should fail", () => { expect(true).toMatchInlineSnapshot("true") }) })
· ──────────────────────────────────────────
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:1:43]
1it.concurrent("something", (context) => { expect(true).toMatchSnapshot() })
· ──────────────────────────────
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:2:21]
1it.concurrent("something", () => {
2expect(true).toMatchSnapshot();
· ──────────────────────────────
3
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:4:21]
3
4expect(true).toMatchSnapshot();
· ──────────────────────────────
5 │ })
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:4:21]
3
4expect(true).toMatchSnapshot();
· ──────────────────────────────
5 │ })
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:1:38]
1it.concurrent("should fail", () => { expect(true).toMatchFileSnapshot("./test/basic.output.html") })
· ────────────────────────────────────────────────────────────
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:1:38]
1it.concurrent("should fail", () => { expect(foo()).toThrowErrorMatchingSnapshot() })
· ────────────────────────────────────────────
╰────
help: Use local Test Context instead

eslint-plugin-vitest(require-local-test-context-for-concurrent-snapshots): Require local Test Context for concurrent snapshot tests
╭─[require_local_test_context_for_concurrent_snapshots.tsx:1:38]
1it.concurrent("should fail", () => { expect(foo()).toThrowErrorMatchingInlineSnapshot("bar") })
· ───────────────────────────────────────────────────────
╰────
help: Use local Test Context instead

0 comments on commit ed9a1c4

Please sign in to comment.