Skip to content

Commit 5f49808

Browse files
committed
Add machine applicable suggestion for bool_assert_comparison
1 parent 8d3c7f0 commit 5f49808

File tree

4 files changed

+453
-61
lines changed

4 files changed

+453
-61
lines changed

clippy_lints/src/bool_assert_comparison.rs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
12
use clippy_utils::macros::{find_assert_eq_args, root_macro_call_first_node};
2-
use clippy_utils::{diagnostics::span_lint_and_sugg, ty::implements_trait};
3+
use clippy_utils::ty::{implements_trait, is_copy};
34
use rustc_ast::ast::LitKind;
45
use rustc_errors::Applicability;
56
use rustc_hir::{Expr, ExprKind, Lit};
6-
use rustc_lint::{LateContext, LateLintPass};
7-
use rustc_middle::ty;
7+
use rustc_lint::{LateContext, LateLintPass, LintContext};
8+
use rustc_middle::ty::{self, Ty};
89
use rustc_session::{declare_lint_pass, declare_tool_lint};
910
use rustc_span::symbol::Ident;
1011

@@ -43,9 +44,7 @@ fn is_bool_lit(e: &Expr<'_>) -> bool {
4344
) && !e.span.from_expansion()
4445
}
4546

46-
fn is_impl_not_trait_with_bool_out(cx: &LateContext<'_>, e: &Expr<'_>) -> bool {
47-
let ty = cx.typeck_results().expr_ty(e);
48-
47+
fn is_impl_not_trait_with_bool_out<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
4948
cx.tcx
5049
.lang_items()
5150
.not_trait()
@@ -77,31 +76,57 @@ impl<'tcx> LateLintPass<'tcx> for BoolAssertComparison {
7776
return;
7877
}
7978
let Some ((a, b, _)) = find_assert_eq_args(cx, expr, macro_call.expn) else { return };
80-
if !(is_bool_lit(a) ^ is_bool_lit(b)) {
79+
80+
let a_span = a.span.source_callsite();
81+
let b_span = b.span.source_callsite();
82+
83+
let (lit_span, non_lit_expr) = match (is_bool_lit(a), is_bool_lit(b)) {
84+
// assert_eq!(true, b)
85+
// ^^^^^^
86+
(true, false) => (a_span.until(b_span), b),
87+
// assert_eq!(a, true)
88+
// ^^^^^^
89+
(false, true) => (b_span.with_lo(a_span.hi()), a),
8190
// If there are two boolean arguments, we definitely don't understand
8291
// what's going on, so better leave things as is...
8392
//
8493
// Or there is simply no boolean and then we can leave things as is!
85-
return;
86-
}
94+
_ => return,
95+
};
8796

88-
if !is_impl_not_trait_with_bool_out(cx, a) || !is_impl_not_trait_with_bool_out(cx, b) {
97+
let non_lit_ty = cx.typeck_results().expr_ty(non_lit_expr);
98+
99+
if !is_impl_not_trait_with_bool_out(cx, non_lit_ty) {
89100
// At this point the expression which is not a boolean
90101
// literal does not implement Not trait with a bool output,
91102
// so we cannot suggest to rewrite our code
92103
return;
93104
}
94105

106+
if !is_copy(cx, non_lit_ty) {
107+
// Only lint with types that are `Copy` because `assert!(x)` takes
108+
// ownership of `x` whereas `assert_eq(x, true)` does not
109+
return;
110+
}
111+
95112
let macro_name = macro_name.as_str();
96113
let non_eq_mac = &macro_name[..macro_name.len() - 3];
97-
span_lint_and_sugg(
114+
span_lint_and_then(
98115
cx,
99116
BOOL_ASSERT_COMPARISON,
100117
macro_call.span,
101118
&format!("used `{macro_name}!` with a literal bool"),
102-
"replace it with",
103-
format!("{non_eq_mac}!(..)"),
104-
Applicability::MaybeIncorrect,
119+
|diag| {
120+
// assert_eq!(...)
121+
// ^^^^^^^^^
122+
let name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
123+
124+
diag.multipart_suggestion(
125+
format!("replace it with `{non_eq_mac}!(..)`"),
126+
vec![(name_span, non_eq_mac.to_string()), (lit_span, String::new())],
127+
Applicability::MachineApplicable,
128+
);
129+
},
105130
);
106131
}
107132
}

tests/ui/bool_assert_comparison.fixed

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// run-rustfix
2+
3+
#![allow(unused, clippy::assertions_on_constants)]
4+
#![warn(clippy::bool_assert_comparison)]
5+
6+
use std::ops::Not;
7+
8+
macro_rules! a {
9+
() => {
10+
true
11+
};
12+
}
13+
macro_rules! b {
14+
() => {
15+
true
16+
};
17+
}
18+
19+
// Implements the Not trait but with an output type
20+
// that's not bool. Should not suggest a rewrite
21+
#[derive(Debug, Clone, Copy)]
22+
enum ImplNotTraitWithoutBool {
23+
VariantX(bool),
24+
VariantY(u32),
25+
}
26+
27+
impl PartialEq<bool> for ImplNotTraitWithoutBool {
28+
fn eq(&self, other: &bool) -> bool {
29+
match *self {
30+
ImplNotTraitWithoutBool::VariantX(b) => b == *other,
31+
_ => false,
32+
}
33+
}
34+
}
35+
36+
impl Not for ImplNotTraitWithoutBool {
37+
type Output = Self;
38+
39+
fn not(self) -> Self::Output {
40+
match self {
41+
ImplNotTraitWithoutBool::VariantX(b) => ImplNotTraitWithoutBool::VariantX(!b),
42+
ImplNotTraitWithoutBool::VariantY(0) => ImplNotTraitWithoutBool::VariantY(1),
43+
ImplNotTraitWithoutBool::VariantY(_) => ImplNotTraitWithoutBool::VariantY(0),
44+
}
45+
}
46+
}
47+
48+
// This type implements the Not trait with an Output of
49+
// type bool. Using assert!(..) must be suggested
50+
#[derive(Debug, Clone, Copy)]
51+
struct ImplNotTraitWithBool;
52+
53+
impl PartialEq<bool> for ImplNotTraitWithBool {
54+
fn eq(&self, other: &bool) -> bool {
55+
false
56+
}
57+
}
58+
59+
impl Not for ImplNotTraitWithBool {
60+
type Output = bool;
61+
62+
fn not(self) -> Self::Output {
63+
true
64+
}
65+
}
66+
67+
#[derive(Debug)]
68+
struct NonCopy;
69+
70+
impl PartialEq<bool> for NonCopy {
71+
fn eq(&self, other: &bool) -> bool {
72+
false
73+
}
74+
}
75+
76+
impl Not for NonCopy {
77+
type Output = bool;
78+
79+
fn not(self) -> Self::Output {
80+
true
81+
}
82+
}
83+
84+
fn main() {
85+
let a = ImplNotTraitWithoutBool::VariantX(true);
86+
let b = ImplNotTraitWithBool;
87+
88+
assert_eq!("a".len(), 1);
89+
assert!("a".is_empty());
90+
assert!("".is_empty());
91+
assert!("".is_empty());
92+
assert_eq!(a!(), b!());
93+
assert_eq!(a!(), "".is_empty());
94+
assert_eq!("".is_empty(), b!());
95+
assert_eq!(a, true);
96+
assert!(b);
97+
98+
assert_ne!("a".len(), 1);
99+
assert!("a".is_empty());
100+
assert!("".is_empty());
101+
assert!("".is_empty());
102+
assert_ne!(a!(), b!());
103+
assert_ne!(a!(), "".is_empty());
104+
assert_ne!("".is_empty(), b!());
105+
assert_ne!(a, true);
106+
assert!(b);
107+
108+
debug_assert_eq!("a".len(), 1);
109+
debug_assert!("a".is_empty());
110+
debug_assert!("".is_empty());
111+
debug_assert!("".is_empty());
112+
debug_assert_eq!(a!(), b!());
113+
debug_assert_eq!(a!(), "".is_empty());
114+
debug_assert_eq!("".is_empty(), b!());
115+
debug_assert_eq!(a, true);
116+
debug_assert!(b);
117+
118+
debug_assert_ne!("a".len(), 1);
119+
debug_assert!("a".is_empty());
120+
debug_assert!("".is_empty());
121+
debug_assert!("".is_empty());
122+
debug_assert_ne!(a!(), b!());
123+
debug_assert_ne!(a!(), "".is_empty());
124+
debug_assert_ne!("".is_empty(), b!());
125+
debug_assert_ne!(a, true);
126+
debug_assert!(b);
127+
128+
// assert with error messages
129+
assert_eq!("a".len(), 1, "tadam {}", 1);
130+
assert_eq!("a".len(), 1, "tadam {}", true);
131+
assert!("a".is_empty(), "tadam {}", 1);
132+
assert!("a".is_empty(), "tadam {}", true);
133+
assert!("a".is_empty(), "tadam {}", true);
134+
assert_eq!(a, true, "tadam {}", false);
135+
136+
debug_assert_eq!("a".len(), 1, "tadam {}", 1);
137+
debug_assert_eq!("a".len(), 1, "tadam {}", true);
138+
debug_assert!("a".is_empty(), "tadam {}", 1);
139+
debug_assert!("a".is_empty(), "tadam {}", true);
140+
debug_assert!("a".is_empty(), "tadam {}", true);
141+
debug_assert_eq!(a, true, "tadam {}", false);
142+
143+
assert!(a!());
144+
assert!(b!());
145+
146+
use debug_assert_eq as renamed;
147+
renamed!(a, true);
148+
debug_assert!(b);
149+
150+
let non_copy = NonCopy;
151+
assert_eq!(non_copy, true);
152+
// changing the above to `assert!(non_copy)` would cause a `borrow of moved value`
153+
println!("{non_copy:?}");
154+
155+
macro_rules! in_macro {
156+
($v:expr) => {{
157+
assert_eq!($v, true);
158+
}};
159+
}
160+
in_macro!(a);
161+
}

tests/ui/bool_assert_comparison.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// run-rustfix
2+
3+
#![allow(unused, clippy::assertions_on_constants)]
14
#![warn(clippy::bool_assert_comparison)]
25

36
use std::ops::Not;
@@ -15,7 +18,7 @@ macro_rules! b {
1518

1619
// Implements the Not trait but with an output type
1720
// that's not bool. Should not suggest a rewrite
18-
#[derive(Debug)]
21+
#[derive(Debug, Clone, Copy)]
1922
enum ImplNotTraitWithoutBool {
2023
VariantX(bool),
2124
VariantY(u32),
@@ -44,7 +47,7 @@ impl Not for ImplNotTraitWithoutBool {
4447

4548
// This type implements the Not trait with an Output of
4649
// type bool. Using assert!(..) must be suggested
47-
#[derive(Debug)]
50+
#[derive(Debug, Clone, Copy)]
4851
struct ImplNotTraitWithBool;
4952

5053
impl PartialEq<bool> for ImplNotTraitWithBool {
@@ -61,6 +64,23 @@ impl Not for ImplNotTraitWithBool {
6164
}
6265
}
6366

67+
#[derive(Debug)]
68+
struct NonCopy;
69+
70+
impl PartialEq<bool> for NonCopy {
71+
fn eq(&self, other: &bool) -> bool {
72+
false
73+
}
74+
}
75+
76+
impl Not for NonCopy {
77+
type Output = bool;
78+
79+
fn not(self) -> Self::Output {
80+
true
81+
}
82+
}
83+
6484
fn main() {
6585
let a = ImplNotTraitWithoutBool::VariantX(true);
6686
let b = ImplNotTraitWithBool;
@@ -119,4 +139,23 @@ fn main() {
119139
debug_assert_eq!("a".is_empty(), false, "tadam {}", true);
120140
debug_assert_eq!(false, "a".is_empty(), "tadam {}", true);
121141
debug_assert_eq!(a, true, "tadam {}", false);
142+
143+
assert_eq!(a!(), true);
144+
assert_eq!(true, b!());
145+
146+
use debug_assert_eq as renamed;
147+
renamed!(a, true);
148+
renamed!(b, true);
149+
150+
let non_copy = NonCopy;
151+
assert_eq!(non_copy, true);
152+
// changing the above to `assert!(non_copy)` would cause a `borrow of moved value`
153+
println!("{non_copy:?}");
154+
155+
macro_rules! in_macro {
156+
($v:expr) => {{
157+
assert_eq!($v, true);
158+
}};
159+
}
160+
in_macro!(a);
122161
}

0 commit comments

Comments
 (0)