From acc57295d590fa5447712efc24053003e2c76ce8 Mon Sep 17 00:00:00 2001 From: cinchen Date: Thu, 18 Jul 2024 10:13:45 +0800 Subject: [PATCH] feat(linter): eslint-plugin-vitest/expect-expect (#4299) support [eslint-plugin-vitest/expect-expect](https://github.com/veritem/eslint-plugin-vitest/blob/main/src/rules/expect-expect.ts) --------- Co-authored-by: wenzhe --- .../src/rules/jest/expect_expect.rs | 285 +++++++++++++++++- .../src/snapshots/expect_expect.snap | 94 ++++++ crates/oxc_linter/src/utils/mod.rs | 3 +- 3 files changed, 371 insertions(+), 11 deletions(-) diff --git a/crates/oxc_linter/src/rules/jest/expect_expect.rs b/crates/oxc_linter/src/rules/jest/expect_expect.rs index afd59c3e7b68a..f791f7a610039 100644 --- a/crates/oxc_linter/src/rules/jest/expect_expect.rs +++ b/crates/oxc_linter/src/rules/jest/expect_expect.rs @@ -13,13 +13,13 @@ use crate::{ context::LintContext, rule::Rule, utils::{ - collect_possible_jest_call_node, get_node_name, is_type_of_jest_fn_call, JestFnKind, - JestGeneralFnKind, PossibleJestNode, + collect_possible_jest_call_node, get_node_name, get_test_plugin_name, + is_type_of_jest_fn_call, JestFnKind, JestGeneralFnKind, PossibleJestNode, TestPluginName, }, }; -fn expect_expect_diagnostic(span0: Span) -> OxcDiagnostic { - OxcDiagnostic::warn("eslint-plugin-jest(expect-expect): Test has no assertions") +fn expect_expect_diagnostic(x0: TestPluginName, span0: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("{x0}(expect-expect): Test has no assertions")) .with_help("Add assertion(s) in this Test") .with_label(span0) } @@ -67,6 +67,17 @@ declare_oxc_lint!( /// }); /// test('should assert something', () => {}); /// ``` + /// + /// This rule is compatible with [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest/blob/main/docs/rules/expect-expect.md), + /// to use it, add the following configuration to your `.eslintrc.json`: + /// + /// ```json + /// { + /// "rules": { + /// "vitest/expect-expect": "error" + /// } + /// } + /// ``` ExpectExpect, correctness ); @@ -111,6 +122,7 @@ fn run<'a>( ) { let node = possible_jest_node.node; if let AstKind::CallExpression(call_expr) = node.kind() { + let plugin_name = get_test_plugin_name(ctx); let name = get_node_name(&call_expr.callee); if is_type_of_jest_fn_call( call_expr, @@ -126,6 +138,9 @@ fn run<'a>( if property_name == "todo" { return; } + if property_name == "skip" && plugin_name.eq(&TestPluginName::Vitest) { + return; + } } // Record visited nodes to avoid infinite loop. @@ -135,7 +150,7 @@ fn run<'a>( check_arguments(call_expr, &rule.assert_function_names, &mut visited, ctx); if !has_assert_function { - ctx.diagnostic(expect_expect_diagnostic(call_expr.callee.span())); + ctx.diagnostic(expect_expect_diagnostic(plugin_name, call_expr.callee.span())); } } } @@ -271,7 +286,7 @@ fn convert_pattern(pattern: &str) -> String { fn test() { use crate::tester::Tester; - let pass = vec![ + let mut pass = vec![ ("it.todo('will test something eventually')", None), ("test.todo('will test something eventually')", None), ("['x']();", None), @@ -330,8 +345,8 @@ fn test() { ( " theoretically('the number {input} is correctly translated to string', theories, theory => { - const output = NumberToLongString(theory.input); - expect(output).toBe(theory.expected); + const output = NumberToLongString(theory.input); + expect(output).toBe(theory.expected); }) ", Some(serde_json::json!([{ "additionalTestBlockFunctions": ["theoretically"] }])), @@ -394,7 +409,7 @@ fn test() { ), ]; - let fail = vec![ + let mut fail = vec![ ("it(\"should fail\", () => {});", None), ("it(\"should fail\", myTest); function myTest() {}", None), ("test(\"should fail\", () => {});", None), @@ -486,5 +501,255 @@ fn test() { ), ]; - Tester::new(ExpectExpect::NAME, pass, fail).with_jest_plugin(true).test_and_snapshot(); + let pass_vitest = vec![ + ( + " + import { test } from 'vitest'; + test.skip(\"skipped test\", () => {}) + ", + None, + ), + ("it.todo(\"will test something eventually\")", None), + ("test.todo(\"will test something eventually\")", None), + ("['x']();", None), + ("it(\"should pass\", () => expect(true).toBeDefined())", None), + ("test(\"should pass\", () => expect(true).toBeDefined())", None), + ("it(\"should pass\", () => somePromise().then(() => expect(true).toBeDefined()))", None), + ("it(\"should pass\", myTest); function myTest() { expect(true).toBeDefined() }", None), + ( + " + test('should pass', () => { + expect(true).toBeDefined(); + foo(true).toBe(true); + }); + ", + Some(serde_json::json!([{ "assertFunctionNames": ["expect", "foo"] }])) + ), + ( + " + import { bench } from 'vitest' + + bench('normal sorting', () => { + const x = [1, 5, 4, 2, 3] + x.sort((a, b) => { + return a - b + }) + }, { time: 1000 }) + ", + None, + ), + ( + "it(\"should return undefined\", () => expectSaga(mySaga).returns());", + Some(serde_json::json!([{ "assertFunctionNames": ["expectSaga"] }])), + ), + ( + "test('verifies expect method call', () => expect$(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["expect\\$"] }])), + ), + ( + "test('verifies expect method call', () => new Foo().expect(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["Foo.expect"] }])), + ), + ( + " + test('verifies deep expect method call', () => { + tester.foo().expect(123); + }); + ", + Some(serde_json::json!([{ "assertFunctionNames": ["tester.foo.expect"] }])), + ), + ( + " + test('verifies chained expect method call', () => { + tester + .foo() + .bar() + .expect(456); + }); + ", + Some(serde_json::json!([{ "assertFunctionNames": ["tester.foo.bar.expect"] }])), + ), + ( + " + test(\"verifies the function call\", () => { + td.verify(someFunctionCall()) + }) + ", + Some(serde_json::json!([{ "assertFunctionNames": ["td.verify"] }])), + ), + ( + "it(\"should pass\", () => expect(true).toBeDefined())", + Some(serde_json::json!([{ + "assertFunctionNames": "undefined", + "additionalTestBlockFunctions": "undefined", + }])), + ), + ( + " + theoretically('the number {input} is correctly translated to string', theories, theory => { + const output = NumberToLongString(theory.input); + expect(output).toBe(theory.expected); + }) + ", + Some(serde_json::json!([{ "additionalTestBlockFunctions": ["theoretically"] }])), + ), + ( + "test('should pass *', () => expect404ToBeLoaded());", + Some(serde_json::json!([{ "assertFunctionNames": ["expect*"] }])), + ), + ( + "test('should pass *', () => expect.toHaveStatus404());", + Some(serde_json::json!([{ "assertFunctionNames": ["expect.**"] }])), + ), + ( + "test('should pass', () => tester.foo().expect(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["tester.*.expect"] }])), + ), + ( + "test('should pass **', () => tester.foo().expect(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["**"] }])), + ), + ( + "test('should pass *', () => tester.foo().expect(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["*"] }])), + ), + ( + "test('should pass', () => tester.foo().expect(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["tester.**"] }])), + ), + ( + "test('should pass', () => tester.foo().expect(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["tester.*"] }])), + ), + ( + "test('should pass', () => tester.foo().bar().expectIt(456));", + Some(serde_json::json!([{ "assertFunctionNames": ["tester.**.expect*"] }])), + ), + ( + "test('should pass', () => request.get().foo().expect(456));", + Some(serde_json::json!([{ "assertFunctionNames": ["request.**.expect"] }])), + ), + ( + "test('should pass', () => request.get().foo().expect(456));", + Some(serde_json::json!([{ "assertFunctionNames": ["request.**.e*e*t"] }])), + ), + ( + " + import { test } from 'vitest'; + + test('should pass', () => { + expect(true).toBeDefined(); + foo(true).toBe(true); + }); + ", + Some(serde_json::json!([{ "assertFunctionNames": ["expect", "foo"] }])), + ), + ( + " + import { test as checkThat } from 'vitest'; + + checkThat('this passes', () => { + expect(true).toBeDefined(); + foo(true).toBe(true); + }); + ", + Some(serde_json::json!([{ "assertFunctionNames": ["expect", "foo"] }])), + ), + ( + " + const { test } = require('vitest'); + + test('verifies chained expect method call', () => { + tester + .foo() + .bar() + .expect(456); + }); + ", + Some(serde_json::json!([{ "assertFunctionNames": ["tester.foo.bar.expect"] }])), + ), + ( + " + it(\"should pass with 'typecheck' enabled\", () => { + expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() + }); + ", + None + ), + ]; + + let fail_vitest = vec![ + ("it(\"should fail\", () => {});", None), + ("it(\"should fail\", myTest); function myTest() {}", None), + ("test(\"should fail\", () => {});", None), + ( + "afterEach(() => {});", + Some(serde_json::json!([{ "additionalTestBlockFunctions": ["afterEach"] }])), + ), + // Todo: currently it's not support + // ( + // " + // theoretically('the number {input} is correctly translated to string', theories, theory => { + // const output = NumberToLongString(theory.input); + // }) + // ", + // Some(serde_json::json!([{ "additionalTestBlockFunctions": ["theoretically"] }])), + // ), + ("it(\"should fail\", () => { somePromise.then(() => {}); });", None), + ( + "test(\"should fail\", () => { foo(true).toBe(true); })", + Some(serde_json::json!([{ "assertFunctionNames": ["expect"] }])), + ), + ( + "it(\"should also fail\",() => expectSaga(mySaga).returns());", + Some(serde_json::json!([{ "assertFunctionNames": ["expect"] }])), + ), + ( + "test('should fail', () => request.get().foo().expect(456));", + Some(serde_json::json!([{ "assertFunctionNames": ["request.*.expect"] }])), + ), + ( + "test('should fail', () => request.get().foo().bar().expect(456));", + Some(serde_json::json!([{ "assertFunctionNames": ["request.foo**.expect"] }])), + ), + ( + "test('should fail', () => tester.request(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["request.*"] }])), + ), + ( + "test('should fail', () => request(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["request.*"] }])), + ), + ( + "test('should fail', () => request(123));", + Some(serde_json::json!([{ "assertFunctionNames": ["request.**"] }])), + ), + ( + " + import { test as checkThat } from 'vitest'; + + checkThat('this passes', () => { + // ... + }); + ", + Some(serde_json::json!([{ "assertFunctionNames": ["expect", "foo"] }])), + ), + // Todo: currently we couldn't support ignore the typecheck option. + // ( + // " + // it(\"should fail without 'typecheck' enabled\", () => { + // expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() + // }); + // ", + // None, + // ), + ]; + + pass.extend(pass_vitest); + fail.extend(fail_vitest); + + Tester::new(ExpectExpect::NAME, pass, fail) + .with_jest_plugin(true) + .with_vitest_plugin(true) + .test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/expect_expect.snap b/crates/oxc_linter/src/snapshots/expect_expect.snap index 9d86e2b1f567d..b3032412ce5d5 100644 --- a/crates/oxc_linter/src/snapshots/expect_expect.snap +++ b/crates/oxc_linter/src/snapshots/expect_expect.snap @@ -1,5 +1,6 @@ --- source: crates/oxc_linter/src/tester.rs +assertion_line: 216 --- ⚠ eslint-plugin-jest(expect-expect): Test has no assertions ╭─[expect_expect.tsx:1:1] @@ -127,3 +128,96 @@ source: crates/oxc_linter/src/tester.rs 3 │ t.test("emitter with newListener that removes handler", function(t) { ╰──── help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ it("should fail", () => {}); + · ── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ it("should fail", myTest); function myTest() {} + · ── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ test("should fail", () => {}); + · ──── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ afterEach(() => {}); + · ───────── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ it("should fail", () => { somePromise.then(() => {}); }); + · ── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ test("should fail", () => { foo(true).toBe(true); }) + · ──── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ it("should also fail",() => expectSaga(mySaga).returns()); + · ── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ test('should fail', () => request.get().foo().expect(456)); + · ──── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ test('should fail', () => request.get().foo().bar().expect(456)); + · ──── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ test('should fail', () => tester.request(123)); + · ──── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ test('should fail', () => request(123)); + · ──── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-jest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:1:1] + 1 │ test('should fail', () => request(123)); + · ──── + ╰──── + help: Add assertion(s) in this Test + + ⚠ eslint-plugin-vitest(expect-expect): Test has no assertions + ╭─[expect_expect.tsx:4:17] + 3 │ + 4 │ checkThat('this passes', () => { + · ───────── + 5 │ // ... + ╰──── + help: Add assertion(s) in this Test diff --git a/crates/oxc_linter/src/utils/mod.rs b/crates/oxc_linter/src/utils/mod.rs index 431c742d3287e..c62c3de126fc1 100644 --- a/crates/oxc_linter/src/utils/mod.rs +++ b/crates/oxc_linter/src/utils/mod.rs @@ -18,6 +18,7 @@ pub use self::{ pub fn is_jest_rule_adapted_to_vitest(rule_name: &str) -> bool { let jest_rules: &[&str] = &[ "consistent-test-it", + "expect-expect", "no-alias-methods", "no-disabled-tests", "no-focused-tests", @@ -30,7 +31,7 @@ pub fn is_jest_rule_adapted_to_vitest(rule_name: &str) -> bool { jest_rules.contains(&rule_name) } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum TestPluginName { Jest, Vitest,