1 use clippy_utils::diagnostics::span_lint_and_then;
2 use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node};
3 use clippy_utils::sugg::Sugg;
4 use clippy_utils::ty::{implements_trait, is_copy};
5 use rustc_ast::ast::LitKind;
6 use rustc_errors::Applicability;
7 use rustc_hir::{Expr, ExprKind, Lit};
8 use rustc_lint::{LateContext, LateLintPass, LintContext};
9 use rustc_middle::ty::{self, Ty};
10 use rustc_session::{declare_lint_pass, declare_tool_lint};
11 use rustc_span::symbol::Ident;
12
13 declare_clippy_lint! {
14 /// ### What it does
15 /// This lint warns about boolean comparisons in assert-like macros.
16 ///
17 /// ### Why is this bad?
18 /// It is shorter to use the equivalent.
19 ///
20 /// ### Example
21 /// ```rust
22 /// assert_eq!("a".is_empty(), false);
23 /// assert_ne!("a".is_empty(), true);
24 /// ```
25 ///
26 /// Use instead:
27 /// ```rust
28 /// assert!(!"a".is_empty());
29 /// ```
30 #[clippy::version = "1.53.0"]
31 pub BOOL_ASSERT_COMPARISON,
32 style,
33 "Using a boolean as comparison value in an assert_* macro when there is no need"
34 }
35
36 declare_lint_pass!(BoolAssertComparison => [BOOL_ASSERT_COMPARISON]);
37
extract_bool_lit(e: &Expr<'_>) -> Option<bool>38 fn extract_bool_lit(e: &Expr<'_>) -> Option<bool> {
39 if let ExprKind::Lit(Lit {
40 node: LitKind::Bool(b), ..
41 }) = e.kind
42 && !e.span.from_expansion()
43 {
44 Some(*b)
45 } else {
46 None
47 }
48 }
49
is_impl_not_trait_with_bool_out<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool50 fn is_impl_not_trait_with_bool_out<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
51 cx.tcx
52 .lang_items()
53 .not_trait()
54 .filter(|trait_id| implements_trait(cx, ty, *trait_id, &[]))
55 .and_then(|trait_id| {
56 cx.tcx.associated_items(trait_id).find_by_name_and_kind(
57 cx.tcx,
58 Ident::from_str("Output"),
59 ty::AssocKind::Type,
60 trait_id,
61 )
62 })
63 .map_or(false, |assoc_item| {
64 let proj = Ty::new_projection(cx.tcx,assoc_item.def_id, cx.tcx.mk_substs_trait(ty, []));
65 let nty = cx.tcx.normalize_erasing_regions(cx.param_env, proj);
66
67 nty.is_bool()
68 })
69 }
70
71 impl<'tcx> LateLintPass<'tcx> for BoolAssertComparison {
check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>)72 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
73 let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return };
74 let macro_name = cx.tcx.item_name(macro_call.def_id);
75 let eq_macro = match macro_name.as_str() {
76 "assert_eq" | "debug_assert_eq" => true,
77 "assert_ne" | "debug_assert_ne" => false,
78 _ => return,
79 };
80 let Some ((a, b, _)) = find_assert_eq_args(cx, expr, macro_call.expn) else { return };
81
82 let a_span = a.span.source_callsite();
83 let b_span = b.span.source_callsite();
84
85 let (lit_span, bool_value, non_lit_expr) = match (extract_bool_lit(a), extract_bool_lit(b)) {
86 // assert_eq!(true/false, b)
87 // ^^^^^^^^^^^^
88 (Some(bool_value), None) => (a_span.until(b_span), bool_value, b),
89 // assert_eq!(a, true/false)
90 // ^^^^^^^^^^^^
91 (None, Some(bool_value)) => (b_span.with_lo(a_span.hi()), bool_value, a),
92 // If there are two boolean arguments, we definitely don't understand
93 // what's going on, so better leave things as is...
94 //
95 // Or there is simply no boolean and then we can leave things as is!
96 _ => return,
97 };
98
99 let non_lit_ty = cx.typeck_results().expr_ty(non_lit_expr);
100
101 if !is_impl_not_trait_with_bool_out(cx, non_lit_ty) {
102 // At this point the expression which is not a boolean
103 // literal does not implement Not trait with a bool output,
104 // so we cannot suggest to rewrite our code
105 return;
106 }
107
108 if !is_copy(cx, non_lit_ty) {
109 // Only lint with types that are `Copy` because `assert!(x)` takes
110 // ownership of `x` whereas `assert_eq(x, true)` does not
111 return;
112 }
113
114 let macro_name = macro_name.as_str();
115 let non_eq_mac = ¯o_name[..macro_name.len() - 3];
116 span_lint_and_then(
117 cx,
118 BOOL_ASSERT_COMPARISON,
119 macro_call.span,
120 &format!("used `{macro_name}!` with a literal bool"),
121 |diag| {
122 // assert_eq!(...)
123 // ^^^^^^^^^
124 let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
125
126 let mut suggestions = vec![(name_span, non_eq_mac.to_string()), (lit_span, String::new())];
127
128 if bool_value ^ eq_macro {
129 let Some(sugg) = Sugg::hir_opt(cx, non_lit_expr) else { return };
130 suggestions.push((non_lit_expr.span, (!sugg).to_string()));
131 }
132
133 diag.multipart_suggestion(
134 format!("replace it with `{non_eq_mac}!(..)`"),
135 suggestions,
136 Applicability::MachineApplicable,
137 );
138 },
139 );
140 }
141 }
142