Skip to content

Commit 1cc3977

Browse files
committed
Replace assert!(a==b) with assert_eq!
Expand lint `bool_assert_comparison` to catch `assert!(a == b)` and `assert!(a != b)` (or their debug versions), and suggest to replace them with the corresponding `assert_eq!(a, b)`.
1 parent a81f1c8 commit 1cc3977

File tree

8 files changed

+170
-71
lines changed

8 files changed

+170
-71
lines changed
Lines changed: 108 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2-
use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node};
2+
use clippy_utils::macros::{find_assert_args, find_assert_eq_args, root_macro_call_first_node, MacroCall};
33
use clippy_utils::sugg::Sugg;
44
use clippy_utils::ty::{implements_trait, is_copy};
55
use rustc_ast::ast::LitKind;
6+
use rustc_ast::BinOpKind;
67
use rustc_errors::Applicability;
78
use rustc_hir::{Expr, ExprKind, Lit};
89
use rustc_lint::{LateContext, LateLintPass, LintContext};
910
use rustc_middle::ty::{self, Ty};
1011
use rustc_session::declare_lint_pass;
12+
use rustc_span::source_map::Spanned;
1113
use rustc_span::symbol::Ident;
14+
use rustc_span::Span;
1215

1316
declare_clippy_lint! {
1417
/// ### What it does
@@ -74,74 +77,115 @@ impl<'tcx> LateLintPass<'tcx> for BoolAssertComparison {
7477
return;
7578
};
7679
let macro_name = cx.tcx.item_name(macro_call.def_id);
77-
let eq_macro = match macro_name.as_str() {
78-
"assert_eq" | "debug_assert_eq" => true,
79-
"assert_ne" | "debug_assert_ne" => false,
80-
_ => return,
81-
};
82-
let Some((a, b, _)) = find_assert_eq_args(cx, expr, macro_call.expn) else {
83-
return;
80+
let macro_name = macro_name.as_str();
81+
match macro_name {
82+
"assert_eq" | "debug_assert_eq" => check_eq(cx, expr, &macro_call, macro_name, true),
83+
"assert_ne" | "debug_assert_ne" => check_eq(cx, expr, &macro_call, macro_name, false),
84+
"assert" | "debug_assert" => check(cx, expr, &macro_call, macro_name),
85+
_ => {},
8486
};
87+
}
88+
}
8589

86-
let a_span = a.span.source_callsite();
87-
let b_span = b.span.source_callsite();
88-
89-
let (lit_span, bool_value, non_lit_expr) = match (extract_bool_lit(a), extract_bool_lit(b)) {
90-
// assert_eq!(true/false, b)
91-
// ^^^^^^^^^^^^
92-
(Some(bool_value), None) => (a_span.until(b_span), bool_value, b),
93-
// assert_eq!(a, true/false)
94-
// ^^^^^^^^^^^^
95-
(None, Some(bool_value)) => (b_span.with_lo(a_span.hi()), bool_value, a),
96-
// If there are two boolean arguments, we definitely don't understand
97-
// what's going on, so better leave things as is...
98-
//
99-
// Or there is simply no boolean and then we can leave things as is!
100-
_ => return,
101-
};
90+
fn check_eq<'tcx>(
91+
cx: &LateContext<'tcx>,
92+
expr: &'tcx Expr<'_>,
93+
macro_call: &MacroCall,
94+
macro_name: &str,
95+
eq_macro: bool,
96+
) {
97+
let Some((a, b, _)) = find_assert_eq_args(cx, expr, macro_call.expn) else {
98+
return;
99+
};
102100

103-
let non_lit_ty = cx.typeck_results().expr_ty(non_lit_expr);
101+
let a_span = a.span.source_callsite();
102+
let b_span = b.span.source_callsite();
104103

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

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

118-
let macro_name = macro_name.as_str();
119-
let non_eq_mac = &macro_name[..macro_name.len() - 3];
120-
span_lint_and_then(
121-
cx,
122-
BOOL_ASSERT_COMPARISON,
123-
macro_call.span,
124-
format!("used `{macro_name}!` with a literal bool"),
125-
|diag| {
126-
// assert_eq!(...)
127-
// ^^^^^^^^^
128-
let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
129-
130-
let mut suggestions = vec![(name_span, non_eq_mac.to_string()), (lit_span, String::new())];
131-
132-
if bool_value ^ eq_macro {
133-
let Some(sugg) = Sugg::hir_opt(cx, non_lit_expr) else {
134-
return;
135-
};
136-
suggestions.push((non_lit_expr.span, (!sugg).to_string()));
137-
}
138-
139-
diag.multipart_suggestion(
140-
format!("replace it with `{non_eq_mac}!(..)`"),
141-
suggestions,
142-
Applicability::MachineApplicable,
143-
);
144-
},
145-
);
120+
if !is_impl_not_trait_with_bool_out(cx, non_lit_ty) {
121+
// At this point the expression which is not a boolean
122+
// literal does not implement Not trait with a bool output,
123+
// so we cannot suggest to rewrite our code
124+
return;
125+
}
126+
127+
if !is_copy(cx, non_lit_ty) {
128+
// Only lint with types that are `Copy` because `assert!(x)` takes
129+
// ownership of `x` whereas `assert_eq(x, true)` does not
130+
return;
131+
}
132+
133+
let non_eq_mac = &macro_name[..macro_name.len() - 3];
134+
span_lint_and_then(
135+
cx,
136+
BOOL_ASSERT_COMPARISON,
137+
macro_call.span,
138+
format!("used `{macro_name}!` with a literal bool"),
139+
|diag| {
140+
// assert_eq!(...)
141+
// ^^^^^^^^^
142+
let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
143+
144+
let mut suggestions = vec![(name_span, non_eq_mac.to_string()), (lit_span, String::new())];
145+
146+
if bool_value ^ eq_macro {
147+
let Some(sugg) = Sugg::hir_opt(cx, non_lit_expr) else {
148+
return;
149+
};
150+
suggestions.push((non_lit_expr.span, (!sugg).to_string()));
151+
}
152+
153+
diag.multipart_suggestion(
154+
format!("replace it with `{non_eq_mac}!(..)`"),
155+
suggestions,
156+
Applicability::MachineApplicable,
157+
);
158+
},
159+
);
160+
}
161+
162+
/// Checks for `assert!(a == b)` and `assert!(a != b)`
163+
fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, macro_call: &MacroCall, macro_name: &str) {
164+
let Some((cond, _)) = find_assert_args(cx, expr, macro_call.expn) else {
165+
return;
166+
};
167+
let ExprKind::Binary(Spanned { node, span }, lhs, rhs) = cond.kind else {
168+
return;
169+
};
170+
// TODO: is `cond.span.from_expansion()` correct / needed?
171+
if (node != BinOpKind::Eq && node != BinOpKind::Ne) || cond.span.from_expansion() {
172+
return;
146173
}
174+
175+
let new_name = format!("{macro_name}_{}", if node == BinOpKind::Eq { "eq" } else { "ne" });
176+
let msg = format!("replace it with `{new_name}!(..)`");
177+
span_lint_and_then(
178+
cx,
179+
BOOL_ASSERT_COMPARISON,
180+
macro_call.span,
181+
format!("used `{macro_name}!` with an equality comparison"),
182+
|diag| {
183+
let macro_name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
184+
// TODO: should this be `cond.span`, expanded to include preceding whitespace? If so, how?
185+
let equality_span = Span::new(lhs.span.hi(), rhs.span.lo(), span.ctxt(), span.parent());
186+
let suggestions = vec![(macro_name_span, new_name), (equality_span, ", ".to_string())];
187+
188+
diag.multipart_suggestion(msg, suggestions, Applicability::MachineApplicable);
189+
},
190+
);
147191
}

clippy_lints/src/eta_reduction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ fn has_late_bound_to_non_late_bound_regions(from_sig: FnSig<'_>, to_sig: FnSig<'
331331
}
332332
}
333333

334-
assert!(from_sig.inputs_and_output.len() == to_sig.inputs_and_output.len());
334+
assert_eq!(from_sig.inputs_and_output.len(), to_sig.inputs_and_output.len());
335335
from_sig
336336
.inputs_and_output
337337
.iter()

clippy_lints/src/functions/renamed_function_params.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ impl RenamedFnArgs {
5858
{
5959
let mut renamed: Vec<(Span, String)> = vec![];
6060

61-
debug_assert!(default_names.size_hint() == current_names.size_hint());
61+
debug_assert_eq!(default_names.size_hint(), current_names.size_hint());
6262
while let (Some(def_name), Some(cur_name)) = (default_names.next(), current_names.next()) {
6363
let current_name = cur_name.name;
6464
let default_name = def_name.name;

clippy_utils/src/ty.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,8 +1037,9 @@ fn assert_generic_args_match<'tcx>(tcx: TyCtxt<'tcx>, did: DefId, args: &[Generi
10371037
.chain(&g.own_params)
10381038
.map(|x| &x.kind);
10391039

1040-
assert!(
1041-
count == args.len(),
1040+
assert_eq!(
1041+
count,
1042+
args.len(),
10421043
"wrong number of arguments for `{did:?}`: expected `{count}`, found {}\n\
10431044
note: the expected arguments are: `[{}]`\n\
10441045
the given arguments are: `{args:#?}`",

lintcheck/src/output.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ fn print_stats(old_stats: HashMap<String, usize>, new_stats: HashMap<&String, us
228228

229229
// remove duplicates from both hashmaps
230230
for (k, v) in &same_in_both_hashmaps {
231-
assert!(old_stats_deduped.remove(k) == Some(*v));
232-
assert!(new_stats_deduped.remove(k) == Some(*v));
231+
assert_eq!(old_stats_deduped.remove(k), Some(*v));
232+
assert_eq!(new_stats_deduped.remove(k), Some(*v));
233233
}
234234

235235
println!("\nStats:");

tests/ui/bool_assert_comparison.fixed

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,9 @@ fn main() {
166166
debug_assert!("".is_empty());
167167
debug_assert!(!"requires negation".is_empty());
168168
debug_assert!(!"requires negation".is_empty());
169+
170+
assert_eq!("a", "a".to_ascii_lowercase());
171+
assert_eq!("a", "a".to_ascii_lowercase(), "a==a");
172+
assert_ne!("A", "A".to_ascii_lowercase());
173+
assert_ne!("A", "A".to_ascii_lowercase(), "A!=a");
169174
}

tests/ui/bool_assert_comparison.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,9 @@ fn main() {
166166
debug_assert_ne!("".is_empty(), false);
167167
debug_assert_ne!("requires negation".is_empty(), true);
168168
debug_assert_eq!("requires negation".is_empty(), false);
169+
170+
assert!("a" == "a".to_ascii_lowercase());
171+
assert!("a" == "a".to_ascii_lowercase(), "a==a");
172+
assert!("A" != "A".to_ascii_lowercase());
173+
assert!("A" != "A".to_ascii_lowercase(), "A!=a");
169174
}

tests/ui/bool_assert_comparison.stderr

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,5 +396,49 @@ LL - debug_assert_eq!("requires negation".is_empty(), false);
396396
LL + debug_assert!(!"requires negation".is_empty());
397397
|
398398

399-
error: aborting due to 33 previous errors
399+
error: used `assert!` with an equality comparison
400+
--> tests/ui/bool_assert_comparison.rs:170:5
401+
|
402+
LL | assert!("a" == "a".to_ascii_lowercase());
403+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
404+
|
405+
help: replace it with `assert_eq!(..)`
406+
|
407+
LL | assert_eq!("a", "a".to_ascii_lowercase());
408+
| ~~~~~~~~~ ~
409+
410+
error: used `assert!` with an equality comparison
411+
--> tests/ui/bool_assert_comparison.rs:171:5
412+
|
413+
LL | assert!("a" == "a".to_ascii_lowercase(), "a==a");
414+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
415+
|
416+
help: replace it with `assert_eq!(..)`
417+
|
418+
LL | assert_eq!("a", "a".to_ascii_lowercase(), "a==a");
419+
| ~~~~~~~~~ ~
420+
421+
error: used `assert!` with an equality comparison
422+
--> tests/ui/bool_assert_comparison.rs:172:5
423+
|
424+
LL | assert!("A" != "A".to_ascii_lowercase());
425+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
426+
|
427+
help: replace it with `assert_ne!(..)`
428+
|
429+
LL | assert_ne!("A", "A".to_ascii_lowercase());
430+
| ~~~~~~~~~ ~
431+
432+
error: used `assert!` with an equality comparison
433+
--> tests/ui/bool_assert_comparison.rs:173:5
434+
|
435+
LL | assert!("A" != "A".to_ascii_lowercase(), "A!=a");
436+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
437+
|
438+
help: replace it with `assert_ne!(..)`
439+
|
440+
LL | assert_ne!("A", "A".to_ascii_lowercase(), "A!=a");
441+
| ~~~~~~~~~ ~
442+
443+
error: aborting due to 37 previous errors
400444

0 commit comments

Comments
 (0)