Skip to content

Replace assert!(a==b) with assert_eq!(a,b) as part of bool_assert_comparison lint #13333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
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
176 changes: 112 additions & 64 deletions clippy_lints/src/bool_assert_comparison.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node};
use clippy_utils::is_in_const_context;
use clippy_utils::macros::{find_assert_args, find_assert_eq_args, root_macro_call_first_node, MacroCall};
use clippy_utils::sugg::Sugg;
use clippy_utils::ty::{implements_trait, is_copy};
use rustc_ast::ast::LitKind;
use rustc_ast::BinOpKind;
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind, Lit};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::ty::{self, Ty};
use rustc_session::declare_lint_pass;
use rustc_span::source_map::Spanned;
use rustc_span::symbol::Ident;
use rustc_span::Span;

declare_clippy_lint! {
/// ### What it does
Expand Down Expand Up @@ -74,74 +78,118 @@ impl<'tcx> LateLintPass<'tcx> for BoolAssertComparison {
return;
};
let macro_name = cx.tcx.item_name(macro_call.def_id);
let eq_macro = match macro_name.as_str() {
"assert_eq" | "debug_assert_eq" => true,
"assert_ne" | "debug_assert_ne" => false,
_ => return,
};
let Some((a, b, _)) = find_assert_eq_args(cx, expr, macro_call.expn) else {
return;
let macro_name = macro_name.as_str();
match macro_name {
"assert_eq" | "debug_assert_eq" => check_eq(cx, expr, &macro_call, macro_name, true),
"assert_ne" | "debug_assert_ne" => check_eq(cx, expr, &macro_call, macro_name, false),
"assert" | "debug_assert" => check(cx, expr, &macro_call, macro_name),
_ => {},
};
}
}

let a_span = a.span.source_callsite();
let b_span = b.span.source_callsite();

let (lit_span, bool_value, non_lit_expr) = match (extract_bool_lit(a), extract_bool_lit(b)) {
// assert_eq!(true/false, b)
// ^^^^^^^^^^^^
(Some(bool_value), None) => (a_span.until(b_span), bool_value, b),
// assert_eq!(a, true/false)
// ^^^^^^^^^^^^
(None, Some(bool_value)) => (b_span.with_lo(a_span.hi()), bool_value, a),
// If there are two boolean arguments, we definitely don't understand
// what's going on, so better leave things as is...
//
// Or there is simply no boolean and then we can leave things as is!
_ => return,
};
fn check_eq<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'_>,
macro_call: &MacroCall,
macro_name: &str,
eq_macro: bool,
) {
let Some((a, b, _)) = find_assert_eq_args(cx, expr, macro_call.expn) else {
return;
};

let non_lit_ty = cx.typeck_results().expr_ty(non_lit_expr);
let a_span = a.span.source_callsite();
let b_span = b.span.source_callsite();

if !is_impl_not_trait_with_bool_out(cx, non_lit_ty) {
// At this point the expression which is not a boolean
// literal does not implement Not trait with a bool output,
// so we cannot suggest to rewrite our code
return;
}
let (lit_span, bool_value, non_lit_expr) = match (extract_bool_lit(a), extract_bool_lit(b)) {
// assert_eq!(true/false, b)
// ^^^^^^^^^^^^
(Some(bool_value), None) => (a_span.until(b_span), bool_value, b),
// assert_eq!(a, true/false)
// ^^^^^^^^^^^^
(None, Some(bool_value)) => (b_span.with_lo(a_span.hi()), bool_value, a),
// If there are two boolean arguments, we definitely don't understand
// what's going on, so better leave things as is...
//
// Or there is simply no boolean and then we can leave things as is!
_ => return,
};

if !is_copy(cx, non_lit_ty) {
// Only lint with types that are `Copy` because `assert!(x)` takes
// ownership of `x` whereas `assert_eq(x, true)` does not
return;
}
let non_lit_ty = cx.typeck_results().expr_ty(non_lit_expr);

let macro_name = macro_name.as_str();
let non_eq_mac = &macro_name[..macro_name.len() - 3];
span_lint_and_then(
cx,
BOOL_ASSERT_COMPARISON,
macro_call.span,
format!("used `{macro_name}!` with a literal bool"),
|diag| {
// assert_eq!(...)
// ^^^^^^^^^
let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');

let mut suggestions = vec![(name_span, non_eq_mac.to_string()), (lit_span, String::new())];

if bool_value ^ eq_macro {
let Some(sugg) = Sugg::hir_opt(cx, non_lit_expr) else {
return;
};
suggestions.push((non_lit_expr.span, (!sugg).to_string()));
}

diag.multipart_suggestion(
format!("replace it with `{non_eq_mac}!(..)`"),
suggestions,
Applicability::MachineApplicable,
);
},
);
if !is_impl_not_trait_with_bool_out(cx, non_lit_ty) {
// At this point the expression which is not a boolean
// literal does not implement Not trait with a bool output,
// so we cannot suggest to rewrite our code
return;
}

if !is_copy(cx, non_lit_ty) {
// Only lint with types that are `Copy` because `assert!(x)` takes
// ownership of `x` whereas `assert_eq(x, true)` does not
return;
}

let non_eq_mac = &macro_name[..macro_name.len() - 3];
span_lint_and_then(
cx,
BOOL_ASSERT_COMPARISON,
macro_call.span,
format!("used `{macro_name}!` with a literal bool"),
|diag| {
// assert_eq!(...)
// ^^^^^^^^^
let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');

let mut suggestions = vec![(name_span, non_eq_mac.to_string()), (lit_span, String::new())];

if bool_value ^ eq_macro {
let Some(sugg) = Sugg::hir_opt(cx, non_lit_expr) else {
return;
};
suggestions.push((non_lit_expr.span, (!sugg).to_string()));
}

diag.multipart_suggestion(
format!("replace it with `{non_eq_mac}!(..)`"),
suggestions,
Applicability::MachineApplicable,
);
},
);
}

/// Checks for `assert!(a == b)` and `assert!(a != b)`
fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, macro_call: &MacroCall, macro_name: &str) {
if is_in_const_context(cx) {
return;
}
let Some((cond, _)) = find_assert_args(cx, expr, macro_call.expn) else {
return;
};
let ExprKind::Binary(Spanned { node, span }, lhs, rhs) = cond.kind else {
return;
};
// TODO: is `cond.span.from_expansion()` correct / needed?
if (node != BinOpKind::Eq && node != BinOpKind::Ne) || cond.span.from_expansion() {
Comment on lines +174 to +175
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// TODO: is cond.span.from_expansion() correct / needed?

It should be correct 👍

return;
}

let new_name = format!("{macro_name}_{}", if node == BinOpKind::Eq { "eq" } else { "ne" });
let msg = format!("replace it with `{new_name}!(..)`");
span_lint_and_then(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_in_const_context should be checked some time before here, and it would be good to have tests for it.

cx,
BOOL_ASSERT_COMPARISON,
macro_call.span,
format!("used `{macro_name}!` with an equality comparison"),
|diag| {
let macro_name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
// TODO: should this be `cond.span`, expanded to include preceding whitespace? If so, how?
let equality_span = Span::new(lhs.span.hi(), rhs.span.lo(), span.ctxt(), span.parent());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We know that the == actually comes from the source code, so I would probably use cond.span.with_lo(lhs.span.hi()).with_high(rhs.span.lo()). But this should result in the same span, so either way should be fine :)

let suggestions = vec![(macro_name_span, new_name), (equality_span, ", ".to_string())];

diag.multipart_suggestion(msg, suggestions, Applicability::MachineApplicable);
},
);
}
2 changes: 1 addition & 1 deletion clippy_lints/src/eta_reduction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ fn has_late_bound_to_non_late_bound_regions(from_sig: FnSig<'_>, to_sig: FnSig<'
}
}

assert!(from_sig.inputs_and_output.len() == to_sig.inputs_and_output.len());
assert_eq!(from_sig.inputs_and_output.len(), to_sig.inputs_and_output.len());
from_sig
.inputs_and_output
.iter()
Expand Down
2 changes: 1 addition & 1 deletion clippy_lints/src/functions/renamed_function_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl RenamedFnArgs {
{
let mut renamed: Vec<(Span, String)> = vec![];

debug_assert!(default_names.size_hint() == current_names.size_hint());
debug_assert_eq!(default_names.size_hint(), current_names.size_hint());
while let (Some(def_name), Some(cur_name)) = (default_names.next(), current_names.next()) {
let current_name = cur_name.name;
let default_name = def_name.name;
Expand Down
5 changes: 3 additions & 2 deletions clippy_utils/src/ty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1029,8 +1029,9 @@ fn assert_generic_args_match<'tcx>(tcx: TyCtxt<'tcx>, did: DefId, args: &[Generi
.chain(&g.own_params)
.map(|x| &x.kind);

assert!(
count == args.len(),
assert_eq!(
count,
args.len(),
"wrong number of arguments for `{did:?}`: expected `{count}`, found {}\n\
note: the expected arguments are: `[{}]`\n\
the given arguments are: `{args:#?}`",
Expand Down
4 changes: 2 additions & 2 deletions lintcheck/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ fn print_stats(old_stats: HashMap<String, usize>, new_stats: HashMap<&String, us

// remove duplicates from both hashmaps
for (k, v) in &same_in_both_hashmaps {
assert!(old_stats_deduped.remove(k) == Some(*v));
assert!(new_stats_deduped.remove(k) == Some(*v));
assert_eq!(old_stats_deduped.remove(k), Some(*v));
assert_eq!(new_stats_deduped.remove(k), Some(*v));
}

println!("\nStats:");
Expand Down
64 changes: 64 additions & 0 deletions tests/ui/assertions_on_constants.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#![allow(non_fmt_panics, clippy::needless_bool, clippy::eq_op)]

macro_rules! assert_const {
($len:expr) => {
assert!($len > 0);
debug_assert!($len < 0);
};
}
fn main() {
assert!(true);
//~^ ERROR: `assert!(true)` will be optimized out by the compiler
assert!(false);
//~^ ERROR: `assert!(false)` should probably be replaced
assert!(true, "true message");
//~^ ERROR: `assert!(true)` will be optimized out by the compiler
assert!(false, "false message");
//~^ ERROR: `assert!(false, ..)` should probably be replaced

let msg = "panic message";
assert!(false, "{}", msg.to_uppercase());
//~^ ERROR: `assert!(false, ..)` should probably be replaced

const B: bool = true;
assert!(B);
//~^ ERROR: `assert!(true)` will be optimized out by the compiler

const C: bool = false;
assert!(C);
//~^ ERROR: `assert!(false)` should probably be replaced
assert!(C, "C message");
//~^ ERROR: `assert!(false, ..)` should probably be replaced

debug_assert!(true);
//~^ ERROR: `debug_assert!(true)` will be optimized out by the compiler
// Don't lint this, since there is no better way for expressing "Only panic in debug mode".
debug_assert!(false); // #3948
assert_const!(3);
assert_const!(-1);

// Don't lint if based on `cfg!(..)`:
assert!(cfg!(feature = "hey") || cfg!(not(feature = "asdf")));

let flag: bool = cfg!(not(feature = "asdf"));
assert!(flag);

const CFG_FLAG: &bool = &cfg!(feature = "hey");
assert!(!CFG_FLAG);

const _: () = assert!(true);
//~^ ERROR: `assert!(true)` will be optimized out by the compiler
#[allow(clippy::bool_assert_comparison)]
assert_eq!(8, (7 + 1));
//~^ ERROR: `assert!(true)` will be optimized out by the compiler

// Don't lint if the value is dependent on a defined constant:
const N: usize = 1024;
const _: () = assert!(N.is_power_of_two());
}

const _: () = {
assert!(true);
//~^ ERROR: `assert!(true)` will be optimized out by the compiler
assert!(8 == (7 + 1));
};
2 changes: 1 addition & 1 deletion tests/ui/assertions_on_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fn main() {

const _: () = assert!(true);
//~^ ERROR: `assert!(true)` will be optimized out by the compiler

#[allow(clippy::bool_assert_comparison)]
assert!(8 == (7 + 1));
//~^ ERROR: `assert!(true)` will be optimized out by the compiler

Expand Down
15 changes: 14 additions & 1 deletion tests/ui/assertions_on_constants.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ LL | assert!(8 == (7 + 1));
|
= help: remove it

error: used `assert!` with an equality comparison
--> tests/ui/assertions_on_constants.rs:52:5
|
LL | assert!(8 == (7 + 1));
| ^^^^^^^^^^^^^^^^^^^^^
|
= note: `-D clippy::bool-assert-comparison` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::bool_assert_comparison)]`
help: replace it with `assert_eq!(..)`
|
LL | assert_eq!(8, (7 + 1));
| ~~~~~~~~~ ~

error: `assert!(true)` will be optimized out by the compiler
--> tests/ui/assertions_on_constants.rs:61:5
|
Expand All @@ -96,5 +109,5 @@ LL | assert!(true);
|
= help: remove it

error: aborting due to 12 previous errors
error: aborting due to 13 previous errors

12 changes: 11 additions & 1 deletion tests/ui/bool_assert_comparison.fixed
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![allow(unused, clippy::assertions_on_constants, clippy::const_is_empty)]
#![allow(unused, clippy::assertions_on_constants, clippy::const_is_empty, clippy::eq_op)]
#![warn(clippy::bool_assert_comparison)]

use std::ops::Not;
Expand Down Expand Up @@ -166,4 +166,14 @@ fn main() {
debug_assert!("".is_empty());
debug_assert!(!"requires negation".is_empty());
debug_assert!(!"requires negation".is_empty());

assert_eq!("a", "a".to_ascii_lowercase());
assert_eq!("a", "a".to_ascii_lowercase(), "a==a");
assert_ne!("A", "A".to_ascii_lowercase());
assert_ne!("A", "A".to_ascii_lowercase(), "A!=a");
const _: () = assert!(5 == 2 + 3);
}

const _: () = {
assert!(8 == (7 + 1));
};
12 changes: 11 additions & 1 deletion tests/ui/bool_assert_comparison.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![allow(unused, clippy::assertions_on_constants, clippy::const_is_empty)]
#![allow(unused, clippy::assertions_on_constants, clippy::const_is_empty, clippy::eq_op)]
#![warn(clippy::bool_assert_comparison)]

use std::ops::Not;
Expand Down Expand Up @@ -166,4 +166,14 @@ fn main() {
debug_assert_ne!("".is_empty(), false);
debug_assert_ne!("requires negation".is_empty(), true);
debug_assert_eq!("requires negation".is_empty(), false);

assert!("a" == "a".to_ascii_lowercase());
assert!("a" == "a".to_ascii_lowercase(), "a==a");
assert!("A" != "A".to_ascii_lowercase());
assert!("A" != "A".to_ascii_lowercase(), "A!=a");
const _: () = assert!(5 == 2 + 3);
}

const _: () = {
assert!(8 == (7 + 1));
};
Loading
Loading