Skip to content

Commit 47fdfc8

Browse files
Add lint to check for boolean comparison in assert macro calls
1 parent b1c675f commit 47fdfc8

File tree

5 files changed

+256
-0
lines changed

5 files changed

+256
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,6 +2124,7 @@ Released 2018-09-13
21242124
[`blacklisted_name`]: https://rust-lang.github.io/rust-clippy/master/index.html#blacklisted_name
21252125
[`blanket_clippy_restriction_lints`]: https://rust-lang.github.io/rust-clippy/master/index.html#blanket_clippy_restriction_lints
21262126
[`blocks_in_if_conditions`]: https://rust-lang.github.io/rust-clippy/master/index.html#blocks_in_if_conditions
2127+
[`bool_assert_comparison`]: https://rust-lang.github.io/rust-clippy/master/index.html#bool_assert_comparison
21272128
[`bool_comparison`]: https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison
21282129
[`borrow_interior_mutable_const`]: https://rust-lang.github.io/rust-clippy/master/index.html#borrow_interior_mutable_const
21292130
[`borrowed_box`]: https://rust-lang.github.io/rust-clippy/master/index.html#borrowed_box
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::source::snippet;
3+
use rustc_ast::ast::{MacArgs, MacCall};
4+
use rustc_ast::token::{Token, TokenKind};
5+
use rustc_ast::tokenstream::TokenTree;
6+
use rustc_errors::Applicability;
7+
use rustc_lint::{EarlyContext, EarlyLintPass};
8+
use rustc_session::{declare_lint_pass, declare_tool_lint};
9+
use rustc_span::symbol::kw::{False, True};
10+
11+
declare_clippy_lint! {
12+
/// **What it does:** This lint warns about boolean comparisons in assert-like macros.
13+
///
14+
/// **Why is this bad?** It is shorter to use the equivalent.
15+
///
16+
/// **Known problems:** None.
17+
///
18+
/// **Example:**
19+
///
20+
/// ```rust
21+
/// // Bad
22+
/// assert_eq!("a".is_empty(), false);
23+
/// assert_ne!("a".is_empty(), true);
24+
///
25+
/// // Good
26+
/// assert!(!"a".is_empty());
27+
/// ```
28+
pub BOOL_ASSERT_COMPARISON,
29+
pedantic,
30+
"Using a boolean as comparison value when there is no need"
31+
}
32+
33+
declare_lint_pass!(BoolAssertComparison => [BOOL_ASSERT_COMPARISON]);
34+
35+
fn is_bool(tt: Option<&TokenTree>) -> bool {
36+
match tt {
37+
Some(TokenTree::Token(Token {
38+
kind: TokenKind::Ident(i, _),
39+
..
40+
})) => *i == True || *i == False,
41+
_ => false,
42+
}
43+
}
44+
45+
impl EarlyLintPass for BoolAssertComparison {
46+
fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &MacCall) {
47+
let macros = ["assert_eq", "debug_assert_eq"];
48+
let inverted_macros = ["assert_ne", "debug_assert_ne"];
49+
50+
if let MacArgs::Delimited(_, _, ts) = &*mac.args {
51+
let name;
52+
if let [seg] = &*mac.path.segments {
53+
name = seg.ident.name.as_str().to_string();
54+
if !macros.contains(&name.as_str()) && !inverted_macros.contains(&name.as_str()) {
55+
return;
56+
}
57+
} else {
58+
return;
59+
}
60+
let mut has_true = false;
61+
let mut has_false = false;
62+
let mut bool_span = None;
63+
let mut spans = Vec::new();
64+
let mut nb_comma = 0;
65+
let mut iter = ts.trees().peekable();
66+
while let Some(tt) = iter.next() {
67+
if let TokenTree::Token(ref token) = tt {
68+
match token.kind {
69+
TokenKind::Ident(i, _) => {
70+
// We only want to check the comparison arguments, nothing else!
71+
if nb_comma < 2 {
72+
if i == True {
73+
has_true = true;
74+
bool_span = Some(token.span);
75+
continue;
76+
} else if i == False {
77+
has_false = true;
78+
bool_span = Some(token.span);
79+
continue;
80+
}
81+
}
82+
},
83+
TokenKind::Comma => {
84+
nb_comma += 1;
85+
if nb_comma > 1 || !is_bool(iter.peek()) {
86+
spans.push((token.span, true));
87+
}
88+
continue;
89+
},
90+
_ => {},
91+
}
92+
}
93+
spans.push((tt.span(), false));
94+
}
95+
#[allow(clippy::if_same_then_else)] // It allows better code reability here.
96+
if has_false && has_true {
97+
// Don't know what's going on here, but better leave things as is...
98+
return;
99+
} else if !has_true && !has_false {
100+
// No problem detected!
101+
return;
102+
}
103+
let text = spans
104+
.into_iter()
105+
.map(|(s, needs_whitespace)| {
106+
let mut s = snippet(cx, s, "arg").to_string();
107+
if needs_whitespace {
108+
s.push(' ');
109+
}
110+
s
111+
})
112+
.collect::<String>();
113+
let is_cond_inverted = inverted_macros.contains(&name.as_str());
114+
let extra = (has_true && is_cond_inverted) || has_false;
115+
116+
span_lint_and_sugg(
117+
cx,
118+
BOOL_ASSERT_COMPARISON,
119+
bool_span.expect("failed to get the bool span"),
120+
"assert macro with a boolean comparison",
121+
"replace it with",
122+
format!("{}!({}{})", &name[..name.len() - 3], if extra { "!" } else { "" }, text),
123+
Applicability::MachineApplicable,
124+
);
125+
}
126+
}
127+
}

clippy_lints/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ mod await_holding_invalid;
178178
mod bit_mask;
179179
mod blacklisted_name;
180180
mod blocks_in_if_conditions;
181+
mod bool_assert_comparison;
181182
mod booleans;
182183
mod bytecount;
183184
mod cargo_common_metadata;
@@ -393,6 +394,7 @@ pub fn register_pre_expansion_lints(store: &mut rustc_lint::LintStore) {
393394
store.register_pre_expansion_pass(|| box write::Write::default());
394395
store.register_pre_expansion_pass(|| box attrs::EarlyAttributes);
395396
store.register_pre_expansion_pass(|| box dbg_macro::DbgMacro);
397+
store.register_pre_expansion_pass(|| box bool_assert_comparison::BoolAssertComparison);
396398
}
397399

398400
#[doc(hidden)]
@@ -567,6 +569,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
567569
bit_mask::VERBOSE_BIT_MASK,
568570
blacklisted_name::BLACKLISTED_NAME,
569571
blocks_in_if_conditions::BLOCKS_IN_IF_CONDITIONS,
572+
bool_assert_comparison::BOOL_ASSERT_COMPARISON,
570573
booleans::LOGIC_BUG,
571574
booleans::NONMINIMAL_BOOL,
572575
bytecount::NAIVE_BYTECOUNT,
@@ -1332,6 +1335,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13321335
LintId::of(await_holding_invalid::AWAIT_HOLDING_LOCK),
13331336
LintId::of(await_holding_invalid::AWAIT_HOLDING_REFCELL_REF),
13341337
LintId::of(bit_mask::VERBOSE_BIT_MASK),
1338+
LintId::of(bool_assert_comparison::BOOL_ASSERT_COMPARISON),
13351339
LintId::of(bytecount::NAIVE_BYTECOUNT),
13361340
LintId::of(case_sensitive_file_extension_comparisons::CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS),
13371341
LintId::of(casts::CAST_LOSSLESS),

tests/ui/bool_assert_comparison.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#![warn(clippy::bool_assert_comparison)]
2+
3+
fn main() {
4+
assert_eq!("a".len(), 1);
5+
assert_eq!("a".is_empty(), false);
6+
assert_eq!("".is_empty(), true);
7+
assert_eq!(true, "".is_empty());
8+
9+
assert_ne!("a".len(), 1);
10+
assert_ne!("a".is_empty(), false);
11+
assert_ne!("".is_empty(), true);
12+
assert_ne!(true, "".is_empty());
13+
14+
debug_assert_eq!("a".len(), 1);
15+
debug_assert_eq!("a".is_empty(), false);
16+
debug_assert_eq!("".is_empty(), true);
17+
debug_assert_eq!(true, "".is_empty());
18+
19+
debug_assert_ne!("a".len(), 1);
20+
debug_assert_ne!("a".is_empty(), false);
21+
debug_assert_ne!("".is_empty(), true);
22+
debug_assert_ne!(true, "".is_empty());
23+
24+
// assert with error messages
25+
assert_eq!("a".len(), 1, "tadam {}", 1);
26+
assert_eq!("a".len(), 1, "tadam {}", true);
27+
assert_eq!("a".is_empty(), false, "tadam {}", 1);
28+
assert_eq!("a".is_empty(), false, "tadam {}", true);
29+
assert_eq!(false, "a".is_empty(), "tadam {}", true);
30+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
error: assert macro with a boolean comparison
2+
--> $DIR/bool_assert_comparison.rs:5:32
3+
|
4+
LL | assert_eq!("a".is_empty(), false);
5+
| ^^^^^ help: replace it with: `assert!(!"a".is_empty())`
6+
|
7+
= note: `-D clippy::bool-assert-comparison` implied by `-D warnings`
8+
9+
error: assert macro with a boolean comparison
10+
--> $DIR/bool_assert_comparison.rs:6:31
11+
|
12+
LL | assert_eq!("".is_empty(), true);
13+
| ^^^^ help: replace it with: `assert!("".is_empty())`
14+
15+
error: assert macro with a boolean comparison
16+
--> $DIR/bool_assert_comparison.rs:7:16
17+
|
18+
LL | assert_eq!(true, "".is_empty());
19+
| ^^^^ help: replace it with: `assert!`
20+
21+
error: assert macro with a boolean comparison
22+
--> $DIR/bool_assert_comparison.rs:10:32
23+
|
24+
LL | assert_ne!("a".is_empty(), false);
25+
| ^^^^^ help: replace it with: `assert!(!"a".is_empty())`
26+
27+
error: assert macro with a boolean comparison
28+
--> $DIR/bool_assert_comparison.rs:11:31
29+
|
30+
LL | assert_ne!("".is_empty(), true);
31+
| ^^^^ help: replace it with: `assert!(!"".is_empty())`
32+
33+
error: assert macro with a boolean comparison
34+
--> $DIR/bool_assert_comparison.rs:12:16
35+
|
36+
LL | assert_ne!(true, "".is_empty());
37+
| ^^^^ help: replace it with: `assert!`
38+
39+
error: assert macro with a boolean comparison
40+
--> $DIR/bool_assert_comparison.rs:15:38
41+
|
42+
LL | debug_assert_eq!("a".is_empty(), false);
43+
| ^^^^^ help: replace it with: `debug_assert!(!"a".is_empty())`
44+
45+
error: assert macro with a boolean comparison
46+
--> $DIR/bool_assert_comparison.rs:16:37
47+
|
48+
LL | debug_assert_eq!("".is_empty(), true);
49+
| ^^^^ help: replace it with: `debug_assert!("".is_empty())`
50+
51+
error: assert macro with a boolean comparison
52+
--> $DIR/bool_assert_comparison.rs:17:22
53+
|
54+
LL | debug_assert_eq!(true, "".is_empty());
55+
| ^^^^ help: replace it with: `debug_assert!`
56+
57+
error: assert macro with a boolean comparison
58+
--> $DIR/bool_assert_comparison.rs:20:38
59+
|
60+
LL | debug_assert_ne!("a".is_empty(), false);
61+
| ^^^^^ help: replace it with: `debug_assert!(!"a".is_empty())`
62+
63+
error: assert macro with a boolean comparison
64+
--> $DIR/bool_assert_comparison.rs:21:37
65+
|
66+
LL | debug_assert_ne!("".is_empty(), true);
67+
| ^^^^ help: replace it with: `debug_assert!(!"".is_empty())`
68+
69+
error: assert macro with a boolean comparison
70+
--> $DIR/bool_assert_comparison.rs:22:22
71+
|
72+
LL | debug_assert_ne!(true, "".is_empty());
73+
| ^^^^ help: replace it with: `debug_assert!`
74+
75+
error: assert macro with a boolean comparison
76+
--> $DIR/bool_assert_comparison.rs:27:32
77+
|
78+
LL | assert_eq!("a".is_empty(), false, "tadam {}", 1);
79+
| ^^^^^ help: replace it with: `assert!(!"a".is_empty(), "tadam {}", 1)`
80+
81+
error: assert macro with a boolean comparison
82+
--> $DIR/bool_assert_comparison.rs:28:32
83+
|
84+
LL | assert_eq!("a".is_empty(), false, "tadam {}", true);
85+
| ^^^^^ help: replace it with: `assert!(!"a".is_empty(), "tadam {}", true)`
86+
87+
error: assert macro with a boolean comparison
88+
--> $DIR/bool_assert_comparison.rs:29:16
89+
|
90+
LL | assert_eq!(false, "a".is_empty(), "tadam {}", true);
91+
| ^^^^^ help: replace it with: `assert!`
92+
93+
error: aborting due to 15 previous errors
94+

0 commit comments

Comments
 (0)