Skip to content

Commit f9e133a

Browse files
unexgeweihanglo
andcommitted
Add missing_assert_message lint
Co-authored-by: Weihang Lo <me@weihanglo.tw>
1 parent 5b6795f commit f9e133a

File tree

8 files changed

+313
-2
lines changed

8 files changed

+313
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4558,6 +4558,7 @@ Released 2018-09-13
45584558
[`mismatching_type_param_order`]: https://rust-lang.github.io/rust-clippy/master/index.html#mismatching_type_param_order
45594559
[`misnamed_getters`]: https://rust-lang.github.io/rust-clippy/master/index.html#misnamed_getters
45604560
[`misrefactored_assign_op`]: https://rust-lang.github.io/rust-clippy/master/index.html#misrefactored_assign_op
4561+
[`missing_assert_message`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_assert_message
45614562
[`missing_const_for_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn
45624563
[`missing_docs_in_private_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_docs_in_private_items
45634564
[`missing_enforced_import_renames`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_enforced_import_renames

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
414414
crate::misc_early::UNSEPARATED_LITERAL_SUFFIX_INFO,
415415
crate::misc_early::ZERO_PREFIXED_LITERAL_INFO,
416416
crate::mismatching_type_param_order::MISMATCHING_TYPE_PARAM_ORDER_INFO,
417+
crate::missing_assert_message::MISSING_ASSERT_MESSAGE_INFO,
417418
crate::missing_const_for_fn::MISSING_CONST_FOR_FN_INFO,
418419
crate::missing_doc::MISSING_DOCS_IN_PRIVATE_ITEMS_INFO,
419420
crate::missing_enforced_import_rename::MISSING_ENFORCED_IMPORT_RENAMES_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ mod minmax;
190190
mod misc;
191191
mod misc_early;
192192
mod mismatching_type_param_order;
193+
mod missing_assert_message;
193194
mod missing_const_for_fn;
194195
mod missing_doc;
195196
mod missing_enforced_import_rename;
@@ -911,6 +912,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
911912
store.register_late_pass(|_| Box::new(size_of_ref::SizeOfRef));
912913
store.register_late_pass(|_| Box::new(multiple_unsafe_ops_per_block::MultipleUnsafeOpsPerBlock));
913914
store.register_late_pass(|_| Box::new(extra_unused_type_parameters::ExtraUnusedTypeParameters));
915+
store.register_pre_expansion_pass(|| Box::new(missing_assert_message::MissingAssertMessage));
914916
// add lints here, do not remove this comment, it's used in `new_lint`
915917
}
916918

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use clippy_utils::diagnostics::span_lint;
2+
use rustc_ast::ast;
3+
use rustc_ast::{
4+
token::{Token, TokenKind},
5+
tokenstream::TokenTree,
6+
};
7+
use rustc_lint::{EarlyContext, EarlyLintPass};
8+
use rustc_session::{declare_tool_lint, impl_lint_pass};
9+
use rustc_span::sym;
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
/// Checks assertions that doesn't have a custom panic message.
14+
///
15+
/// ### Why is this bad?
16+
/// If the assertion fails, a custom message may make it easier to debug what went wrong.
17+
///
18+
/// ### Example
19+
/// ```rust
20+
/// let threshold = 50;
21+
/// let num = 42;
22+
/// assert!(num < threshold);
23+
/// ```
24+
/// Use instead:
25+
/// ```rust
26+
/// let threshold = 50;
27+
/// let num = 42;
28+
/// assert!(num < threshold, "{num} is lower than threshold ({threshold})");
29+
/// ```
30+
#[clippy::version = "1.69.0"]
31+
pub MISSING_ASSERT_MESSAGE,
32+
pedantic,
33+
"checks assertions that doesn't have a custom panic message"
34+
}
35+
36+
#[derive(Default, Clone, Debug)]
37+
pub struct MissingAssertMessage {
38+
// This field will be greater than zero if we are inside a `#[test]` or `#[cfg(test)]`
39+
test_deepnes: usize,
40+
}
41+
42+
impl_lint_pass!(MissingAssertMessage => [MISSING_ASSERT_MESSAGE]);
43+
44+
impl EarlyLintPass for MissingAssertMessage {
45+
fn check_mac(&mut self, cx: &EarlyContext<'_>, mac_call: &ast::MacCall) {
46+
if self.test_deepnes != 0 {
47+
return;
48+
}
49+
50+
let Some(last_segment) = mac_call.path.segments.last() else { return; };
51+
let num_separators_needed = match last_segment.ident.as_str() {
52+
"assert" | "debug_assert" => 1,
53+
"assert_eq" | "assert_ne" | "debug_assert_eq" | "debug_assert_ne" => 2,
54+
_ => return,
55+
};
56+
let num_separators = num_commas_on_arguments(mac_call);
57+
58+
if num_separators < num_separators_needed {
59+
span_lint(
60+
cx,
61+
MISSING_ASSERT_MESSAGE,
62+
mac_call.span(),
63+
"assert without any message",
64+
);
65+
}
66+
}
67+
68+
fn check_item(&mut self, _: &EarlyContext<'_>, item: &ast::Item) {
69+
if item.attrs.iter().any(is_a_test_attribute) {
70+
self.test_deepnes += 1;
71+
}
72+
}
73+
74+
fn check_item_post(&mut self, _: &EarlyContext<'_>, item: &ast::Item) {
75+
if item.attrs.iter().any(is_a_test_attribute) {
76+
self.test_deepnes -= 1;
77+
}
78+
}
79+
}
80+
81+
// Returns number of commas (excluding trailing comma) from `MacCall`'s arguments.
82+
fn num_commas_on_arguments(mac_call: &ast::MacCall) -> usize {
83+
let mut num_separators = 0;
84+
let mut is_trailing = false;
85+
for tt in mac_call.args.tokens.trees() {
86+
match tt {
87+
TokenTree::Token(
88+
Token {
89+
kind: TokenKind::Comma,
90+
span: _,
91+
},
92+
_,
93+
) => {
94+
num_separators += 1;
95+
is_trailing = true;
96+
},
97+
_ => {
98+
is_trailing = false;
99+
},
100+
}
101+
}
102+
if is_trailing {
103+
num_separators -= 1;
104+
}
105+
num_separators
106+
}
107+
108+
// Returns true if the attribute is either a `#[test]` or a `#[cfg(test)]`.
109+
fn is_a_test_attribute(attr: &ast::Attribute) -> bool {
110+
if attr.has_name(sym::test) {
111+
return true;
112+
}
113+
114+
if attr.has_name(sym::cfg)
115+
&& let Some(items) = attr.meta_item_list()
116+
&& let [item] = &*items
117+
&& item.has_name(sym::test)
118+
{
119+
true
120+
} else {
121+
false
122+
}
123+
}

tests/ui/filter_map_next_fixable.fixed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// run-rustfix
22

33
#![warn(clippy::all, clippy::pedantic)]
4-
#![allow(unused)]
4+
#![allow(unused, clippy::missing_assert_message)]
55

66
fn main() {
77
let a = ["1", "lol", "3", "NaN", "5"];

tests/ui/filter_map_next_fixable.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// run-rustfix
22

33
#![warn(clippy::all, clippy::pedantic)]
4-
#![allow(unused)]
4+
#![allow(unused, clippy::missing_assert_message)]
55

66
fn main() {
77
let a = ["1", "lol", "3", "NaN", "5"];

tests/ui/missing_assert_message.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#![allow(unused)]
2+
#![warn(clippy::missing_assert_message)]
3+
4+
macro_rules! bar {
5+
($( $x:expr ),*) => {
6+
foo()
7+
};
8+
}
9+
10+
fn main() {}
11+
12+
// Should trigger warning
13+
fn asserts_without_message() {
14+
assert!(foo());
15+
assert_eq!(foo(), foo());
16+
assert_ne!(foo(), foo());
17+
debug_assert!(foo());
18+
debug_assert_eq!(foo(), foo());
19+
debug_assert_ne!(foo(), foo());
20+
}
21+
22+
// Should trigger warning
23+
fn asserts_without_message_but_with_macro_calls() {
24+
assert!(bar!(true));
25+
assert!(bar!(true, false));
26+
assert_eq!(bar!(true), foo());
27+
assert_ne!(bar!(true, true), bar!(true));
28+
}
29+
30+
// Should trigger warning
31+
fn asserts_with_trailing_commas() {
32+
assert!(foo(),);
33+
assert_eq!(foo(), foo(),);
34+
assert_ne!(foo(), foo(),);
35+
debug_assert!(foo(),);
36+
debug_assert_eq!(foo(), foo(),);
37+
debug_assert_ne!(foo(), foo(),);
38+
}
39+
40+
// Should not trigger warning
41+
fn asserts_with_message_and_with_macro_calls() {
42+
assert!(bar!(true), "msg");
43+
assert!(bar!(true, false), "msg");
44+
assert_eq!(bar!(true), foo(), "msg");
45+
assert_ne!(bar!(true, true), bar!(true), "msg");
46+
}
47+
48+
// Should not trigger warning
49+
fn asserts_with_message() {
50+
assert!(foo(), "msg");
51+
assert_eq!(foo(), foo(), "msg");
52+
assert_ne!(foo(), foo(), "msg");
53+
debug_assert!(foo(), "msg");
54+
debug_assert_eq!(foo(), foo(), "msg");
55+
debug_assert_ne!(foo(), foo(), "msg");
56+
}
57+
58+
// Should not trigger warning
59+
#[test]
60+
fn asserts_without_message_but_inside_a_test_function() {
61+
assert!(foo());
62+
assert_eq!(foo(), foo());
63+
assert_ne!(foo(), foo());
64+
debug_assert!(foo());
65+
debug_assert_eq!(foo(), foo());
66+
debug_assert_ne!(foo(), foo());
67+
}
68+
69+
// Should not trigger warning
70+
#[cfg(test)]
71+
mod tests {
72+
fn asserts_without_message_but_inside_a_test_module() {
73+
assert!(foo());
74+
assert_eq!(foo(), foo());
75+
assert_ne!(foo(), foo());
76+
debug_assert!(foo());
77+
debug_assert_eq!(foo(), foo());
78+
debug_assert_ne!(foo(), foo());
79+
}
80+
}
81+
82+
fn foo() -> bool {
83+
true
84+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
error: assert without any message
2+
--> $DIR/missing_assert_message.rs:14:5
3+
|
4+
LL | assert!(foo());
5+
| ^^^^^^^^^^^^^^
6+
|
7+
= note: `-D clippy::missing-assert-message` implied by `-D warnings`
8+
9+
error: assert without any message
10+
--> $DIR/missing_assert_message.rs:15:5
11+
|
12+
LL | assert_eq!(foo(), foo());
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^
14+
15+
error: assert without any message
16+
--> $DIR/missing_assert_message.rs:16:5
17+
|
18+
LL | assert_ne!(foo(), foo());
19+
| ^^^^^^^^^^^^^^^^^^^^^^^^
20+
21+
error: assert without any message
22+
--> $DIR/missing_assert_message.rs:17:5
23+
|
24+
LL | debug_assert!(foo());
25+
| ^^^^^^^^^^^^^^^^^^^^
26+
27+
error: assert without any message
28+
--> $DIR/missing_assert_message.rs:18:5
29+
|
30+
LL | debug_assert_eq!(foo(), foo());
31+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
32+
33+
error: assert without any message
34+
--> $DIR/missing_assert_message.rs:19:5
35+
|
36+
LL | debug_assert_ne!(foo(), foo());
37+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
38+
39+
error: assert without any message
40+
--> $DIR/missing_assert_message.rs:24:5
41+
|
42+
LL | assert!(bar!(true));
43+
| ^^^^^^^^^^^^^^^^^^^
44+
45+
error: assert without any message
46+
--> $DIR/missing_assert_message.rs:25:5
47+
|
48+
LL | assert!(bar!(true, false));
49+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
50+
51+
error: assert without any message
52+
--> $DIR/missing_assert_message.rs:26:5
53+
|
54+
LL | assert_eq!(bar!(true), foo());
55+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
56+
57+
error: assert without any message
58+
--> $DIR/missing_assert_message.rs:27:5
59+
|
60+
LL | assert_ne!(bar!(true, true), bar!(true));
61+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62+
63+
error: assert without any message
64+
--> $DIR/missing_assert_message.rs:32:5
65+
|
66+
LL | assert!(foo(),);
67+
| ^^^^^^^^^^^^^^^
68+
69+
error: assert without any message
70+
--> $DIR/missing_assert_message.rs:33:5
71+
|
72+
LL | assert_eq!(foo(), foo(),);
73+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
74+
75+
error: assert without any message
76+
--> $DIR/missing_assert_message.rs:34:5
77+
|
78+
LL | assert_ne!(foo(), foo(),);
79+
| ^^^^^^^^^^^^^^^^^^^^^^^^^
80+
81+
error: assert without any message
82+
--> $DIR/missing_assert_message.rs:35:5
83+
|
84+
LL | debug_assert!(foo(),);
85+
| ^^^^^^^^^^^^^^^^^^^^^
86+
87+
error: assert without any message
88+
--> $DIR/missing_assert_message.rs:36:5
89+
|
90+
LL | debug_assert_eq!(foo(), foo(),);
91+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
92+
93+
error: assert without any message
94+
--> $DIR/missing_assert_message.rs:37:5
95+
|
96+
LL | debug_assert_ne!(foo(), foo(),);
97+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
98+
99+
error: aborting due to 16 previous errors
100+

0 commit comments

Comments
 (0)