From 743b17c5e1b8dd3ab7be35425781b80792f5898b Mon Sep 17 00:00:00 2001 From: keita Date: Sun, 11 Feb 2024 16:44:34 +0900 Subject: [PATCH 1/3] feat(linter): eslint-plugin-jest require-to-throw-message --- crates/oxc_linter/src/rules.rs | 2 + .../rules/jest/require_to_throw_message.rs | 183 ++++++++++++++++++ .../snapshots/require_to_throw_message.snap | 37 ++++ 3 files changed, 222 insertions(+) create mode 100644 crates/oxc_linter/src/rules/jest/require_to_throw_message.rs create mode 100644 crates/oxc_linter/src/snapshots/require_to_throw_message.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index c86c3bc991626..b99a288042baf 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -143,6 +143,7 @@ mod jest { pub mod prefer_called_with; pub mod prefer_equality_matcher; pub mod prefer_todo; + pub mod require_to_throw_message; pub mod valid_describe_callback; pub mod valid_expect; pub mod valid_title; @@ -431,6 +432,7 @@ oxc_macros::declare_all_lint_rules! { jest::prefer_called_with, jest::prefer_equality_matcher, jest::prefer_todo, + jest::require_to_throw_message, jest::valid_describe_callback, jest::valid_expect, jest::valid_title, diff --git a/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs b/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs new file mode 100644 index 0000000000000..73a5bf71bccd6 --- /dev/null +++ b/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs @@ -0,0 +1,183 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{ + context::LintContext, + rule::Rule, + utils::{collect_possible_jest_call_node, parse_expect_jest_fn_call, PossibleJestNode}, +}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`.")] +#[diagnostic(severity(warning), help("Add an error message to {0:?}()"))] +struct RequireToThrowMessageDiagnostic(pub String, #[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct RequireToThrowMessage; + +declare_oxc_lint!( + /// ### What it does + /// This rule triggers a warning if `toThrow()` or `toThrowError()` is used without an error message. + /// + /// ### Example + /// ```javascript + /// // invalid + /// test('all the things', async () => { + /// expect(() => a()).toThrow(); + /// expect(() => a()).toThrowError(); + /// await expect(a()).rejects.toThrow(); + /// await expect(a()).rejects.toThrowError(); + /// }); + /// + /// // valid + /// test('all the things', async () => { + /// expect(() => a()).toThrow('a'); + /// expect(() => a()).toThrowError('a'); + /// await expect(a()).rejects.toThrow('a'); + /// await expect(a()).rejects.toThrowError('a'); + /// }); + /// ``` + /// + RequireToThrowMessage, + correctness +); + +impl Rule for RequireToThrowMessage { + fn run_once(&self, ctx: &LintContext) { + for possible_jest_node in &collect_possible_jest_call_node(ctx) { + Self::run(possible_jest_node, ctx); + } + } +} + +impl RequireToThrowMessage { + pub fn run<'a>(possible_jest_node: &PossibleJestNode<'a, '_>, ctx: &LintContext<'a>) { + let node = possible_jest_node.node; + let AstKind::CallExpression(call_expr) = node.kind() else { + return; + }; + + let Some(jest_fn_call) = parse_expect_jest_fn_call(call_expr, possible_jest_node, ctx) + else { + return; + }; + + let Some(matcher) = jest_fn_call.matcher() else { + return; + }; + + let Some(matcher_name) = matcher.name() else { + return; + }; + + let has_not = jest_fn_call.modifiers().iter().any(|modifier| modifier.is_name_equal("not")); + + if jest_fn_call.args.len() == 0 + && (matcher_name == "toThrow" || matcher_name == "toThrowError") + && !has_not + { + ctx.diagnostic(RequireToThrowMessageDiagnostic(matcher_name.to_string(), matcher.span)); + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + // String + ("expect(() => { throw new Error('a'); }).toThrow('a');", None), + ("expect(() => { throw new Error('a'); }).toThrowError('a');", None), + ( + " + test('string', async () => { + const throwErrorAsync = async () => { throw new Error('a') }; + await expect(throwErrorAsync()).rejects.toThrow('a'); + await expect(throwErrorAsync()).rejects.toThrowError('a'); + }) + ", + None, + ), + // Template literal + ("const a = 'a'; expect(() => { throw new Error('a'); }).toThrow(`${a}`);", None), + ("const a = 'a'; expect(() => { throw new Error('a'); }).toThrowError(`${a}`);", None), + ( + " + test('Template literal', async () => { + const a = 'a'; + const throwErrorAsync = async () => { throw new Error('a') }; + await expect(throwErrorAsync()).rejects.toThrow(`${a}`); + await expect(throwErrorAsync()).rejects.toThrowError(`${a}`); + }) + ", + None, + ), + // Regex + ("expect(() => { throw new Error('a'); }).toThrow(/^a$/);", None), + ("expect(() => { throw new Error('a'); }).toThrowError(/^a$/);", None), + ( + " + test('Regex', async () => { + const throwErrorAsync = async () => { throw new Error('a') }; + await expect(throwErrorAsync()).rejects.toThrow(/^a$/); + await expect(throwErrorAsync()).rejects.toThrowError(/^a$/); + }) + ", + None, + ), + // Function + ("expect(() => { throw new Error('a'); }).toThrow((() => { return 'a'; })());", None), + ("expect(() => { throw new Error('a'); }).toThrowError((() => { return 'a'; })());", None), + ( + " + test('Function', async () => { + const throwErrorAsync = async () => { throw new Error('a') }; + const fn = () => { return 'a'; }; + await expect(throwErrorAsync()).rejects.toThrow(fn()); + await expect(throwErrorAsync()).rejects.toThrowError(fn()); + }) + ", + None, + ), + // Allow no message for `not`. + ("expect(() => { throw new Error('a'); }).not.toThrow();", None), + ("expect(() => { throw new Error('a'); }).not.toThrowError();", None), + ( + " + test('Allow no message for `not`', async () => { + const throwErrorAsync = async () => { throw new Error('a') }; + await expect(throwErrorAsync()).resolves.not.toThrow(); + await expect(throwErrorAsync()).resolves.not.toThrowError(); + }) + ", + None, + ), + ("expect(a);", None), + ]; + + let fail = vec![ + // Empty toThrow + ("expect(() => { throw new Error('a'); }).toThrow();", None), + // Empty toThrowError + ("expect(() => { throw new Error('a'); }).toThrowError();", None), + // Empty rejects.toThrow / rejects.toThrowError + ( + " + test('empty rejects.toThrow', async () => { + const throwErrorAsync = async () => { throw new Error('a') }; + await expect(throwErrorAsync()).rejects.toThrow(); + await expect(throwErrorAsync()).rejects.toThrowError(); + }) + ", + None, + ), + ]; + + Tester::new(RequireToThrowMessage::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/require_to_throw_message.snap b/crates/oxc_linter/src/snapshots/require_to_throw_message.snap new file mode 100644 index 0000000000000..4045edfe11d3c --- /dev/null +++ b/crates/oxc_linter/src/snapshots/require_to_throw_message.snap @@ -0,0 +1,37 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: require_to_throw_message +--- + + ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. + ╭─[require_to_throw_message.tsx:1:41] + 1 │ expect(() => { throw new Error('a'); }).toThrow(); + · ─────── + ╰──── + help: Add an error message to "toThrow"() + + ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. + ╭─[require_to_throw_message.tsx:1:41] + 1 │ expect(() => { throw new Error('a'); }).toThrowError(); + · ──────────── + ╰──── + help: Add an error message to "toThrowError"() + + ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. + ╭─[require_to_throw_message.tsx:4:61] + 3 │ const throwErrorAsync = async () => { throw new Error('a') }; + 4 │ await expect(throwErrorAsync()).rejects.toThrow(); + · ─────── + 5 │ await expect(throwErrorAsync()).rejects.toThrowError(); + ╰──── + help: Add an error message to "toThrow"() + + ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. + ╭─[require_to_throw_message.tsx:5:61] + 4 │ await expect(throwErrorAsync()).rejects.toThrow(); + 5 │ await expect(throwErrorAsync()).rejects.toThrowError(); + · ──────────── + 6 │ }) + ╰──── + help: Add an error message to "toThrowError"() + From b08bf7ac961146f0d41e08c42fa8121fb249a5fc Mon Sep 17 00:00:00 2001 From: keita hino Date: Sun, 11 Feb 2024 20:55:45 +0900 Subject: [PATCH 2/3] fix: Remove unnecessary parentheses --- .../oxc_linter/src/rules/jest/require_to_throw_message.rs | 2 +- .../src/snapshots/require_to_throw_message.snap | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs b/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs index 73a5bf71bccd6..61e97d92c2ff0 100644 --- a/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs +++ b/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs @@ -14,7 +14,7 @@ use crate::{ #[derive(Debug, Error, Diagnostic)] #[error("eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`.")] -#[diagnostic(severity(warning), help("Add an error message to {0:?}()"))] +#[diagnostic(severity(warning), help("Add an error message to {0:?}"))] struct RequireToThrowMessageDiagnostic(pub String, #[label] pub Span); #[derive(Debug, Default, Clone)] diff --git a/crates/oxc_linter/src/snapshots/require_to_throw_message.snap b/crates/oxc_linter/src/snapshots/require_to_throw_message.snap index 4045edfe11d3c..e38443d87dad2 100644 --- a/crates/oxc_linter/src/snapshots/require_to_throw_message.snap +++ b/crates/oxc_linter/src/snapshots/require_to_throw_message.snap @@ -8,14 +8,14 @@ expression: require_to_throw_message 1 │ expect(() => { throw new Error('a'); }).toThrow(); · ─────── ╰──── - help: Add an error message to "toThrow"() + help: Add an error message to "toThrow" ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. ╭─[require_to_throw_message.tsx:1:41] 1 │ expect(() => { throw new Error('a'); }).toThrowError(); · ──────────── ╰──── - help: Add an error message to "toThrowError"() + help: Add an error message to "toThrowError" ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. ╭─[require_to_throw_message.tsx:4:61] @@ -24,7 +24,7 @@ expression: require_to_throw_message · ─────── 5 │ await expect(throwErrorAsync()).rejects.toThrowError(); ╰──── - help: Add an error message to "toThrow"() + help: Add an error message to "toThrow" ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. ╭─[require_to_throw_message.tsx:5:61] @@ -33,5 +33,5 @@ expression: require_to_throw_message · ──────────── 6 │ }) ╰──── - help: Add an error message to "toThrowError"() + help: Add an error message to "toThrowError" From c926297d16117121a3240f61a2b16edbf83cc51c Mon Sep 17 00:00:00 2001 From: keita hino Date: Sun, 11 Feb 2024 20:58:20 +0900 Subject: [PATCH 3/3] fix: Correct matcher related to error --- .../oxc_linter/src/rules/jest/require_to_throw_message.rs | 2 +- .../src/snapshots/require_to_throw_message.snap | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs b/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs index 61e97d92c2ff0..96d183ecdcef2 100644 --- a/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs +++ b/crates/oxc_linter/src/rules/jest/require_to_throw_message.rs @@ -13,7 +13,7 @@ use crate::{ }; #[derive(Debug, Error, Diagnostic)] -#[error("eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`.")] +#[error("eslint-plugin-jest(require-to-throw-message): Require a message for {0:?}.")] #[diagnostic(severity(warning), help("Add an error message to {0:?}"))] struct RequireToThrowMessageDiagnostic(pub String, #[label] pub Span); diff --git a/crates/oxc_linter/src/snapshots/require_to_throw_message.snap b/crates/oxc_linter/src/snapshots/require_to_throw_message.snap index e38443d87dad2..9eadcfd9f0368 100644 --- a/crates/oxc_linter/src/snapshots/require_to_throw_message.snap +++ b/crates/oxc_linter/src/snapshots/require_to_throw_message.snap @@ -3,21 +3,21 @@ source: crates/oxc_linter/src/tester.rs expression: require_to_throw_message --- - ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. + ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for "toThrow". ╭─[require_to_throw_message.tsx:1:41] 1 │ expect(() => { throw new Error('a'); }).toThrow(); · ─────── ╰──── help: Add an error message to "toThrow" - ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. + ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for "toThrowError". ╭─[require_to_throw_message.tsx:1:41] 1 │ expect(() => { throw new Error('a'); }).toThrowError(); · ──────────── ╰──── help: Add an error message to "toThrowError" - ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. + ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for "toThrow". ╭─[require_to_throw_message.tsx:4:61] 3 │ const throwErrorAsync = async () => { throw new Error('a') }; 4 │ await expect(throwErrorAsync()).rejects.toThrow(); @@ -26,7 +26,7 @@ expression: require_to_throw_message ╰──── help: Add an error message to "toThrow" - ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for `toThrow()`. + ⚠ eslint-plugin-jest(require-to-throw-message): Require a message for "toThrowError". ╭─[require_to_throw_message.tsx:5:61] 4 │ await expect(throwErrorAsync()).rejects.toThrow(); 5 │ await expect(throwErrorAsync()).rejects.toThrowError();