From 812baeb2173f10b91a6c76c20d1e5bea52be10af Mon Sep 17 00:00:00 2001 From: Wenzhe Wang Date: Wed, 4 Oct 2023 22:06:12 -0500 Subject: [PATCH] feat(linter): add `eslint(jest/valid-expect)` rule (#941) --- crates/oxc_linter/src/jest_ast_util.rs | 197 +++-- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/jest/no_standalone_expect.rs | 9 +- .../oxc_linter/src/rules/jest/valid_expect.rs | 744 ++++++++++++++++++ .../src/snapshots/valid_expect.snap | 502 ++++++++++++ 5 files changed, 1395 insertions(+), 59 deletions(-) create mode 100644 crates/oxc_linter/src/rules/jest/valid_expect.rs create mode 100644 crates/oxc_linter/src/snapshots/valid_expect.snap diff --git a/crates/oxc_linter/src/jest_ast_util.rs b/crates/oxc_linter/src/jest_ast_util.rs index 374aa8aca..054e6e248 100644 --- a/crates/oxc_linter/src/jest_ast_util.rs +++ b/crates/oxc_linter/src/jest_ast_util.rs @@ -96,23 +96,17 @@ pub fn parse_jest_fn_call<'a>( // If bailed out, we're not jest function let resolved = resolve_to_jest_fn(call_expr, ctx)?; - // only the top level Call expression callee's parent is None, it's not necessary to set it to None, but - // I didn't know how to pass Expression to it. - let chain = get_node_chain(callee, None); - let all_member_expr_except_last = chain - .iter() - .rev() - .skip(1) - .all(|member| matches!(member.parent, Some(Expression::MemberExpression(_)))); - - // Ensure that we're at the "top" of the function call chain otherwise when - // parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though - // the full chain is not a valid jest function call chain - if ctx.nodes().parent_node(node.id()).is_some_and(|parent_node| { - matches!(parent_node.kind(), AstKind::CallExpression(_) | AstKind::MemberExpression(_)) - }) { - return None; - } + let params = NodeChainParams { + expr: callee, + parent: None, // TODO: not really know how to convert type of call_expr to Expression, set to `None` temporarily. + parent_kind: Some(KnownMemberExpressionParentKind::Call), + grandparent_kind: None, + }; + let chain = get_node_chain(¶ms); + let all_member_expr_except_last = + chain.iter().rev().skip(1).all(|member| { + matches!(member.parent_kind, Some(KnownMemberExpressionParentKind::Member)) + }); if let Some(last) = chain.last() { // If we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`) @@ -144,7 +138,17 @@ pub fn parse_jest_fn_call<'a>( } if matches!(kind, JestFnKind::Expect) { - return parse_jest_expect_fn_call(call_expr, members, name, head); + let options = ExpectFnCallOptions { call_expr, members, name, head, node, ctx }; + return parse_jest_expect_fn_call(options); + } + + // Ensure that we're at the "top" of the function call chain otherwise when + // parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though + // the full chain is not a valid jest function call chain + if ctx.nodes().parent_node(node.id()).is_some_and(|parent_node| { + matches!(parent_node.kind(), AstKind::CallExpression(_) | AstKind::MemberExpression(_)) + }) { + return None; } // Check every link in the chain except the last is a member expression @@ -168,21 +172,59 @@ pub fn parse_jest_fn_call<'a>( None } -fn parse_jest_expect_fn_call<'a>( +fn is_top_most_call_expr<'a, 'b>(node: &'b AstNode<'a>, ctx: &'b LintContext<'a>) -> bool { + let mut node = node; + + loop { + let Some(parent) = ctx.nodes().parent_node(node.id()) else { return true }; + + match parent.kind() { + AstKind::CallExpression(_) => return false, + AstKind::MemberExpression(_) => node = parent, + _ => { + return true; + } + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum ExpectError { + ModifierUnknown, + MatcherNotFound, + MatcherNotCalled, +} + +struct ExpectFnCallOptions<'a, 'b> { call_expr: &'a CallExpression<'a>, members: Vec>, name: &'a str, head: KnownMemberExpressionProperty<'a>, + node: &'b AstNode<'a>, + ctx: &'b LintContext<'a>, +} + +fn parse_jest_expect_fn_call<'a>( + options: ExpectFnCallOptions<'a, '_>, ) -> Option> { - // check if the `member` is being called, which means it is the matcher - let has_matcher = match &call_expr.callee { - Expression::MemberExpression(member_expr) => member_expr.static_property_name().is_some(), - _ => false, + let ExpectFnCallOptions { call_expr, members, name, head, node, ctx } = options; + let (modifiers, matcher, mut expect_error) = match find_modifiers_and_matcher(&members) { + Ok((modifier, matcher)) => (modifier, matcher, None), + Err(e) => (vec![], None, Some(e)), }; - let Some((modifiers, matcher)) = find_modifiers_and_matcher(&members, has_matcher) else { + // if the `expect` call chain is not valid, only report on the topmost node + // since all members in the chain are likely to get flagged for some reason + if expect_error.is_some() && !is_top_most_call_expr(node, ctx) { return None; - }; + } + + if matches!(expect_error, Some(ExpectError::MatcherNotFound)) { + let parent = ctx.nodes().parent_node(node.id())?; + if matches!(parent.kind(), AstKind::MemberExpression(_)) { + expect_error = Some(ExpectError::MatcherNotCalled); + } + } return Some(ParsedJestFnCall::ExpectFnCall(ParsedExpectFnCall { kind: JestFnKind::Expect, @@ -192,25 +234,25 @@ fn parse_jest_expect_fn_call<'a>( args: &call_expr.arguments, matcher_index: matcher, modifier_indices: modifiers, + expect_error, })); } +type ModifiersAndMatcherIndex = (Vec, Option); + fn find_modifiers_and_matcher( members: &[KnownMemberExpressionProperty], - has_matcher: bool, -) -> Option<(Vec, Option)> { - let mut matcher = None; +) -> Result { let mut modifiers: Vec = vec![]; - // matcher is the end of the entire "expect" call chain - if has_matcher { - matcher = Some(members.len() - 1); - } - for (index, member) in members.iter().enumerate() { - // the last member is the matcher, so we can stop here - if index == members.len() - 1 { - break; + // check if the member is being called, which means it is the matcher + // (and also the end of the entire "expect" call chain) + if matches!(member.parent_kind, Some(KnownMemberExpressionParentKind::Member)) + && matches!(member.grandparent_kind, Some(KnownMemberExpressionParentKind::Call)) + { + let matcher = Some(index); + return Ok((modifiers, matcher)); } // the first modifier can be any of the three modifiers @@ -220,30 +262,27 @@ fn find_modifiers_and_matcher( ModifierName::Resolves, ModifierName::Rejects, ]) { - return None; + return Err(ExpectError::ModifierUnknown); } } else if modifiers.len() == 1 { // the second modifier can only be "not" if !member.is_name_in_modifiers(&[ModifierName::Not]) { - return None; + return Err(ExpectError::ModifierUnknown); } // and the first modifier has to be either "resolves" or "rejects" if !members[modifiers[0]] .is_name_in_modifiers(&[ModifierName::Resolves, ModifierName::Rejects]) { - return None; + return Err(ExpectError::ModifierUnknown); } } else { - // the third modifier can only be "resolves" or "rejects" - if !member.is_name_in_modifiers(&[ModifierName::Resolves, ModifierName::Rejects]) { - return None; - } + return Err(ExpectError::ModifierUnknown); } modifiers.push(index); } - Some((modifiers, matcher)) + Err(ExpectError::MatcherNotFound) } #[derive(PartialEq, Eq)] @@ -419,6 +458,7 @@ pub struct ParsedExpectFnCall<'a> { // In `expect(1).toBe(2)`, "toBe" will be matcher // it save the matcher index from members matcher_index: Option, + pub expect_error: Option, } impl<'a> ParsedExpectFnCall<'a> { @@ -426,6 +466,9 @@ impl<'a> ParsedExpectFnCall<'a> { let matcher_index = self.matcher_index?; self.members.get(matcher_index) } + pub fn modifiers(&self) -> Vec<&KnownMemberExpressionProperty<'a>> { + self.modifier_indices.iter().filter_map(|i| self.members.get(*i)).collect::>() + } } struct ResolvedJestFn<'a> { @@ -440,9 +483,18 @@ pub enum JestFnFrom { Import, } +#[derive(Clone, Copy, Debug)] +pub enum KnownMemberExpressionParentKind { + Member, + Call, + TaggedTemplate, +} + pub struct KnownMemberExpressionProperty<'a> { pub element: MemberExpressionElement<'a>, pub parent: Option<&'a Expression<'a>>, + pub parent_kind: Option, + pub grandparent_kind: Option, pub span: Span, } @@ -547,46 +599,83 @@ pub fn get_node_name_vec<'a>(expr: &'a Expression<'a>) -> Vec> { chain } -/// Port from [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest/blob/a058f22f94774eeea7980ea2d1f24c6808bf3e2c/src/rules/utils/parseJestFnCall.ts#L36-L51) -fn get_node_chain<'a>( +struct NodeChainParams<'a> { expr: &'a Expression<'a>, parent: Option<&'a Expression<'a>>, -) -> Vec> { + parent_kind: Option, + grandparent_kind: Option, +} + +/// Port from [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest/blob/a058f22f94774eeea7980ea2d1f24c6808bf3e2c/src/rules/utils/parseJestFnCall.ts#L36-L51) +fn get_node_chain<'a>(params: &NodeChainParams<'a>) -> Vec> { let mut chain = Vec::new(); + let NodeChainParams { expr, parent, parent_kind, grandparent_kind } = params; match expr { Expression::MemberExpression(member_expr) => { - chain.extend(get_node_chain(member_expr.object(), Some(expr))); + let params = NodeChainParams { + expr: member_expr.object(), + parent: Some(expr), + parent_kind: Some(KnownMemberExpressionParentKind::Member), + grandparent_kind: *parent_kind, + }; + + chain.extend(get_node_chain(¶ms)); if let Some((span, element)) = MemberExpressionElement::from_member_expr(member_expr) { - chain.push(KnownMemberExpressionProperty { element, parent: Some(expr), span }); + chain.push(KnownMemberExpressionProperty { + element, + parent: Some(expr), + parent_kind: Some(KnownMemberExpressionParentKind::Member), + grandparent_kind: *parent_kind, + span, + }); } } Expression::Identifier(ident) => { chain.push(KnownMemberExpressionProperty { element: MemberExpressionElement::Expression(expr), - parent, + parent: *parent, + parent_kind: *parent_kind, + grandparent_kind: *grandparent_kind, span: ident.span, }); } Expression::CallExpression(call_expr) => { - let sub_chain = get_node_chain(&call_expr.callee, Some(expr)); + let params = NodeChainParams { + expr: &call_expr.callee, + parent: Some(expr), + parent_kind: Some(KnownMemberExpressionParentKind::Call), + grandparent_kind: *parent_kind, + }; + let sub_chain = get_node_chain(¶ms); chain.extend(sub_chain); } Expression::TaggedTemplateExpression(tagged_expr) => { - let sub_chain = get_node_chain(&tagged_expr.tag, Some(expr)); + let params = NodeChainParams { + expr: &tagged_expr.tag, + parent: Some(expr), + parent_kind: Some(KnownMemberExpressionParentKind::TaggedTemplate), + grandparent_kind: *parent_kind, + }; + + let sub_chain = get_node_chain(¶ms); chain.extend(sub_chain); } Expression::StringLiteral(string_literal) => { chain.push(KnownMemberExpressionProperty { element: MemberExpressionElement::Expression(expr), - parent, + parent: *parent, + parent_kind: *parent_kind, + grandparent_kind: *grandparent_kind, span: string_literal.span, }); } Expression::TemplateLiteral(template_literal) if is_pure_string(template_literal) => { chain.push(KnownMemberExpressionProperty { element: MemberExpressionElement::Expression(expr), - parent, + parent: *parent, + parent_kind: *parent_kind, + grandparent_kind: *grandparent_kind, span: template_literal.span, }); } diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index e18239442..a8aa90ac2 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -117,6 +117,7 @@ mod jest { pub mod no_standalone_expect; pub mod no_test_prefixes; pub mod valid_describe_callback; + pub mod valid_expect; } mod unicorn { @@ -210,6 +211,7 @@ oxc_macros::declare_all_lint_rules! { jest::no_test_prefixes, jest::no_focused_tests, jest::valid_describe_callback, + jest::valid_expect, jest::no_commented_out_tests, jest::expect_expect, jest::no_alias_methods, diff --git a/crates/oxc_linter/src/rules/jest/no_standalone_expect.rs b/crates/oxc_linter/src/rules/jest/no_standalone_expect.rs index 8445cfc67..59d12ec2e 100644 --- a/crates/oxc_linter/src/rules/jest/no_standalone_expect.rs +++ b/crates/oxc_linter/src/rules/jest/no_standalone_expect.rs @@ -1,4 +1,4 @@ -use oxc_ast::{ast::Expression, AstKind}; +use oxc_ast::AstKind; use oxc_diagnostics::{ miette::{self, Diagnostic}, thiserror::Error, @@ -10,7 +10,7 @@ use crate::{ context::LintContext, jest_ast_util::{ get_node_name, parse_expect_jest_fn_call, parse_general_jest_fn_call, JestFnKind, - JestGeneralFnKind, ParsedExpectFnCall, + JestGeneralFnKind, KnownMemberExpressionParentKind, ParsedExpectFnCall, }, rule::Rule, AstNode, @@ -73,10 +73,9 @@ impl Rule for NoStandaloneExpect { if members.len() == 1 && members[0].is_name_unequal("assertions") && members[0].is_name_unequal("hasAssertions") + && matches!(head.parent_kind, Some(KnownMemberExpressionParentKind::Member)) { - if let Some(Expression::MemberExpression(_)) = head.parent { - return; - } + return; } if is_correct_place_to_call_expect(node, ctx, &self.additional_test_block_functions) diff --git a/crates/oxc_linter/src/rules/jest/valid_expect.rs b/crates/oxc_linter/src/rules/jest/valid_expect.rs new file mode 100644 index 000000000..4662678b9 --- /dev/null +++ b/crates/oxc_linter/src/rules/jest/valid_expect.rs @@ -0,0 +1,744 @@ +use oxc_ast::{ + ast::{Expression, MemberExpression}, + AstKind, +}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::{Atom, GetSpan, Span}; + +use crate::{ + context::LintContext, + jest_ast_util::{parse_expect_jest_fn_call, ExpectError}, + rule::Rule, + AstNode, +}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint(jest/valid-expect): {0:?}")] +#[diagnostic(severity(warning), help("{1:?}"))] +struct ValidExpectDiagnostic(pub Atom, pub &'static str, #[label] pub Span); + +#[derive(Debug, Clone)] +pub struct ValidExpect { + async_matchers: Vec, + min_args: usize, + max_args: usize, + always_await: bool, +} + +impl Default for ValidExpect { + fn default() -> Self { + Self { + async_matchers: vec![String::from("toResolve"), String::from("toReject")], + min_args: 1, + max_args: 1, + always_await: false, + } + } +} + +declare_oxc_lint!( + /// ### What it does + /// + /// This rule triggers a warning if `expect()` is called with more than one argument + /// or without arguments. It would also issue a warning if there is nothing called + /// on `expect()`, e.g.: + /// + /// ### Example + /// ```javascript + /// expect(); + /// expect('something'); + /// expect(true).toBeDefined; + /// expect(Promise.resolve('Hi!')).resolves.toBe('Hi!'); + /// ``` + ValidExpect, + restriction +); + +impl Rule for ValidExpect { + fn from_configuration(value: serde_json::Value) -> Self { + let default_async_matchers = vec![String::from("toResolve"), String::from("toReject")]; + let config = value.get(0); + + let async_matchers = config + .and_then(|config| config.get("asyncMatchers")) + .and_then(serde_json::Value::as_array) + .map_or(default_async_matchers, |v| { + v.iter().filter_map(serde_json::Value::as_str).map(String::from).collect() + }); + let min_args = config + .and_then(|config| config.get("minArgs")) + .and_then(serde_json::Value::as_number) + .and_then(serde_json::Number::as_u64) + .map_or(1, |v| usize::try_from(v).unwrap_or(1)); + + let max_args = config + .and_then(|config| config.get("maxArgs")) + .and_then(serde_json::Value::as_number) + .and_then(serde_json::Number::as_u64) + .map_or(1, |v| usize::try_from(v).unwrap_or(1)); + + let always_await = config + .and_then(|config| config.get("alwaysAwait")) + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + + Self { async_matchers, min_args, max_args, always_await } + } + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::CallExpression(call_expr) = node.kind() else { return }; + let Some(jest_fn_call) = parse_expect_jest_fn_call(call_expr, node, ctx) else { return }; + let reporting_span = jest_fn_call.expect_error.map_or(call_expr.span, |_| { + find_top_most_member_expression(node, ctx).map_or(call_expr.span, GetSpan::span) + }); + + match jest_fn_call.expect_error { + Some(ExpectError::MatcherNotFound) => { + let (error, help) = Message::MatcherNotFound.details(); + ctx.diagnostic(ValidExpectDiagnostic(error, help, reporting_span)); + return; + } + Some(ExpectError::MatcherNotCalled) => { + let (error, help) = Message::MatcherNotCalled.details(); + ctx.diagnostic(ValidExpectDiagnostic(error, help, reporting_span)); + return; + } + Some(ExpectError::ModifierUnknown) => { + let (error, help) = Message::ModifierUnknown.details(); + ctx.diagnostic(ValidExpectDiagnostic(error, help, reporting_span)); + return; + } + None => {} + } + + let Some(Expression::CallExpression(call_expr)) = jest_fn_call.head.parent else { return }; + + if call_expr.arguments.len() < self.min_args { + let error = Atom::from(format!( + "Expect takes at most {} argument{} ", + self.min_args, + if self.min_args > 1 { "s" } else { "" } + )); + let help = "Remove the extra arguments."; + ctx.diagnostic(ValidExpectDiagnostic(error, help, call_expr.span)); + return; + } + if call_expr.arguments.len() > self.max_args { + let error = Atom::from(format!( + "Expect requires at least {} argument{} ", + self.max_args, + if self.max_args > 1 { "s" } else { "" } + )); + let help = "Add the missing arguments."; + ctx.diagnostic(ValidExpectDiagnostic(error, help, call_expr.span)); + return; + } + + let Some(matcher) = jest_fn_call.matcher() else { + return; + }; + let Some(matcher_name) = matcher.name() else { + return; + }; + + let Some(parent) = ctx.nodes().parent_node(node.id()) else { + return; + }; + + let should_be_awaited = + jest_fn_call.modifiers().iter().any(|modifier| modifier.is_name_unequal("not")) + || self.async_matchers.contains(&matcher_name.to_string()); + + if ctx.nodes().parent_node(parent.id()).is_none() || !should_be_awaited { + return; + } + + // An async assertion can be chained with `then` or `catch` statements. + // In that case our target CallExpression node is the one with + // the last `then` or `catch` statement. + let target_node = get_parent_if_thenable(node, ctx); + let Some(final_node) = find_promise_call_expression_node(node, ctx, target_node) else { + return; + }; + let Some(parent) = ctx.nodes().parent_node(final_node.id()) else { return }; + if !is_acceptable_return_node(parent, !self.always_await, ctx) { + let span; + let (error, help) = if target_node.id() == final_node.id() { + let AstKind::CallExpression(call_expr) = target_node.kind() else { return }; + span = call_expr.span; + Message::AsyncMustBeAwaited.details() + } else { + let AstKind::CallExpression(call_expr) = final_node.kind() else { return }; + span = call_expr.span; + Message::PromisesWithAsyncAssertionsMustBeAwaited.details() + }; + ctx.diagnostic(ValidExpectDiagnostic(error, help, span)); + } + } +} + +fn find_top_most_member_expression<'a, 'b>( + node: &'b AstNode<'a>, + ctx: &'b LintContext<'a>, +) -> Option<&'b MemberExpression<'a>> { + let mut top_most_member_expression = None; + let mut node = node; + + loop { + let parent = ctx.nodes().parent_node(node.id())?; + match node.kind() { + AstKind::MemberExpression(member_expr) => { + top_most_member_expression = Some(member_expr); + } + _ => { + if !matches!(parent.kind(), AstKind::MemberExpression(_)) { + break; + } + } + } + node = parent; + } + + top_most_member_expression +} + +fn is_acceptable_return_node<'a, 'b>( + node: &'b AstNode<'a>, + allow_return: bool, + ctx: &'b LintContext<'a>, +) -> bool { + let mut node = node; + loop { + if allow_return && matches!(node.kind(), AstKind::ReturnStatement(_)) { + return true; + } + + match node.kind() { + AstKind::ConditionalExpression(_) + | AstKind::Argument(_) + | AstKind::ExpressionStatement(_) + | AstKind::FunctionBody(_) => { + let Some(parent) = ctx.nodes().parent_node(node.id()) else { return false }; + node = parent; + } + AstKind::ArrowExpression(arrow_expr) => return arrow_expr.expression, + AstKind::AwaitExpression(_) => return true, + _ => return false, + } + } +} + +type ParentAndIsFirstItem<'a, 'b> = (&'b AstNode<'a>, bool); + +// Returns the parent node of the given node, ignoring some nodes, +// and return whether the first item if parent is an array. +fn get_parent_with_ignore<'a, 'b>( + node: &'b AstNode<'a>, + ctx: &'b LintContext<'a>, +) -> Option> { + let mut node = node; + loop { + let parent = ctx.nodes().parent_node(node.id())?; + if !matches!( + parent.kind(), + AstKind::Argument(_) + | AstKind::ExpressionArrayElement(_) + | AstKind::ArrayExpressionElement(_) + ) { + // we don't want to report `Promise.all([invalidExpectCall_1, invalidExpectCall_2])` twice. + // so we need mark whether the node is the first item of an array. + // if it not the first item, we ignore it in `find_promise_call_expression_node`. + if let AstKind::ArrayExpressionElement(array_expr_element) = node.kind() { + if let AstKind::ArrayExpression(array_expr) = parent.kind() { + return Some(( + parent, + array_expr.elements.first()?.span() == array_expr_element.span(), + )); + } + } + + // if parent is not an array, we assume it's the first item + return Some((parent, true)); + } + + node = parent; + } +} + +fn find_promise_call_expression_node<'a, 'b>( + node: &'b AstNode<'a>, + ctx: &'b LintContext<'a>, + default_node: &'b AstNode<'a>, +) -> Option<&'b AstNode<'a>> { + let Some((mut parent, is_first_array_item)) = get_parent_with_ignore(node, ctx) else { + return Some(default_node); + }; + if !matches!(parent.kind(), AstKind::CallExpression(_) | AstKind::ArrayExpression(_)) { + return Some(default_node); + } + let Some((grandparent, _)) = get_parent_with_ignore(parent, ctx) else { + return Some(default_node); + }; + if matches!(parent.kind(), AstKind::ArrayExpression(_)) + && matches!(grandparent.kind(), AstKind::CallExpression(_)) + { + parent = grandparent; + } + + if let AstKind::CallExpression(call_expr) = parent.kind() { + if let Expression::MemberExpression(member_expr) = &call_expr.callee { + if let Expression::Identifier(ident) = member_expr.object() { + if matches!(ident.name.as_str(), "Promise") + && ctx.nodes().parent_node(parent.id()).is_some() + { + if is_first_array_item { + return Some(parent); + } + return None; + } + } + } + } + + Some(default_node) +} + +fn get_parent_if_thenable<'a, 'b>( + node: &'b AstNode<'a>, + ctx: &'b LintContext<'a>, +) -> &'b AstNode<'a> { + let grandparent = + ctx.nodes().parent_node(node.id()).and_then(|node| ctx.nodes().parent_node(node.id())); + + let Some(grandparent) = grandparent else { return node }; + let AstKind::CallExpression(call_expr) = grandparent.kind() else { return node }; + let Expression::MemberExpression(member_expr) = &call_expr.callee else { return node }; + let Some(name) = member_expr.static_property_name() else { return node }; + + if ["then", "catch"].contains(&name) { + return get_parent_if_thenable(grandparent, ctx); + } + + node +} + +#[derive(Clone, Copy)] +enum Message { + MatcherNotFound, + MatcherNotCalled, + ModifierUnknown, + AsyncMustBeAwaited, + PromisesWithAsyncAssertionsMustBeAwaited, +} + +impl Message { + fn details(self) -> (Atom, &'static str) { + match self { + Self::MatcherNotFound => ( + Atom::from("Expect must have a corresponding matcher call."), + "Did you forget add a matcher(e.g. `toBe`, `toBeDefined`)", + ), + Self::MatcherNotCalled => ( + Atom::from("Matchers must be called to assert."), + "You need call your matcher, e.g. `expect(true).toBe(true)`.", + ), + Self::ModifierUnknown => { + (Atom::from("Expect has an unknown modifier."), "Is it a spelling mistake?") + } + Self::AsyncMustBeAwaited => { + (Atom::from("Async assertions must be awaited."), "Add `await` to your assertion.") + } + Self::PromisesWithAsyncAssertionsMustBeAwaited => ( + Atom::from("Promises which return async assertions must be awaited."), + "Add `await` to your assertion.", + ), + } + } +} + +#[ignore] +#[test] +fn test_1() { + use crate::tester::Tester; + + let pass = vec![ + ("test('valid-expect', async () => { await Promise.race([expect(Promise.reject(2)).rejects.not.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); })", None) + ]; + let fail = vec![]; + + Tester::new(ValidExpect::NAME, pass, fail).test_and_snapshot(); +} + +#[allow(clippy::too_many_lines)] +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ("expect.hasAssertions", None), + ("expect.hasAssertions()", None), + ("expect('something').toEqual('else');", None), + ("expect(true).toBeDefined();", None), + ("expect([1, 2, 3]).toEqual([1, 2, 3]);", None), + ("expect(undefined).not.toBeDefined();", None), + ("test('valid-expect', () => { return expect(Promise.resolve(2)).resolves.toBeDefined(); });", None), + ("test('valid-expect', () => { return expect(Promise.reject(2)).rejects.toBeDefined(); });", None), + ("test('valid-expect', () => { return expect(Promise.resolve(2)).resolves.not.toBeDefined(); });", None), + ("test('valid-expect', () => { return expect(Promise.resolve(2)).rejects.not.toBeDefined(); });", None), + ("test('valid-expect', function () { return expect(Promise.resolve(2)).resolves.not.toBeDefined(); });", None), + ("test('valid-expect', function () { return expect(Promise.resolve(2)).rejects.not.toBeDefined(); });", None), + ("test('valid-expect', function () { return Promise.resolve(expect(Promise.resolve(2)).resolves.not.toBeDefined()); });", None), + ("test('valid-expect', function () { return Promise.resolve(expect(Promise.resolve(2)).rejects.not.toBeDefined()); });", None), + ("test('valid-expect', () => expect(Promise.resolve(2)).resolves.toBeDefined());", Some(serde_json::json!([{ "alwaysAwait": true }]))), + ("test('valid-expect', () => expect(Promise.resolve(2)).resolves.toBeDefined());", None), + ("test('valid-expect', () => expect(Promise.reject(2)).rejects.toBeDefined());", None), + ("test('valid-expect', () => expect(Promise.reject(2)).resolves.not.toBeDefined());", None), + ("test('valid-expect', () => expect(Promise.reject(2)).rejects.not.toBeDefined());", None), + ("test('valid-expect', async () => { await expect(Promise.reject(2)).resolves.not.toBeDefined(); });", None), + ("test('valid-expect', async () => { await expect(Promise.reject(2)).rejects.not.toBeDefined(); });", None), + ("test('valid-expect', async function () { await expect(Promise.reject(2)).resolves.not.toBeDefined(); });", None), + ("test('valid-expect', async function () { await expect(Promise.reject(2)).rejects.not.toBeDefined(); });", None), + ("test('valid-expect', async () => { await Promise.resolve(expect(Promise.reject(2)).rejects.not.toBeDefined()); });", None), + ("test('valid-expect', async () => { await Promise.reject(expect(Promise.reject(2)).rejects.not.toBeDefined()); });", None), + ("test('valid-expect', async () => { await Promise.all([expect(Promise.reject(2)).rejects.not.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });", None), + ("test('valid-expect', async () => { await Promise.race([expect(Promise.reject(2)).rejects.not.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });", None), + ("test('valid-expect', async () => { await Promise.allSettled([expect(Promise.reject(2)).rejects.not.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });", None), + ("test('valid-expect', async () => { await Promise.any([expect(Promise.reject(2)).rejects.not.toBeDefined(), expect(Promise.reject(2)).rejects.not.toBeDefined()]); });", None), + ("test('valid-expect', async () => { return expect(Promise.reject(2)).resolves.not.toBeDefined().then(() => console.log('valid-case')); });", None), + ("test('valid-expect', async () => { return expect(Promise.reject(2)).resolves.not.toBeDefined().then(() => console.log('valid-case')).then(() => console.log('another valid case')); });", None), + ("test('valid-expect', async () => { return expect(Promise.reject(2)).resolves.not.toBeDefined().catch(() => console.log('valid-case')); });", None), + ("test('valid-expect', async () => { return expect(Promise.reject(2)).resolves.not.toBeDefined().then(() => console.log('valid-case')).catch(() => console.log('another valid case')); });", None), + ("test('valid-expect', async () => { return expect(Promise.reject(2)).resolves.not.toBeDefined().then(() => { expect(someMock).toHaveBeenCalledTimes(1); }); });", None), + ("test('valid-expect', async () => { await expect(Promise.reject(2)).resolves.not.toBeDefined().then(() => console.log('valid-case')); });", None), + ("test('valid-expect', async () => { await expect(Promise.reject(2)).resolves.not.toBeDefined().then(() => console.log('valid-case')).then(() => console.log('another valid case')); });", None), + ("test('valid-expect', async () => { await expect(Promise.reject(2)).resolves.not.toBeDefined().catch(() => console.log('valid-case')); });", None), + ("test('valid-expect', async () => { await expect(Promise.reject(2)).resolves.not.toBeDefined().then(() => console.log('valid-case')).catch(() => console.log('another valid case')); });", None), + ("test('valid-expect', async () => { await expect(Promise.reject(2)).resolves.not.toBeDefined().then(() => { expect(someMock).toHaveBeenCalledTimes(1); }); });", None), + ( + " + test('valid-expect', () => { + return expect(functionReturningAPromise()).resolves.toEqual(1).then(() => { + return expect(Promise.resolve(2)).resolves.toBe(1); + }); + }); + ", + None + ), + ( + " + test('valid-expect', () => { + return expect(functionReturningAPromise()).resolves.toEqual(1).then(async () => { + await expect(Promise.resolve(2)).resolves.toBe(1); + }); + }); + ", + None + ), + ( + " + test('valid-expect', () => { + return expect(functionReturningAPromise()).resolves.toEqual(1).then(() => expect(Promise.resolve(2)).resolves.toBe(1)); + }); + ", + None + ), + ( + " + expect.extend({ + toResolve(obj) { + return this.isNot + ? expect(obj).toBe(true) + : expect(obj).resolves.not.toThrow(); + } + }); + ", + None + ), + ( + " + expect.extend({ + toResolve(obj) { + return this.isNot + ? expect(obj).resolves.not.toThrow() + : expect(obj).toBe(true); + } + }); + ", + None + ), + ( + " + expect.extend({ + toResolve(obj) { + return this.isNot + ? expect(obj).toBe(true) + : anotherCondition + ? expect(obj).resolves.not.toThrow() + : expect(obj).toBe(false) + } + }); + ", + None + ), + ("expect(1).toBe(2);", Some(serde_json::json!([{ "maxArgs": 2 }]))), + ("expect(1, '1 !== 2').toBe(2);", Some(serde_json::json!([{ "maxArgs": 2 }]))), + ("expect(1, '1 !== 2').toBe(2);", Some(serde_json::json!([{ "maxArgs": 2, "minArgs": 2 }]))), + ("test('valid-expect', () => { expect(2).not.toBe(2); });", Some(serde_json::json!([{ "asyncMatchers": ["toRejectWith"] }]))), + ("test('valid-expect', () => { expect(Promise.reject(2)).toRejectWith(2); });", Some(serde_json::json!([{ "asyncMatchers": ["toResolveWith"] }]))), + ("test('valid-expect', async () => { await expect(Promise.resolve(2)).toResolve(); });", Some(serde_json::json!([{ "asyncMatchers": ["toResolveWith"] }]))), + ("test('valid-expect', async () => { expect(Promise.resolve(2)).toResolve(); });", Some(serde_json::json!([{ "asyncMatchers": ["toResolveWith"] }]))) + ]; + + let fail = vec![ + ("expect().toBe(2);", None), + ("expect().toBe(true);", None), + ("expect().toEqual('something');", None), + ("expect('something', 'else').toEqual('something');", None), + ("expect('something', 'else', 'entirely').toEqual('something');", Some(serde_json::json!([{ "maxArgs": 2 }]))), + ("expect('something', 'else', 'entirely').toEqual('something');", Some(serde_json::json!([{ "maxArgs": 2, "minArgs": 2 }]))), + ("expect('something', 'else', 'entirely').toEqual('something');", Some(serde_json::json!([{ "maxArgs": 2, "minArgs": 1 }]))), + ("expect('something').toEqual('something');", Some(serde_json::json!([{ "minArgs": 2 }]))), + ("expect('something', 'else').toEqual('something');", Some(serde_json::json!([{ "maxArgs": 1, "minArgs": 3 }]))), + ("expect('something');", None), + ("expect();", None), + ("expect(true).toBeDefined;", None), + ("expect(true).not.toBeDefined;", None), + ("expect(true).nope.toBeDefined;", None), + ("expect(true).nope.toBeDefined();", None), + ("expect(true).not.resolves.toBeDefined();", None), + ("expect(true).not.not.toBeDefined();", None), + ("expect(true).resolves.not.exactly.toBeDefined();", None), + ("expect(true).resolves;", None), + ("expect(true).rejects;", None), + ("expect(true).not;", None), + ("expect(Promise.resolve(2)).resolves.toBeDefined();", None), + ("expect(Promise.resolve(2)).rejects.toBeDefined();", None), + ("expect(Promise.resolve(2)).resolves.toBeDefined();", Some(serde_json::json!([{ "alwaysAwait": true }]))), + ( + " + expect.extend({ + toResolve(obj) { + this.isNot + ? expect(obj).toBe(true) + : expect(obj).resolves.not.toThrow(); + } + }); + ", + None + ), + ( + " + expect.extend({ + toResolve(obj) { + this.isNot + ? expect(obj).resolves.not.toThrow() + : expect(obj).toBe(true); + } + }); + ", + None + ), + ( + " + expect.extend({ + toResolve(obj) { + this.isNot + ? expect(obj).toBe(true) + : anotherCondition + ? expect(obj).resolves.not.toThrow() + : expect(obj).toBe(false) + } + }); + ", + None + ), + ("test('valid-expect', () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });", None), + ("test('valid-expect', () => { expect(Promise.resolve(2)).toResolve(); });", None), + ("test('valid-expect', () => { expect(Promise.resolve(2)).toResolve(); });", None), + ("test('valid-expect', () => { expect(Promise.resolve(2)).toReject(); });", None), + ("test('valid-expect', () => { expect(Promise.resolve(2)).not.toReject(); });", None), + ("test('valid-expect', () => { expect(Promise.resolve(2)).resolves.not.toBeDefined(); });", None), + ("test('valid-expect', () => { expect(Promise.resolve(2)).rejects.toBeDefined(); });", None), + ("test('valid-expect', () => { expect(Promise.resolve(2)).rejects.not.toBeDefined(); });", None), + ("test('valid-expect', async () => { expect(Promise.resolve(2)).resolves.toBeDefined(); });", None), + ("test('valid-expect', async () => { expect(Promise.resolve(2)).resolves.not.toBeDefined(); });", None), + ("test('valid-expect', () => { expect(Promise.reject(2)).toRejectWith(2); });", Some(serde_json::json!([{ "asyncMatchers": ["toRejectWith"] }]))), + ("test('valid-expect', () => { expect(Promise.reject(2)).rejects.toBe(2); });", Some(serde_json::json!([{ "asyncMatchers": ["toRejectWith"] }]))), + ( + " + test('valid-expect', async () => { + expect(Promise.resolve(2)).resolves.not.toBeDefined(); + expect(Promise.resolve(1)).rejects.toBeDefined(); + }); + ", + None + ), + ( + " + test('valid-expect', async () => { + await expect(Promise.resolve(2)).resolves.not.toBeDefined(); + expect(Promise.resolve(1)).rejects.toBeDefined(); + }); + ", + None + ), + ( + " + test('valid-expect', async () => { + expect(Promise.resolve(2)).resolves.not.toBeDefined(); + return expect(Promise.resolve(1)).rejects.toBeDefined(); + }); + ", + Some(serde_json::json!([{ "alwaysAwait": true }])) + ), + ( + " + test('valid-expect', async () => { + expect(Promise.resolve(2)).resolves.not.toBeDefined(); + return expect(Promise.resolve(1)).rejects.toBeDefined(); + }); + ", + None + ), + ( + " + test('valid-expect', async () => { + await expect(Promise.resolve(2)).resolves.not.toBeDefined(); + return expect(Promise.resolve(1)).rejects.toBeDefined(); + }); + ", + Some(serde_json::json!([{ "alwaysAwait": true }])) + ), + ( + " + test('valid-expect', async () => { + await expect(Promise.resolve(2)).toResolve(); + return expect(Promise.resolve(1)).toReject(); + }); + ", + Some(serde_json::json!([{ "alwaysAwait": true }])) + ), + ( + " + test('valid-expect', () => { + Promise.resolve(expect(Promise.resolve(2)).resolves.not.toBeDefined()); + }); + ", + None + ), + ( + " + test('valid-expect', () => { + Promise.reject(expect(Promise.resolve(2)).resolves.not.toBeDefined()); + }); + ", + None + ), + ( + " + test('valid-expect', () => { + Promise.x(expect(Promise.resolve(2)).resolves.not.toBeDefined()); + }); + ", + None + ), + ( + " + test('valid-expect', () => { + Promise.resolve(expect(Promise.resolve(2)).resolves.not.toBeDefined()); + }); + ", + Some(serde_json::json!([{ "alwaysAwait": true }])) + ), + ( + " + test('valid-expect', () => { + Promise.all([ + expect(Promise.resolve(2)).resolves.not.toBeDefined(), + expect(Promise.resolve(3)).resolves.not.toBeDefined(), + ]); + }); + ", + None + ), + ( + " + test('valid-expect', () => { + Promise.x([ + expect(Promise.resolve(2)).resolves.not.toBeDefined(), + expect(Promise.resolve(3)).resolves.not.toBeDefined(), + ]); + }); + ", + None + ), + ( + " + test('valid-expect', () => { + const assertions = [ + expect(Promise.resolve(2)).resolves.not.toBeDefined(), + expect(Promise.resolve(3)).resolves.not.toBeDefined(), + ] + }); + ", + None + ), + ( + " + test('valid-expect', () => { + const assertions = [ + expect(Promise.resolve(2)).toResolve(), + expect(Promise.resolve(3)).toReject(), + ] + }); + ", + None + ), + ( + " + test('valid-expect', () => { + const assertions = [ + expect(Promise.resolve(2)).not.toResolve(), + expect(Promise.resolve(3)).resolves.toReject(), + ] + }); + ", + None + ), + ("expect(Promise.resolve(2)).resolves.toBe;", None), + ( + " + test('valid-expect', () => { + return expect(functionReturningAPromise()).resolves.toEqual(1).then(() => { + expect(Promise.resolve(2)).resolves.toBe(1); + }); + }); + ", + None + ), + ( + " + test('valid-expect', () => { + return expect(functionReturningAPromise()).resolves.toEqual(1).then(async () => { + await expect(Promise.resolve(2)).resolves.toBe(1); + expect(Promise.resolve(4)).resolves.toBe(4); + }); + }); + ", + None + ), + ( + " + test('valid-expect', async () => { + await expect(Promise.resolve(1)); + }); + ", + None + ) + ]; + + Tester::new(ValidExpect::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/valid_expect.snap b/crates/oxc_linter/src/snapshots/valid_expect.snap new file mode 100644 index 000000000..b2700f9ae --- /dev/null +++ b/crates/oxc_linter/src/snapshots/valid_expect.snap @@ -0,0 +1,502 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: valid_expect +--- + ⚠ eslint(jest/valid-expect): "Expect takes at most 1 argument " + ╭─[valid_expect.tsx:1:1] + 1 │ expect().toBe(2); + · ──────── + ╰──── + help: "Remove the extra arguments." + + ⚠ eslint(jest/valid-expect): "Expect takes at most 1 argument " + ╭─[valid_expect.tsx:1:1] + 1 │ expect().toBe(true); + · ──────── + ╰──── + help: "Remove the extra arguments." + + ⚠ eslint(jest/valid-expect): "Expect takes at most 1 argument " + ╭─[valid_expect.tsx:1:1] + 1 │ expect().toEqual('something'); + · ──────── + ╰──── + help: "Remove the extra arguments." + + ⚠ eslint(jest/valid-expect): "Expect requires at least 1 argument " + ╭─[valid_expect.tsx:1:1] + 1 │ expect('something', 'else').toEqual('something'); + · ─────────────────────────── + ╰──── + help: "Add the missing arguments." + + ⚠ eslint(jest/valid-expect): "Expect requires at least 2 arguments " + ╭─[valid_expect.tsx:1:1] + 1 │ expect('something', 'else', 'entirely').toEqual('something'); + · ─────────────────────────────────────── + ╰──── + help: "Add the missing arguments." + + ⚠ eslint(jest/valid-expect): "Expect requires at least 2 arguments " + ╭─[valid_expect.tsx:1:1] + 1 │ expect('something', 'else', 'entirely').toEqual('something'); + · ─────────────────────────────────────── + ╰──── + help: "Add the missing arguments." + + ⚠ eslint(jest/valid-expect): "Expect requires at least 2 arguments " + ╭─[valid_expect.tsx:1:1] + 1 │ expect('something', 'else', 'entirely').toEqual('something'); + · ─────────────────────────────────────── + ╰──── + help: "Add the missing arguments." + + ⚠ eslint(jest/valid-expect): "Expect takes at most 2 arguments " + ╭─[valid_expect.tsx:1:1] + 1 │ expect('something').toEqual('something'); + · ─────────────────── + ╰──── + help: "Remove the extra arguments." + + ⚠ eslint(jest/valid-expect): "Expect takes at most 3 arguments " + ╭─[valid_expect.tsx:1:1] + 1 │ expect('something', 'else').toEqual('something'); + · ─────────────────────────── + ╰──── + help: "Remove the extra arguments." + + ⚠ eslint(jest/valid-expect): "Expect must have a corresponding matcher call." + ╭─[valid_expect.tsx:1:1] + 1 │ expect('something'); + · ─────────────────── + ╰──── + help: "Did you forget add a matcher(e.g. `toBe`, `toBeDefined`)" + + ⚠ eslint(jest/valid-expect): "Expect must have a corresponding matcher call." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(); + · ──────── + ╰──── + help: "Did you forget add a matcher(e.g. `toBe`, `toBeDefined`)" + + ⚠ eslint(jest/valid-expect): "Matchers must be called to assert." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).toBeDefined; + · ──────────────────────── + ╰──── + help: "You need call your matcher, e.g. `expect(true).toBe(true)`." + + ⚠ eslint(jest/valid-expect): "Matchers must be called to assert." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).not.toBeDefined; + · ──────────────────────────── + ╰──── + help: "You need call your matcher, e.g. `expect(true).toBe(true)`." + + ⚠ eslint(jest/valid-expect): "Matchers must be called to assert." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).nope.toBeDefined; + · ───────────────────────────── + ╰──── + help: "You need call your matcher, e.g. `expect(true).toBe(true)`." + + ⚠ eslint(jest/valid-expect): "Expect has an unknown modifier." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).nope.toBeDefined(); + · ─────────────────────────────── + ╰──── + help: "Is it a spelling mistake?" + + ⚠ eslint(jest/valid-expect): "Expect has an unknown modifier." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).not.resolves.toBeDefined(); + · ─────────────────────────────────────── + ╰──── + help: "Is it a spelling mistake?" + + ⚠ eslint(jest/valid-expect): "Expect has an unknown modifier." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).not.not.toBeDefined(); + · ────────────────────────────────── + ╰──── + help: "Is it a spelling mistake?" + + ⚠ eslint(jest/valid-expect): "Expect has an unknown modifier." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).resolves.not.exactly.toBeDefined(); + · ─────────────────────────────────────────────── + ╰──── + help: "Is it a spelling mistake?" + + ⚠ eslint(jest/valid-expect): "Matchers must be called to assert." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).resolves; + · ───────────────────── + ╰──── + help: "You need call your matcher, e.g. `expect(true).toBe(true)`." + + ⚠ eslint(jest/valid-expect): "Matchers must be called to assert." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).rejects; + · ──────────────────── + ╰──── + help: "You need call your matcher, e.g. `expect(true).toBe(true)`." + + ⚠ eslint(jest/valid-expect): "Matchers must be called to assert." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(true).not; + · ──────────────── + ╰──── + help: "You need call your matcher, e.g. `expect(true).toBe(true)`." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(Promise.resolve(2)).resolves.toBeDefined(); + · ───────────────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(Promise.resolve(2)).rejects.toBeDefined(); + · ──────────────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(Promise.resolve(2)).resolves.toBeDefined(); + · ───────────────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:5:1] + 5 │ ? expect(obj).toBe(true) + 6 │ : expect(obj).resolves.not.toThrow(); + · ────────────────────────────────── + 7 │ } + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:4:1] + 4 │ this.isNot + 5 │ ? expect(obj).resolves.not.toThrow() + · ────────────────────────────────── + 6 │ : expect(obj).toBe(true); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:6:1] + 6 │ : anotherCondition + 7 │ ? expect(obj).resolves.not.toThrow() + · ────────────────────────────────── + 8 │ : expect(obj).toBe(false) + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.resolve(2)).resolves.toBeDefined(); }); + · ───────────────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.resolve(2)).toResolve(); }); + · ────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.resolve(2)).toResolve(); }); + · ────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.resolve(2)).toReject(); }); + · ───────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.resolve(2)).not.toReject(); }); + · ───────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.resolve(2)).resolves.not.toBeDefined(); }); + · ───────────────────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.resolve(2)).rejects.toBeDefined(); }); + · ──────────────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.resolve(2)).rejects.not.toBeDefined(); }); + · ──────────────────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', async () => { expect(Promise.resolve(2)).resolves.toBeDefined(); }); + · ───────────────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', async () => { expect(Promise.resolve(2)).resolves.not.toBeDefined(); }); + · ───────────────────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.reject(2)).toRejectWith(2); }); + · ───────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:1:1] + 1 │ test('valid-expect', () => { expect(Promise.reject(2)).rejects.toBe(2); }); + · ───────────────────────────────────────── + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', async () => { + 3 │ expect(Promise.resolve(2)).resolves.not.toBeDefined(); + · ───────────────────────────────────────────────────── + 4 │ expect(Promise.resolve(1)).rejects.toBeDefined(); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:3:1] + 3 │ expect(Promise.resolve(2)).resolves.not.toBeDefined(); + 4 │ expect(Promise.resolve(1)).rejects.toBeDefined(); + · ──────────────────────────────────────────────── + 5 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:3:1] + 3 │ await expect(Promise.resolve(2)).resolves.not.toBeDefined(); + 4 │ expect(Promise.resolve(1)).rejects.toBeDefined(); + · ──────────────────────────────────────────────── + 5 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', async () => { + 3 │ expect(Promise.resolve(2)).resolves.not.toBeDefined(); + · ───────────────────────────────────────────────────── + 4 │ return expect(Promise.resolve(1)).rejects.toBeDefined(); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:3:1] + 3 │ expect(Promise.resolve(2)).resolves.not.toBeDefined(); + 4 │ return expect(Promise.resolve(1)).rejects.toBeDefined(); + · ──────────────────────────────────────────────── + 5 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', async () => { + 3 │ expect(Promise.resolve(2)).resolves.not.toBeDefined(); + · ───────────────────────────────────────────────────── + 4 │ return expect(Promise.resolve(1)).rejects.toBeDefined(); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:3:1] + 3 │ await expect(Promise.resolve(2)).resolves.not.toBeDefined(); + 4 │ return expect(Promise.resolve(1)).rejects.toBeDefined(); + · ──────────────────────────────────────────────── + 5 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:3:1] + 3 │ await expect(Promise.resolve(2)).toResolve(); + 4 │ return expect(Promise.resolve(1)).toReject(); + · ───────────────────────────────────── + 5 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Promises which return async assertions must be awaited." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', () => { + 3 │ Promise.resolve(expect(Promise.resolve(2)).resolves.not.toBeDefined()); + · ────────────────────────────────────────────────────────────────────── + 4 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Promises which return async assertions must be awaited." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', () => { + 3 │ Promise.reject(expect(Promise.resolve(2)).resolves.not.toBeDefined()); + · ───────────────────────────────────────────────────────────────────── + 4 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Promises which return async assertions must be awaited." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', () => { + 3 │ Promise.x(expect(Promise.resolve(2)).resolves.not.toBeDefined()); + · ──────────────────────────────────────────────────────────────── + 4 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Promises which return async assertions must be awaited." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', () => { + 3 │ Promise.resolve(expect(Promise.resolve(2)).resolves.not.toBeDefined()); + · ────────────────────────────────────────────────────────────────────── + 4 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Promises which return async assertions must be awaited." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', () => { + 3 │ ╭─▶ Promise.all([ + 4 │ │ expect(Promise.resolve(2)).resolves.not.toBeDefined(), + 5 │ │ expect(Promise.resolve(3)).resolves.not.toBeDefined(), + 6 │ ╰─▶ ]); + 7 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Promises which return async assertions must be awaited." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', () => { + 3 │ ╭─▶ Promise.x([ + 4 │ │ expect(Promise.resolve(2)).resolves.not.toBeDefined(), + 5 │ │ expect(Promise.resolve(3)).resolves.not.toBeDefined(), + 6 │ ╰─▶ ]); + 7 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:3:1] + 3 │ const assertions = [ + 4 │ expect(Promise.resolve(2)).resolves.not.toBeDefined(), + · ───────────────────────────────────────────────────── + 5 │ expect(Promise.resolve(3)).resolves.not.toBeDefined(), + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:4:1] + 4 │ expect(Promise.resolve(2)).resolves.not.toBeDefined(), + 5 │ expect(Promise.resolve(3)).resolves.not.toBeDefined(), + · ───────────────────────────────────────────────────── + 6 │ ] + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:3:1] + 3 │ const assertions = [ + 4 │ expect(Promise.resolve(2)).toResolve(), + · ────────────────────────────────────── + 5 │ expect(Promise.resolve(3)).toReject(), + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:4:1] + 4 │ expect(Promise.resolve(2)).toResolve(), + 5 │ expect(Promise.resolve(3)).toReject(), + · ───────────────────────────────────── + 6 │ ] + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:3:1] + 3 │ const assertions = [ + 4 │ expect(Promise.resolve(2)).not.toResolve(), + · ────────────────────────────────────────── + 5 │ expect(Promise.resolve(3)).resolves.toReject(), + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:4:1] + 4 │ expect(Promise.resolve(2)).not.toResolve(), + 5 │ expect(Promise.resolve(3)).resolves.toReject(), + · ────────────────────────────────────────────── + 6 │ ] + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Matchers must be called to assert." + ╭─[valid_expect.tsx:1:1] + 1 │ expect(Promise.resolve(2)).resolves.toBe; + · ──────────────────────────────────────── + ╰──── + help: "You need call your matcher, e.g. `expect(true).toBe(true)`." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:3:1] + 3 │ return expect(functionReturningAPromise()).resolves.toEqual(1).then(() => { + 4 │ expect(Promise.resolve(2)).resolves.toBe(1); + · ─────────────────────────────────────────── + 5 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Async assertions must be awaited." + ╭─[valid_expect.tsx:4:1] + 4 │ await expect(Promise.resolve(2)).resolves.toBe(1); + 5 │ expect(Promise.resolve(4)).resolves.toBe(4); + · ─────────────────────────────────────────── + 6 │ }); + ╰──── + help: "Add `await` to your assertion." + + ⚠ eslint(jest/valid-expect): "Expect must have a corresponding matcher call." + ╭─[valid_expect.tsx:2:1] + 2 │ test('valid-expect', async () => { + 3 │ await expect(Promise.resolve(1)); + · ────────────────────────── + 4 │ }); + ╰──── + help: "Did you forget add a matcher(e.g. `toBe`, `toBeDefined`)" + +