Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
161 changes: 161 additions & 0 deletions clippy_lints/src/assert_multiple.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
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<String> {
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,
}
}
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion clippy_utils/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
74 changes: 74 additions & 0 deletions tests/ui/assert_multiple.fixed
Original file line number Diff line number Diff line change
@@ -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);
}
65 changes: 65 additions & 0 deletions tests/ui/assert_multiple.rs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading