Skip to content

Commit

Permalink
Add lint to check for boolean comparison in assert macro calls
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeGomez committed Apr 15, 2021
1 parent b1c675f commit 3f2c607
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2124,6 +2124,7 @@ Released 2018-09-13
[`blacklisted_name`]: https://rust-lang.github.io/rust-clippy/master/index.html#blacklisted_name
[`blanket_clippy_restriction_lints`]: https://rust-lang.github.io/rust-clippy/master/index.html#blanket_clippy_restriction_lints
[`blocks_in_if_conditions`]: https://rust-lang.github.io/rust-clippy/master/index.html#blocks_in_if_conditions
[`bool_assert_comparison`]: https://rust-lang.github.io/rust-clippy/master/index.html#bool_assert_comparison
[`bool_comparison`]: https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison
[`borrow_interior_mutable_const`]: https://rust-lang.github.io/rust-clippy/master/index.html#borrow_interior_mutable_const
[`borrowed_box`]: https://rust-lang.github.io/rust-clippy/master/index.html#borrowed_box
Expand Down
Binary file added bool_assert_eq
Binary file not shown.
127 changes: 127 additions & 0 deletions clippy_lints/src/bool_assert_comparison.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::snippet;
use rustc_ast::ast::{MacArgs, MacCall};
use rustc_ast::token::{Token, TokenKind};
use rustc_ast::tokenstream::TokenTree;
use rustc_errors::Applicability;
use rustc_lint::{EarlyContext, EarlyLintPass};
use rustc_session::{declare_lint_pass, declare_tool_lint};
use rustc_span::symbol::kw::{False, True};

declare_clippy_lint! {
/// **What it does:** This lint warns about boolean comparisons in assert-like macros.
///
/// **Why is this bad?** It is shorter to use the equivalent.
///
/// **Known problems:** None.
///
/// **Example:**
///
/// ```rust
/// // Bad
/// assert_eq!("a".is_empty(), false);
/// assert_ne!("a".is_empty(), true);
///
/// // Good
/// assert!(!"a".is_empty());
/// ```
pub BOOL_ASSERT_COMPARISON,
pedantic,
"Using a boolean as comparison value when there is no need"
}

declare_lint_pass!(BoolAssertComparison => [BOOL_ASSERT_COMPARISON]);

fn is_bool(tt: Option<&TokenTree>) -> bool {
match tt {
Some(TokenTree::Token(Token {
kind: TokenKind::Ident(i, _),
..
})) => *i == True || *i == False,
_ => false,
}
}

impl EarlyLintPass for BoolAssertComparison {
fn check_mac(&mut self, cx: &EarlyContext<'_>, mac: &MacCall) {
let macros = ["assert_eq", "debug_assert_eq"];
let inverted_macros = ["assert_ne", "debug_assert_ne"];

if let MacArgs::Delimited(_, _, ts) = &*mac.args {
let name;
if let [seg] = &*mac.path.segments {
name = seg.ident.name.as_str().to_string();
if !macros.contains(&name.as_str()) && !inverted_macros.contains(&name.as_str()) {
return;
}
} else {
return;
}
let mut has_true = false;
let mut has_false = false;
let mut bool_span = None;
let mut spans = Vec::new();
let mut nb_comma = 0;
let mut iter = ts.trees().peekable();
while let Some(tt) = iter.next() {
if let TokenTree::Token(ref token) = tt {
match token.kind {
TokenKind::Ident(i, _) => {
// We only want to check the comparison arguments, nothing else!
if nb_comma < 2 {
if i == True {
has_true = true;
bool_span = Some(token.span);
continue;
} else if i == False {
has_false = true;
bool_span = Some(token.span);
continue;
}
}
},
TokenKind::Comma => {
nb_comma += 1;
if nb_comma > 1 || !is_bool(iter.peek()) {
spans.push((token.span, true));
}
continue;
},
_ => {},
}
}
spans.push((tt.span(), false));
}
#[allow(clippy::if_same_then_else)] // It allows better code reability here.
if has_false && has_true {
// Don't know what's going on here, but better leave things as is...
return;
} else if !has_true && !has_false {
// No problem detected!
return;
}
let text = spans
.into_iter()
.map(|(s, needs_whitespace)| {
let mut s = snippet(cx, s, "arg").to_string();
if needs_whitespace {
s.push(' ');
}
s
})
.collect::<String>();
let is_cond_inverted = inverted_macros.contains(&name.as_str());
let extra = (has_true && is_cond_inverted) || has_false;

span_lint_and_sugg(
cx,
BOOL_ASSERT_COMPARISON,
bool_span.expect("failed to get the bool span"),
"assert macro with a boolean comparison",
"replace it with",
format!("{}!({}{})", &name[..name.len() - 3], if extra { "!" } else { "" }, text),
Applicability::MachineApplicable,
);
}
}
}
4 changes: 4 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ mod await_holding_invalid;
mod bit_mask;
mod blacklisted_name;
mod blocks_in_if_conditions;
mod bool_assert_comparison;
mod booleans;
mod bytecount;
mod cargo_common_metadata;
Expand Down Expand Up @@ -393,6 +394,7 @@ pub fn register_pre_expansion_lints(store: &mut rustc_lint::LintStore) {
store.register_pre_expansion_pass(|| box write::Write::default());
store.register_pre_expansion_pass(|| box attrs::EarlyAttributes);
store.register_pre_expansion_pass(|| box dbg_macro::DbgMacro);
store.register_pre_expansion_pass(|| box bool_assert_comparison::BoolAssertComparison);
}

#[doc(hidden)]
Expand Down Expand Up @@ -567,6 +569,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
bit_mask::VERBOSE_BIT_MASK,
blacklisted_name::BLACKLISTED_NAME,
blocks_in_if_conditions::BLOCKS_IN_IF_CONDITIONS,
bool_assert_comparison::BOOL_ASSERT_COMPARISON,
booleans::LOGIC_BUG,
booleans::NONMINIMAL_BOOL,
bytecount::NAIVE_BYTECOUNT,
Expand Down Expand Up @@ -1332,6 +1335,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
LintId::of(await_holding_invalid::AWAIT_HOLDING_LOCK),
LintId::of(await_holding_invalid::AWAIT_HOLDING_REFCELL_REF),
LintId::of(bit_mask::VERBOSE_BIT_MASK),
LintId::of(bool_assert_comparison::BOOL_ASSERT_COMPARISON),
LintId::of(bytecount::NAIVE_BYTECOUNT),
LintId::of(case_sensitive_file_extension_comparisons::CASE_SENSITIVE_FILE_EXTENSION_COMPARISONS),
LintId::of(casts::CAST_LOSSLESS),
Expand Down
25 changes: 25 additions & 0 deletions tests/ui/bool_assert_comparison.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#![warn(clippy::bool_assert_comparison)]

fn main() {
assert_eq!("a".len(), 1);
assert_eq!("a".is_empty(), false);
assert_eq!("".is_empty(), true);

assert_ne!("a".len(), 1);
assert_ne!("a".is_empty(), false);
assert_ne!("".is_empty(), true);

debug_assert_eq!("a".len(), 1);
debug_assert_eq!("a".is_empty(), false);
debug_assert_eq!("".is_empty(), true);

debug_assert_ne!("a".len(), 1);
debug_assert_ne!("a".is_empty(), false);
debug_assert_ne!("".is_empty(), true);

// assert with error messages
assert_eq!("a".len(), 1, "tadam {}", 1);
assert_eq!("a".len(), 1, "tadam {}", true);
assert_eq!("a".is_empty(), false, "tadam {}", 1);
assert_eq!("a".is_empty(), false, "tadam {}", true);
}
64 changes: 64 additions & 0 deletions tests/ui/bool_assert_comparison.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:5:32
|
LL | assert_eq!("a".is_empty(), false);
| ^^^^^ help: replace it with: `assert!(!"a".is_empty())`
|
= note: `-D clippy::bool-assert-comparison` implied by `-D warnings`

error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:6:31
|
LL | assert_eq!("".is_empty(), true);
| ^^^^ help: replace it with: `assert!("".is_empty())`

error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:9:32
|
LL | assert_ne!("a".is_empty(), false);
| ^^^^^ help: replace it with: `assert!(!"a".is_empty())`

error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:10:31
|
LL | assert_ne!("".is_empty(), true);
| ^^^^ help: replace it with: `assert!(!"".is_empty())`

error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:13:38
|
LL | debug_assert_eq!("a".is_empty(), false);
| ^^^^^ help: replace it with: `debug_assert!(!"a".is_empty())`

error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:14:37
|
LL | debug_assert_eq!("".is_empty(), true);
| ^^^^ help: replace it with: `debug_assert!("".is_empty())`

error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:17:38
|
LL | debug_assert_ne!("a".is_empty(), false);
| ^^^^^ help: replace it with: `debug_assert!(!"a".is_empty())`

error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:18:37
|
LL | debug_assert_ne!("".is_empty(), true);
| ^^^^ help: replace it with: `debug_assert!(!"".is_empty())`

error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:23:32
|
LL | assert_eq!("a".is_empty(), false, "tadam {}", 1);
| ^^^^^ help: replace it with: `assert!(!"a".is_empty(), "tadam {}", 1)`

error: assert macro with a boolean comparison
--> $DIR/bool_assert_comparison.rs:24:32
|
LL | assert_eq!("a".is_empty(), false, "tadam {}", true);
| ^^^^^ help: replace it with: `assert!(!"a".is_empty(), "tadam {}", true)`

error: aborting due to 10 previous errors

0 comments on commit 3f2c607

Please sign in to comment.