Skip to content

Commit 4db706b

Browse files
committed
feat(manual_assert_eq): new lint
1 parent ad7fc4f commit 4db706b

File tree

11 files changed

+289
-12
lines changed

11 files changed

+289
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6526,6 +6526,7 @@ Released 2018-09-13
65266526
[`main_recursion`]: https://rust-lang.github.io/rust-clippy/master/index.html#main_recursion
65276527
[`manual_abs_diff`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_abs_diff
65286528
[`manual_assert`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_assert
6529+
[`manual_assert_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_assert_eq
65296530
[`manual_async_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
65306531
[`manual_bits`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_bits
65316532
[`manual_c_str_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_c_str_literals

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
292292
crate::main_recursion::MAIN_RECURSION_INFO,
293293
crate::manual_abs_diff::MANUAL_ABS_DIFF_INFO,
294294
crate::manual_assert::MANUAL_ASSERT_INFO,
295+
crate::manual_assert_eq::MANUAL_ASSERT_EQ_INFO,
295296
crate::manual_async_fn::MANUAL_ASYNC_FN_INFO,
296297
crate::manual_bits::MANUAL_BITS_INFO,
297298
crate::manual_clamp::MANUAL_CLAMP_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ mod macro_use;
195195
mod main_recursion;
196196
mod manual_abs_diff;
197197
mod manual_assert;
198+
mod manual_assert_eq;
198199
mod manual_async_fn;
199200
mod manual_bits;
200201
mod manual_clamp;
@@ -819,5 +820,6 @@ pub fn register_lint_passes(store: &mut rustc_lint::LintStore, conf: &'static Co
819820
store.register_late_pass(|_| Box::new(toplevel_ref_arg::ToplevelRefArg));
820821
store.register_late_pass(|_| Box::new(volatile_composites::VolatileComposites));
821822
store.register_late_pass(|_| Box::new(replace_box::ReplaceBox::default()));
823+
store.register_late_pass(|_| Box::new(manual_assert_eq::ManualAssertEq));
822824
// add lints here, do not remove this comment, it's used in `new_lint`
823825
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::macros::{find_assert_args, root_macro_call_first_node};
3+
use clippy_utils::source::walk_span_to_context;
4+
use clippy_utils::{is_in_const_context, sym};
5+
use rustc_errors::Applicability;
6+
use rustc_hir::{BinOpKind, Expr, ExprKind};
7+
use rustc_lint::{LateContext, LateLintPass, LintContext};
8+
use rustc_session::declare_lint_pass;
9+
10+
declare_clippy_lint! {
11+
/// ### What it does
12+
/// Checks for `assert!` and `debug_assert!` that consist of only an (in)equality check
13+
///
14+
/// ### Why is this bad?
15+
/// `assert_{eq,ne}!` and `debug_assert_{eq,ne}!` achieves the same goal, and provides some
16+
/// additional debug information
17+
///
18+
/// ### Example
19+
/// ```no_run
20+
/// assert!(2 * 2 == 4);
21+
/// assert!(2 * 2 != 5);
22+
/// debug_assert!(2 * 2 == 4);
23+
/// debug_assert!(2 * 2 != 5);
24+
/// ```
25+
/// Use instead:
26+
/// ```no_run
27+
/// assert_eq!(2 * 2, 4);
28+
/// assert_ne!(2 * 2, 5);
29+
/// debug_assert_eq!(2 * 2, 4);
30+
/// debug_assert_ne!(2 * 2, 5);
31+
/// ```
32+
#[clippy::version = "1.92.0"]
33+
pub MANUAL_ASSERT_EQ,
34+
complexity,
35+
"checks for assertions consisting of an (in)equality check"
36+
}
37+
declare_lint_pass!(ManualAssertEq => [MANUAL_ASSERT_EQ]);
38+
39+
impl LateLintPass<'_> for ManualAssertEq {
40+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
41+
if let Some(macro_call) = root_macro_call_first_node(cx, expr)
42+
&& let macro_name = cx.tcx.item_name(macro_call.def_id)
43+
&& matches!(macro_name, sym::assert | sym::debug_assert)
44+
// `assert_eq` isn't allowed in const context because it calls non-const `core::panicking::assert_failed`
45+
// XXX: this might change in the future, so might want to relax this restriction
46+
&& !is_in_const_context(cx)
47+
&& let Some((cond, _)) = find_assert_args(cx, expr, macro_call.expn)
48+
&& let ExprKind::Binary(op, lhs, rhs) = cond.kind
49+
&& matches!(op.node, BinOpKind::Eq | BinOpKind::Ne)
50+
&& !cond.span.from_expansion()
51+
{
52+
let macro_name = macro_name.as_str();
53+
span_lint_and_then(
54+
cx,
55+
MANUAL_ASSERT_EQ,
56+
macro_call.span,
57+
format!("used `{macro_name}!` with an equality comparison"),
58+
|diag| {
59+
let new_name = format!("{macro_name}_{}", if op.node == BinOpKind::Eq { "eq" } else { "ne" });
60+
let msg = format!("replace it with `{new_name}!(..)`");
61+
62+
let ctxt = cond.span.ctxt();
63+
if let Some(lhs_span) = walk_span_to_context(lhs.span, ctxt)
64+
&& let Some(rhs_span) = walk_span_to_context(rhs.span, ctxt)
65+
{
66+
let macro_name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
67+
let eq_span = cond.span.with_lo(lhs_span.hi()).with_hi(rhs_span.lo());
68+
let suggestions = vec![(macro_name_span, new_name), (eq_span, ", ".to_string())];
69+
70+
diag.multipart_suggestion(msg, suggestions, Applicability::MachineApplicable);
71+
} else {
72+
diag.span_help(expr.span, msg);
73+
}
74+
},
75+
);
76+
}
77+
}
78+
}

clippy_utils/src/sym.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ generate! {
133133
cycle,
134134
cyclomatic_complexity,
135135
de,
136+
debug_assert,
136137
deprecated_in_future,
137138
diagnostics,
138139
disallowed_types,

tests/ui/manual_assert_eq.fixed

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//@aux-build:proc_macros.rs
2+
#![warn(clippy::manual_assert_eq)]
3+
#![allow(clippy::manual_ignore_case_cmp)] // only raised before the fix
4+
#![expect(clippy::eq_op, clippy::assertions_on_constants)]
5+
6+
fn main() {
7+
assert_eq!("a", "a".to_ascii_lowercase());
8+
//~^ manual_assert_eq
9+
assert_eq!("a", "a".to_ascii_lowercase(), "a==a");
10+
//~^ manual_assert_eq
11+
assert_ne!("A", "A".to_ascii_lowercase());
12+
//~^ manual_assert_eq
13+
assert_ne!("A", "A".to_ascii_lowercase(), "A!=a");
14+
//~^ manual_assert_eq
15+
16+
let v = vec![];
17+
assert_eq!(v, vec![1, 2, 3], "v!=vec");
18+
//~^ manual_assert_eq
19+
assert_eq!(vec![1, 2, 3], v, "v!=vec");
20+
//~^ manual_assert_eq
21+
assert_eq!(vec![1], vec![1, 2, 3], "v!=vec");
22+
//~^ manual_assert_eq
23+
24+
// Don't lint: in const context
25+
const {
26+
assert!(5 == 2 + 3);
27+
}
28+
29+
// Don't lint: in external macro
30+
{
31+
// NOTE: this only works because `root_macro_call_first_node` returns `external!`,
32+
// which then gets rejected by the macro name check
33+
proc_macros::external!(assert!('a' == 'b'));
34+
proc_macros::external!({
35+
let some_padding_before = 'a';
36+
assert!('a' == 'b');
37+
let some_padding_after = 'b';
38+
});
39+
40+
// .. which also means that the following is _technically_ a FN -- but surely no one would write
41+
// code like this (diverging/unit expression as a child expression of a macro call)
42+
vec![(), assert!('a' == 'b'), ()];
43+
}
44+
}
45+
46+
// Don't lint: in const context
47+
const _: () = {
48+
assert!(8 == (7 + 1));
49+
};

tests/ui/manual_assert_eq.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//@aux-build:proc_macros.rs
2+
#![warn(clippy::manual_assert_eq)]
3+
#![allow(clippy::manual_ignore_case_cmp)] // only raised before the fix
4+
#![expect(clippy::eq_op, clippy::assertions_on_constants)]
5+
6+
fn main() {
7+
assert!("a" == "a".to_ascii_lowercase());
8+
//~^ manual_assert_eq
9+
assert!("a" == "a".to_ascii_lowercase(), "a==a");
10+
//~^ manual_assert_eq
11+
assert!("A" != "A".to_ascii_lowercase());
12+
//~^ manual_assert_eq
13+
assert!("A" != "A".to_ascii_lowercase(), "A!=a");
14+
//~^ manual_assert_eq
15+
16+
let v = vec![];
17+
assert!(v == vec![1, 2, 3], "v!=vec");
18+
//~^ manual_assert_eq
19+
assert!(vec![1, 2, 3] == v, "v!=vec");
20+
//~^ manual_assert_eq
21+
assert!(vec![1] == vec![1, 2, 3], "v!=vec");
22+
//~^ manual_assert_eq
23+
24+
// Don't lint: in const context
25+
const {
26+
assert!(5 == 2 + 3);
27+
}
28+
29+
// Don't lint: in external macro
30+
{
31+
// NOTE: this only works because `root_macro_call_first_node` returns `external!`,
32+
// which then gets rejected by the macro name check
33+
proc_macros::external!(assert!('a' == 'b'));
34+
proc_macros::external!({
35+
let some_padding_before = 'a';
36+
assert!('a' == 'b');
37+
let some_padding_after = 'b';
38+
});
39+
40+
// .. which also means that the following is _technically_ a FN -- but surely no one would write
41+
// code like this (diverging/unit expression as a child expression of a macro call)
42+
vec![(), assert!('a' == 'b'), ()];
43+
}
44+
}
45+
46+
// Don't lint: in const context
47+
const _: () = {
48+
assert!(8 == (7 + 1));
49+
};

tests/ui/manual_assert_eq.stderr

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
error: used `assert!` with an equality comparison
2+
--> tests/ui/manual_assert_eq.rs:7:5
3+
|
4+
LL | assert!("a" == "a".to_ascii_lowercase());
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: `-D clippy::manual-assert-eq` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::manual_assert_eq)]`
9+
help: replace it with `assert_eq!(..)`
10+
|
11+
LL - assert!("a" == "a".to_ascii_lowercase());
12+
LL + assert_eq!("a", "a".to_ascii_lowercase());
13+
|
14+
15+
error: used `assert!` with an equality comparison
16+
--> tests/ui/manual_assert_eq.rs:9:5
17+
|
18+
LL | assert!("a" == "a".to_ascii_lowercase(), "a==a");
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
20+
|
21+
help: replace it with `assert_eq!(..)`
22+
|
23+
LL - assert!("a" == "a".to_ascii_lowercase(), "a==a");
24+
LL + assert_eq!("a", "a".to_ascii_lowercase(), "a==a");
25+
|
26+
27+
error: used `assert!` with an equality comparison
28+
--> tests/ui/manual_assert_eq.rs:11:5
29+
|
30+
LL | assert!("A" != "A".to_ascii_lowercase());
31+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
32+
|
33+
help: replace it with `assert_ne!(..)`
34+
|
35+
LL - assert!("A" != "A".to_ascii_lowercase());
36+
LL + assert_ne!("A", "A".to_ascii_lowercase());
37+
|
38+
39+
error: used `assert!` with an equality comparison
40+
--> tests/ui/manual_assert_eq.rs:13:5
41+
|
42+
LL | assert!("A" != "A".to_ascii_lowercase(), "A!=a");
43+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44+
|
45+
help: replace it with `assert_ne!(..)`
46+
|
47+
LL - assert!("A" != "A".to_ascii_lowercase(), "A!=a");
48+
LL + assert_ne!("A", "A".to_ascii_lowercase(), "A!=a");
49+
|
50+
51+
error: used `assert!` with an equality comparison
52+
--> tests/ui/manual_assert_eq.rs:17:5
53+
|
54+
LL | assert!(v == vec![1, 2, 3], "v!=vec");
55+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
56+
|
57+
help: replace it with `assert_eq!(..)`
58+
|
59+
LL - assert!(v == vec![1, 2, 3], "v!=vec");
60+
LL + assert_eq!(v, vec![1, 2, 3], "v!=vec");
61+
|
62+
63+
error: used `assert!` with an equality comparison
64+
--> tests/ui/manual_assert_eq.rs:19:5
65+
|
66+
LL | assert!(vec![1, 2, 3] == v, "v!=vec");
67+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
68+
|
69+
help: replace it with `assert_eq!(..)`
70+
|
71+
LL - assert!(vec![1, 2, 3] == v, "v!=vec");
72+
LL + assert_eq!(vec![1, 2, 3], v, "v!=vec");
73+
|
74+
75+
error: used `assert!` with an equality comparison
76+
--> tests/ui/manual_assert_eq.rs:21:5
77+
|
78+
LL | assert!(vec![1] == vec![1, 2, 3], "v!=vec");
79+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80+
|
81+
help: replace it with `assert_eq!(..)`
82+
|
83+
LL - assert!(vec![1] == vec![1, 2, 3], "v!=vec");
84+
LL + assert_eq!(vec![1], vec![1, 2, 3], "v!=vec");
85+
|
86+
87+
error: aborting due to 7 previous errors
88+

tests/ui/unnecessary_map_or.fixed

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,12 @@ fn issue14201(a: Option<String>, b: Option<String>, s: &String) -> bool {
132132
}
133133

134134
fn issue14714() {
135-
assert!(Some("test") == Some("test"));
136-
//~^ unnecessary_map_or
135+
#[allow(clippy::manual_assert_eq, reason = "will fire after the fix")]
136+
// blocks are needed because attributes (here, the `allow`) on macro calls are ignored
137+
{
138+
assert!(Some("test") == Some("test"));
139+
//~^ unnecessary_map_or
140+
};
137141

138142
// even though we're in a macro context, we still need to parenthesise because of the `then`
139143
assert!((Some("test") == Some("test")).then(|| 1).is_some());

tests/ui/unnecessary_map_or.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,12 @@ fn issue14201(a: Option<String>, b: Option<String>, s: &String) -> bool {
136136
}
137137

138138
fn issue14714() {
139-
assert!(Some("test").map_or(false, |x| x == "test"));
140-
//~^ unnecessary_map_or
139+
#[allow(clippy::manual_assert_eq, reason = "will fire after the fix")]
140+
// blocks are needed because attributes (here, the `allow`) on macro calls are ignored
141+
{
142+
assert!(Some("test").map_or(false, |x| x == "test"));
143+
//~^ unnecessary_map_or
144+
};
141145

142146
// even though we're in a macro context, we still need to parenthesise because of the `then`
143147
assert!(Some("test").map_or(false, |x| x == "test").then(|| 1).is_some());

0 commit comments

Comments
 (0)