diff --git a/CHANGELOG.md b/CHANGELOG.md index 45bfb65b2e67..8155ee36af9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6433,6 +6433,7 @@ Released 2018-09-13 [`as_pointer_underscore`]: https://rust-lang.github.io/rust-clippy/master/index.html#as_pointer_underscore [`as_ptr_cast_mut`]: https://rust-lang.github.io/rust-clippy/master/index.html#as_ptr_cast_mut [`as_underscore`]: https://rust-lang.github.io/rust-clippy/master/index.html#as_underscore +[`assert_multiple`]: https://rust-lang.github.io/rust-clippy/master/index.html#assert_multiple [`assertions_on_constants`]: https://rust-lang.github.io/rust-clippy/master/index.html#assertions_on_constants [`assertions_on_result_states`]: https://rust-lang.github.io/rust-clippy/master/index.html#assertions_on_result_states [`assign_op_pattern`]: https://rust-lang.github.io/rust-clippy/master/index.html#assign_op_pattern diff --git a/clippy_lints/src/assert_multiple.rs b/clippy_lints/src/assert_multiple.rs new file mode 100644 index 000000000000..031dde67a751 --- /dev/null +++ b/clippy_lints/src/assert_multiple.rs @@ -0,0 +1,161 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::macros::{find_assert_args, root_macro_call_first_node}; +use clippy_utils::source::snippet; +use rustc_errors::Applicability; +use rustc_hir::intravisit::Visitor; +use rustc_hir::{BinOpKind, Expr, ExprKind, UnOp}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::declare_lint_pass; +use rustc_span::sym; + +declare_clippy_lint! { + /// ### What it does + /// Looks for cases of assert!(a==b && c==d) and suggests alternative + /// + /// ### Why is this bad? + /// It's hard to identify which test is failing + /// ### Example + /// ```no_run + /// let a = true; + /// let b = true; + /// let c = true; + /// let d = true; + /// assert!(a==b && c!=d /* && ... */) + /// ``` + /// Use instead: + /// ```no_run + /// let a = true; + /// let b = true; + /// let c = true; + /// let d = true; + /// assert_eq!(a, b); + /// assert_ne!(c,d); + /// /* ... */ + /// ``` + #[clippy::version = "1.95.0"] + pub ASSERT_MULTIPLE, + nursery, + "Splitting an assert using '&&' into separate asserts makes it clearer which is failing." +} + +declare_lint_pass!(AssertMultiple => [ASSERT_MULTIPLE]); + +// This visiior is a convenient place to hold the session context, as well as the collection of +// replacement strings and the type of assert to use. + +struct AssertVisitor<'tcx, 'v> { + cx: &'v LateContext<'tcx>, + suggests: Vec, + assert_string: &'v str, +} + +impl<'tcx> Visitor<'tcx> for AssertVisitor<'tcx, '_> { + fn visit_expr(&mut self, e: &'tcx Expr<'_>) { + match e.kind { + ExprKind::Binary(op, lhs, rhs) => match op.node { + BinOpKind::And => { + // For And, turn each of the rhs and lhs expressions into their own assert. + rustc_hir::intravisit::walk_expr(self, lhs); + rustc_hir::intravisit::walk_expr(self, rhs); + }, + BinOpKind::Or => { + // For Or, we cannot break the expression up. + let tmpstr = format!("{}!{};", self.assert_string, snippet(self.cx, e.span, "..")); + self.suggests.push(tmpstr); + }, + _ => { + if let Some(x) = assert_from_op(self, op.node, *lhs, *rhs) { + // handle most of the binary operators here. + self.suggests.push(x); + } + }, + }, + ExprKind::Call(_call, _args) => { + // split function calls into their own assert. + let tmptxt = snippet(self.cx, e.span, ".."); + let tmpassrt = format!("{}!({});", self.assert_string, tmptxt); + self.suggests.push(tmpassrt); + }, + + ExprKind::MethodCall(_path, expr, _args, span) => { + // split method calls into their own assert as well. + let calltext = snippet(self.cx, expr.span, ".."); + let tmptxt = format!("{}.{};", &*calltext, snippet(self.cx, span, "..")); + self.suggests.push(tmptxt); + }, + ExprKind::Path(qpath) => { + // this is a statndalone boolean variable, not an expression. + let name = snippet(self.cx, qpath.span(), "_"); + let tmptxt = format!("{}!({name});", self.assert_string); + self.suggests.push(tmptxt); + }, + ExprKind::Unary(UnOp::Not, expr) => { + // A Not operator, just output the + let exptext = snippet(self.cx, expr.span, "_"); + let tmptxt = format!("{}!(!{exptext});", self.assert_string); + self.suggests.push(tmptxt); + }, + + _ => {}, + } + } +} + +impl<'tcx> LateLintPass<'tcx> for AssertMultiple { + fn check_expr(&mut self, cx: &LateContext<'tcx>, e: &'tcx Expr<'tcx>) { + if let Some(macro_call) = root_macro_call_first_node(cx, e) + && let assert_string = match cx.tcx.get_diagnostic_name(macro_call.def_id) { + Some(sym::assert_macro) => "assert", + Some(sym::debug_assert_macro) => "debug_assert", + _ => return, + } + && let Some((condition, _)) = find_assert_args(cx, e, macro_call.expn) + && matches!(condition.kind, ExprKind::Binary(binop,_lhs,_rhs) if binop.node == BinOpKind::And) + { + // We only get here on assert/debug_assert macro calls whose arguments have an And expression + // on the top of the tree. + let mut am_visitor = AssertVisitor { + cx, + suggests: Vec::new(), + assert_string, + }; + rustc_hir::intravisit::walk_expr(&mut am_visitor, condition); + + if !am_visitor.suggests.is_empty() { + let suggs = am_visitor.suggests.join("\n ").trim_end_matches(';').to_string(); + span_lint_and_sugg( + cx, + ASSERT_MULTIPLE, + macro_call.span, + "multiple asserts combined into one", + "consider writing", + suggs, + Applicability::MaybeIncorrect, + ); + } + } + } +} + +// This function separates out a binary operation into a separate assert, using ..._eq or ..._ne if +// applicable. +fn assert_from_op( + visitor: &mut AssertVisitor<'_, '_>, + node: BinOpKind, + lhs: Expr<'_>, + rhs: Expr<'_>, +) -> Option { + let cx = visitor.cx; + let lhs_name = snippet(cx, lhs.span, "_"); + let rhs_name = snippet(cx, rhs.span, "_"); + match node { + BinOpKind::Eq => Some(format!("{}_eq!({lhs_name}, {rhs_name});", visitor.assert_string)), + BinOpKind::Ne => Some(format!("{}_ne!({lhs_name}, {rhs_name});", visitor.assert_string)), + BinOpKind::Ge | BinOpKind::Gt | BinOpKind::Le | BinOpKind::Lt => Some(format!( + "{}!({lhs_name} {} {rhs_name})", + visitor.assert_string, + node.as_str() + )), + _ => None, + } +} diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 441b907eaf2f..19899f59f893 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -11,6 +11,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::as_conversions::AS_CONVERSIONS_INFO, crate::asm_syntax::INLINE_ASM_X86_ATT_SYNTAX_INFO, crate::asm_syntax::INLINE_ASM_X86_INTEL_SYNTAX_INFO, + crate::assert_multiple::ASSERT_MULTIPLE_INFO, crate::assertions_on_constants::ASSERTIONS_ON_CONSTANTS_INFO, crate::assertions_on_result_states::ASSERTIONS_ON_RESULT_STATES_INFO, crate::assigning_clones::ASSIGNING_CLONES_INFO, diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index 950fcf0db0a4..94e6f57ee4d9 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -67,6 +67,7 @@ mod arbitrary_source_item_ordering; mod arc_with_non_send_sync; mod as_conversions; mod asm_syntax; +mod assert_multiple; mod assertions_on_constants; mod assertions_on_result_states; mod assigning_clones; @@ -866,6 +867,7 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co Box::new(move |_| Box::new(manual_take::ManualTake::new(conf))), Box::new(|_| Box::new(manual_checked_ops::ManualCheckedOps)), Box::new(move |_| Box::new(manual_pop_if::ManualPopIf::new(conf))), + Box::new(|_| Box::new(assert_multiple::AssertMultiple)), // add late passes here, used by `cargo dev new_lint` ]; store.late_passes.extend(late_lints); diff --git a/clippy_utils/src/consts.rs b/clippy_utils/src/consts.rs index 19278708a36a..51db6b6b64c7 100644 --- a/clippy_utils/src/consts.rs +++ b/clippy_utils/src/consts.rs @@ -841,7 +841,8 @@ impl<'tcx> ConstEvalCtxt<'tcx> { && ty.span.ctxt() == self.ctxt.get() && ty_name.ident.span.ctxt() == self.ctxt.get() && matches!(ty_path.res, Res::PrimTy(_)) - && let Some((DefKind::AssocConst { .. }, did)) = self.typeck.type_dependent_def(id) => + && let Some((DefKind::AssocConst { .. }, did)) = self.typeck.type_dependent_def(id) + && self.tcx.inherent_impl_of_assoc(did).is_some() => { did }, diff --git a/tests/ui/assert_multiple.fixed b/tests/ui/assert_multiple.fixed new file mode 100644 index 000000000000..a329913ef751 --- /dev/null +++ b/tests/ui/assert_multiple.fixed @@ -0,0 +1,74 @@ +#![warn(clippy::assert_multiple)] +#![allow(unused)] +use std::thread::sleep; +use std::time::{Duration, SystemTime}; + +fn myfunc1(_a: u32, _b: String) -> bool { + let time1 = SystemTime::now(); + let one_sec = Duration::from_secs(1); + sleep(one_sec); + + time1.elapsed().unwrap() >= one_sec +} + +struct MyStruct {} + +impl MyStruct { + fn myfunc(&self, a: u32, b: String) -> bool { + myfunc1(a, b) + } +} + +fn main() { + #[derive(PartialEq, Debug)] + enum Vals { + Owned, + Borrowed, + Other, + } + let o = Vals::Owned; + let b = Vals::Borrowed; + let other = Vals::Other; + let time = SystemTime::now(); + let one_sec = Duration::from_secs(1); + sleep(one_sec); + let elp = time.elapsed().unwrap(); + let is_bool = true; + + assert!(myfunc1(1, "foo".to_string())); + assert_eq!(b, Vals::Borrowed); + //~^ assert_multiple + let ms = MyStruct {}; + ms.myfunc(1, "foo".to_string()); + assert!(myfunc1(2, "bar".to_string())); + //~^ assert_multiple + + assert_eq!(o, Vals::Owned); + assert_eq!(b, Vals::Other); + //~^ assert_multiple + + debug_assert_eq!(o, b); + debug_assert_eq!(other, Vals::Other); + //~^ assert_multiple + + assert_eq!(o, b); + assert!(o == Vals::Owned || b == Vals::Other); + //~^ assert_multiple + assert_eq!(o, b); + assert!(is_bool); + //~^ assert_multiple + assert!(!is_bool); + assert_eq!(o, b); + //~^ assert_multiple + assert_eq!(o, b); + assert!(!is_bool); + //~^ assert_multiple + assert_eq!(o, b); + assert!(!(is_bool && o == Vals::Owned)); + //~^ assert_multiple + + // Next ones we cannot split. + assert!((o == b && o == Vals::Owned) || b == Vals::Other); + assert!(o == Vals::Owned || b == Vals::Other); + debug_assert!(o == b); +} diff --git a/tests/ui/assert_multiple.rs b/tests/ui/assert_multiple.rs new file mode 100644 index 000000000000..45e15c67998d --- /dev/null +++ b/tests/ui/assert_multiple.rs @@ -0,0 +1,65 @@ +#![warn(clippy::assert_multiple)] +#![allow(unused)] +use std::thread::sleep; +use std::time::{Duration, SystemTime}; + +fn myfunc1(_a: u32, _b: String) -> bool { + let time1 = SystemTime::now(); + let one_sec = Duration::from_secs(1); + sleep(one_sec); + + time1.elapsed().unwrap() >= one_sec +} + +struct MyStruct {} + +impl MyStruct { + fn myfunc(&self, a: u32, b: String) -> bool { + myfunc1(a, b) + } +} + +fn main() { + #[derive(PartialEq, Debug)] + enum Vals { + Owned, + Borrowed, + Other, + } + let o = Vals::Owned; + let b = Vals::Borrowed; + let other = Vals::Other; + let time = SystemTime::now(); + let one_sec = Duration::from_secs(1); + sleep(one_sec); + let elp = time.elapsed().unwrap(); + let is_bool = true; + + assert!(myfunc1(1, "foo".to_string()) && b == Vals::Borrowed); + //~^ assert_multiple + let ms = MyStruct {}; + assert!(ms.myfunc(1, "foo".to_string()) && myfunc1(2, "bar".to_string())); + //~^ assert_multiple + + assert!(o == Vals::Owned && b == Vals::Other); + //~^ assert_multiple + + debug_assert!(o == b && other == Vals::Other); + //~^ assert_multiple + + assert!(o == b && (o == Vals::Owned || b == Vals::Other)); + //~^ assert_multiple + assert!(o == b && is_bool); + //~^ assert_multiple + assert!(!is_bool && o == b); + //~^ assert_multiple + assert!(o == b && !is_bool); + //~^ assert_multiple + assert!(o == b && !(is_bool && o == Vals::Owned)); + //~^ assert_multiple + + // Next ones we cannot split. + assert!((o == b && o == Vals::Owned) || b == Vals::Other); + assert!(o == Vals::Owned || b == Vals::Other); + debug_assert!(o == b); +} diff --git a/tests/ui/assert_multiple.stderr b/tests/ui/assert_multiple.stderr new file mode 100644 index 000000000000..578e87598bd7 --- /dev/null +++ b/tests/ui/assert_multiple.stderr @@ -0,0 +1,112 @@ +error: multiple asserts combined into one + --> tests/ui/assert_multiple.rs:38:5 + | +LL | assert!(myfunc1(1, "foo".to_string()) && b == Vals::Borrowed); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `-D clippy::assert-multiple` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::assert_multiple)]` +help: consider writing + | +LL ~ assert!(myfunc1(1, "foo".to_string())); +LL ~ assert_eq!(b, Vals::Borrowed); + | + +error: multiple asserts combined into one + --> tests/ui/assert_multiple.rs:41:5 + | +LL | assert!(ms.myfunc(1, "foo".to_string()) && myfunc1(2, "bar".to_string())); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider writing + | +LL ~ ms.myfunc(1, "foo".to_string()); +LL ~ assert!(myfunc1(2, "bar".to_string())); + | + +error: multiple asserts combined into one + --> tests/ui/assert_multiple.rs:44:5 + | +LL | assert!(o == Vals::Owned && b == Vals::Other); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider writing + | +LL ~ assert_eq!(o, Vals::Owned); +LL ~ assert_eq!(b, Vals::Other); + | + +error: multiple asserts combined into one + --> tests/ui/assert_multiple.rs:47:5 + | +LL | debug_assert!(o == b && other == Vals::Other); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider writing + | +LL ~ debug_assert_eq!(o, b); +LL ~ debug_assert_eq!(other, Vals::Other); + | + +error: multiple asserts combined into one + --> tests/ui/assert_multiple.rs:50:5 + | +LL | assert!(o == b && (o == Vals::Owned || b == Vals::Other)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider writing + | +LL ~ assert_eq!(o, b); +LL ~ assert!(o == Vals::Owned || b == Vals::Other); + | + +error: multiple asserts combined into one + --> tests/ui/assert_multiple.rs:52:5 + | +LL | assert!(o == b && is_bool); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider writing + | +LL ~ assert_eq!(o, b); +LL ~ assert!(is_bool); + | + +error: multiple asserts combined into one + --> tests/ui/assert_multiple.rs:54:5 + | +LL | assert!(!is_bool && o == b); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider writing + | +LL ~ assert!(!is_bool); +LL ~ assert_eq!(o, b); + | + +error: multiple asserts combined into one + --> tests/ui/assert_multiple.rs:56:5 + | +LL | assert!(o == b && !is_bool); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider writing + | +LL ~ assert_eq!(o, b); +LL ~ assert!(!is_bool); + | + +error: multiple asserts combined into one + --> tests/ui/assert_multiple.rs:58:5 + | +LL | assert!(o == b && !(is_bool && o == Vals::Owned)); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: consider writing + | +LL ~ assert_eq!(o, b); +LL ~ assert!(!(is_bool && o == Vals::Owned)); + | + +error: aborting due to 9 previous errors + diff --git a/tests/ui/match_same_arms.fixed b/tests/ui/match_same_arms.fixed index 8b16fd3193f5..b0ab833c7835 100644 --- a/tests/ui/match_same_arms.fixed +++ b/tests/ui/match_same_arms.fixed @@ -156,3 +156,19 @@ fn issue16678() { }, } } + +fn issue16698() { + trait Foo { + const X: u8; + } + impl Foo for u8 { + const X: u8 = 2; + } + impl Foo for i8 { + const X: u8 = 2; + } + match true { + false => u8::X, + true => i8::X, + }; +} diff --git a/tests/ui/match_same_arms.rs b/tests/ui/match_same_arms.rs index 3b2d585c579d..9c7899122afb 100644 --- a/tests/ui/match_same_arms.rs +++ b/tests/ui/match_same_arms.rs @@ -165,3 +165,19 @@ fn issue16678() { }, } } + +fn issue16698() { + trait Foo { + const X: u8; + } + impl Foo for u8 { + const X: u8 = 2; + } + impl Foo for i8 { + const X: u8 = 2; + } + match true { + false => u8::X, + true => i8::X, + }; +}