Skip to content

Commit 6b87ed6

Browse files
committed
feat(manual_assert_eq): new lint
1 parent ad7fc4f commit 6b87ed6

32 files changed

+573
-98
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/eta_reduction.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ fn has_late_bound_to_non_late_bound_regions(from_sig: FnSig<'_>, to_sig: FnSig<'
371371
}
372372
}
373373

374-
assert!(from_sig.inputs_and_output.len() == to_sig.inputs_and_output.len());
374+
assert_eq!(from_sig.inputs_and_output.len(), to_sig.inputs_and_output.len());
375375
from_sig
376376
.inputs_and_output
377377
.iter()

clippy_lints/src/functions/renamed_function_params.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ impl RenamedFnArgs {
5757
{
5858
let mut renamed: Vec<(Span, String)> = vec![];
5959

60-
debug_assert!(default_idents.size_hint() == current_idents.size_hint());
60+
debug_assert_eq!(default_idents.size_hint(), current_idents.size_hint());
6161
for (default_ident, current_ident) in iter::zip(default_idents, current_idents) {
6262
let has_name_to_check = |ident: Option<Ident>| {
6363
ident

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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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::ty::implements_trait;
5+
use clippy_utils::{is_in_const_context, sym};
6+
use rustc_errors::Applicability;
7+
use rustc_hir::{BinOpKind, Expr, ExprKind};
8+
use rustc_lint::{LateContext, LateLintPass, LintContext};
9+
use rustc_session::declare_lint_pass;
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
/// Checks for `assert!` and `debug_assert!` that consist of only an (in)equality check
14+
///
15+
/// ### Why is this bad?
16+
/// `assert_{eq,ne}!` and `debug_assert_{eq,ne}!` achieves the same goal, and provides some
17+
/// additional debug information
18+
///
19+
/// ### Example
20+
/// ```no_run
21+
/// assert!(2 * 2 == 4);
22+
/// assert!(2 * 2 != 5);
23+
/// debug_assert!(2 * 2 == 4);
24+
/// debug_assert!(2 * 2 != 5);
25+
/// ```
26+
/// Use instead:
27+
/// ```no_run
28+
/// assert_eq!(2 * 2, 4);
29+
/// assert_ne!(2 * 2, 5);
30+
/// debug_assert_eq!(2 * 2, 4);
31+
/// debug_assert_ne!(2 * 2, 5);
32+
/// ```
33+
#[clippy::version = "1.92.0"]
34+
pub MANUAL_ASSERT_EQ,
35+
complexity,
36+
"checks for assertions consisting of an (in)equality check"
37+
}
38+
declare_lint_pass!(ManualAssertEq => [MANUAL_ASSERT_EQ]);
39+
40+
impl LateLintPass<'_> for ManualAssertEq {
41+
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
42+
if let Some(macro_call) = root_macro_call_first_node(cx, expr)
43+
&& let macro_name = match cx.tcx.get_diagnostic_name(macro_call.def_id) {
44+
Some(sym::assert_macro) => "assert",
45+
Some(sym::debug_assert_macro) => "debug_assert",
46+
_ => return,
47+
}
48+
// `assert_eq` isn't allowed in const context because it calls non-const `core::panicking::assert_failed`
49+
// XXX: this might change in the future, so might want to relax this restriction
50+
&& !is_in_const_context(cx)
51+
&& let Some((cond, _)) = find_assert_args(cx, expr, macro_call.expn)
52+
&& let ExprKind::Binary(op, lhs, rhs) = cond.kind
53+
&& matches!(op.node, BinOpKind::Eq | BinOpKind::Ne)
54+
&& !cond.span.from_expansion()
55+
&& let Some(debug_trait) = cx.tcx.get_diagnostic_item(sym::Debug)
56+
&& implements_trait(cx, cx.typeck_results().expr_ty(lhs), debug_trait, &[])
57+
&& implements_trait(cx, cx.typeck_results().expr_ty(rhs), debug_trait, &[])
58+
{
59+
span_lint_and_then(
60+
cx,
61+
MANUAL_ASSERT_EQ,
62+
macro_call.span,
63+
format!("used `{macro_name}!` with an equality comparison"),
64+
|diag| {
65+
let kind = if op.node == BinOpKind::Eq { "eq" } else { "ne" };
66+
let new_name = format!("{macro_name}_{kind}");
67+
let msg = format!("replace it with `{new_name}!(..)`");
68+
69+
let ctxt = cond.span.ctxt();
70+
if let Some(lhs_span) = walk_span_to_context(lhs.span, ctxt)
71+
&& let Some(rhs_span) = walk_span_to_context(rhs.span, ctxt)
72+
{
73+
let macro_name_span = cx.sess().source_map().span_until_char(macro_call.span, '!');
74+
let eq_span = cond.span.with_lo(lhs_span.hi()).with_hi(rhs_span.lo());
75+
let suggestions = vec![
76+
(macro_name_span.shrink_to_hi(), format!("_{kind}")),
77+
(eq_span, ", ".to_string()),
78+
];
79+
80+
diag.multipart_suggestion(msg, suggestions, Applicability::MachineApplicable);
81+
} else {
82+
diag.span_help(expr.span, msg);
83+
}
84+
},
85+
);
86+
}
87+
}
88+
}

clippy_utils/src/ty/mod.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -915,14 +915,20 @@ fn assert_generic_args_match<'tcx>(tcx: TyCtxt<'tcx>, did: DefId, args: &[Generi
915915
.chain(&g.own_params)
916916
.map(|x| &x.kind);
917917

918-
assert!(
919-
count == args.len(),
920-
"wrong number of arguments for `{did:?}`: expected `{count}`, found {}\n\
918+
#[expect(
919+
clippy::manual_assert_eq,
920+
reason = "the message contains `assert_eq!`-like formatting itself"
921+
)]
922+
{
923+
assert!(
924+
count == args.len(),
925+
"wrong number of arguments for `{did:?}`: expected `{count}`, found {}\n\
921926
note: the expected arguments are: `[{}]`\n\
922927
the given arguments are: `{args:#?}`",
923-
args.len(),
924-
params.clone().map(ty::GenericParamDefKind::descr).format(", "),
925-
);
928+
args.len(),
929+
params.clone().map(ty::GenericParamDefKind::descr).format(", "),
930+
);
931+
}
926932

927933
if let Some((idx, (param, arg))) =
928934
params

lintcheck/src/output.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ fn print_stats(old_stats: HashMap<String, usize>, new_stats: HashMap<&String, us
228228

229229
// remove duplicates from both hashmaps
230230
for (k, v) in &same_in_both_hashmaps {
231-
assert!(old_stats_deduped.remove(k) == Some(*v));
232-
assert!(new_stats_deduped.remove(k) == Some(*v));
231+
assert_eq!(old_stats_deduped.remove(k), Some(*v));
232+
assert_eq!(new_stats_deduped.remove(k), Some(*v));
233233
}
234234

235235
println!("\nStats:");

tests/ui/assertions_on_constants.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,11 @@ fn main() {
5454
const _: () = assert!(true);
5555
//~^ assertions_on_constants
5656

57-
assert!(8 == (7 + 1));
58-
//~^ assertions_on_constants
57+
#[allow(clippy::manual_assert_eq, reason = "tests `assert!` specifically")]
58+
{
59+
assert!(8 == (7 + 1));
60+
//~^ assertions_on_constants
61+
}
5962

6063
// Don't lint if the value is dependent on a defined constant:
6164
const N: usize = 1024;
@@ -68,8 +71,11 @@ const _: () = {
6871
assert!(true);
6972
//~^ assertions_on_constants
7073

71-
assert!(8 == (7 + 1));
72-
//~^ assertions_on_constants
74+
#[allow(clippy::manual_assert_eq, reason = "tests `assert!` specifically")]
75+
{
76+
assert!(8 == (7 + 1));
77+
//~^ assertions_on_constants
78+
}
7379

7480
assert!(C);
7581
};

tests/ui/assertions_on_constants.stderr

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,47 +89,47 @@ LL | const _: () = assert!(true);
8989
= help: remove the assertion
9090

9191
error: this assertion is always `true`
92-
--> tests/ui/assertions_on_constants.rs:57:5
92+
--> tests/ui/assertions_on_constants.rs:59:9
9393
|
94-
LL | assert!(8 == (7 + 1));
95-
| ^^^^^^^^^^^^^^^^^^^^^
94+
LL | assert!(8 == (7 + 1));
95+
| ^^^^^^^^^^^^^^^^^^^^^
9696
|
9797
= help: remove the assertion
9898

9999
error: this assertion is always `true`
100-
--> tests/ui/assertions_on_constants.rs:68:5
100+
--> tests/ui/assertions_on_constants.rs:71:5
101101
|
102102
LL | assert!(true);
103103
| ^^^^^^^^^^^^^
104104
|
105105
= help: remove the assertion
106106

107107
error: this assertion is always `true`
108-
--> tests/ui/assertions_on_constants.rs:71:5
108+
--> tests/ui/assertions_on_constants.rs:76:9
109109
|
110-
LL | assert!(8 == (7 + 1));
111-
| ^^^^^^^^^^^^^^^^^^^^^
110+
LL | assert!(8 == (7 + 1));
111+
| ^^^^^^^^^^^^^^^^^^^^^
112112
|
113113
= help: remove the assertion
114114

115115
error: this assertion has a constant value
116-
--> tests/ui/assertions_on_constants.rs:79:5
116+
--> tests/ui/assertions_on_constants.rs:85:5
117117
|
118118
LL | assert!(C);
119119
| ^^^^^^^^^^
120120
|
121121
= help: consider moving this to an anonymous constant: `const _: () = { assert!(..); }`
122122

123123
error: this assertion has a constant value
124-
--> tests/ui/assertions_on_constants.rs:90:5
124+
--> tests/ui/assertions_on_constants.rs:96:5
125125
|
126126
LL | assert!(C);
127127
| ^^^^^^^^^^
128128
|
129129
= help: consider moving this into a const block: `const { assert!(..) }`
130130

131131
error: this assertion has a constant value
132-
--> tests/ui/assertions_on_constants.rs:96:5
132+
--> tests/ui/assertions_on_constants.rs:102:5
133133
|
134134
LL | assert!(C);
135135
| ^^^^^^^^^^

0 commit comments

Comments
 (0)