• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = &macro_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