Skip to content

Commit 216aefb

Browse files
committed
Auto merge of rust-lang#10362 - unexge:missing-assert-message-lint, r=llogiq
Add `missing_assert_message` lint Fixes rust-lang/rust-clippy#6207. changelog: new lint: [`missing_assert_message`]: A new lint for checking assertions that doesn't have a custom panic message. [rust-lang#10362](rust-lang/rust-clippy#10362) <!-- changelog_checked --> r? `@llogiq`
2 parents 41fa24c + b554ff4 commit 216aefb

File tree

7 files changed

+333
-10
lines changed

7 files changed

+333
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4706,6 +4706,7 @@ Released 2018-09-13
47064706
[`mismatching_type_param_order`]: https://rust-lang.github.io/rust-clippy/master/index.html#mismatching_type_param_order
47074707
[`misnamed_getters`]: https://rust-lang.github.io/rust-clippy/master/index.html#misnamed_getters
47084708
[`misrefactored_assign_op`]: https://rust-lang.github.io/rust-clippy/master/index.html#misrefactored_assign_op
4709+
[`missing_assert_message`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_assert_message
47094710
[`missing_const_for_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn
47104711
[`missing_docs_in_private_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_docs_in_private_items
47114712
[`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
@@ -417,6 +417,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
417417
crate::misc_early::UNSEPARATED_LITERAL_SUFFIX_INFO,
418418
crate::misc_early::ZERO_PREFIXED_LITERAL_INFO,
419419
crate::mismatching_type_param_order::MISMATCHING_TYPE_PARAM_ORDER_INFO,
420+
crate::missing_assert_message::MISSING_ASSERT_MESSAGE_INFO,
420421
crate::missing_const_for_fn::MISSING_CONST_FOR_FN_INFO,
421422
crate::missing_doc::MISSING_DOCS_IN_PRIVATE_ITEMS_INFO,
422423
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
@@ -193,6 +193,7 @@ mod minmax;
193193
mod misc;
194194
mod misc_early;
195195
mod mismatching_type_param_order;
196+
mod missing_assert_message;
196197
mod missing_const_for_fn;
197198
mod missing_doc;
198199
mod missing_enforced_import_rename;
@@ -926,6 +927,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
926927
});
927928
store.register_late_pass(|_| Box::new(no_mangle_with_rust_abi::NoMangleWithRustAbi));
928929
store.register_late_pass(|_| Box::new(collection_is_never_read::CollectionIsNeverRead));
930+
store.register_late_pass(|_| Box::new(missing_assert_message::MissingAssertMessage));
929931
// add lints here, do not remove this comment, it's used in `new_lint`
930932
}
931933

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
use clippy_utils::diagnostics::span_lint_and_help;
2+
use clippy_utils::macros::{find_assert_args, find_assert_eq_args, root_macro_call_first_node, PanicExpn};
3+
use clippy_utils::{is_in_cfg_test, is_in_test_function};
4+
use rustc_hir::Expr;
5+
use rustc_lint::{LateContext, LateLintPass};
6+
use rustc_session::{declare_lint_pass, declare_tool_lint};
7+
use rustc_span::sym;
8+
9+
declare_clippy_lint! {
10+
/// ### What it does
11+
/// Checks assertions without a custom panic message.
12+
///
13+
/// ### Why is this bad?
14+
/// Without a good custom message, it'd be hard to understand what went wrong when the assertion fails.
15+
/// A good custom message should be more about why the failure of the assertion is problematic
16+
/// and not what is failed because the assertion already conveys that.
17+
///
18+
/// ### Known problems
19+
/// This lint cannot check the quality of the custom panic messages.
20+
/// Hence, you can suppress this lint simply by adding placeholder messages
21+
/// like "assertion failed". However, we recommend coming up with good messages
22+
/// that provide useful information instead of placeholder messages that
23+
/// don't provide any extra information.
24+
///
25+
/// ### Example
26+
/// ```rust
27+
/// # struct Service { ready: bool }
28+
/// fn call(service: Service) {
29+
/// assert!(service.ready);
30+
/// }
31+
/// ```
32+
/// Use instead:
33+
/// ```rust
34+
/// # struct Service { ready: bool }
35+
/// fn call(service: Service) {
36+
/// assert!(service.ready, "`service.poll_ready()` must be called first to ensure that service is ready to receive requests");
37+
/// }
38+
/// ```
39+
#[clippy::version = "1.69.0"]
40+
pub MISSING_ASSERT_MESSAGE,
41+
restriction,
42+
"checks assertions without a custom panic message"
43+
}
44+
45+
declare_lint_pass!(MissingAssertMessage => [MISSING_ASSERT_MESSAGE]);
46+
47+
impl<'tcx> LateLintPass<'tcx> for MissingAssertMessage {
48+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
49+
let Some(macro_call) = root_macro_call_first_node(cx, expr) else { return };
50+
let single_argument = match cx.tcx.get_diagnostic_name(macro_call.def_id) {
51+
Some(sym::assert_macro | sym::debug_assert_macro) => true,
52+
Some(
53+
sym::assert_eq_macro | sym::assert_ne_macro | sym::debug_assert_eq_macro | sym::debug_assert_ne_macro,
54+
) => false,
55+
_ => return,
56+
};
57+
58+
// This lint would be very noisy in tests, so just ignore if we're in test context
59+
if is_in_test_function(cx.tcx, expr.hir_id) || is_in_cfg_test(cx.tcx, expr.hir_id) {
60+
return;
61+
}
62+
63+
let panic_expn = if single_argument {
64+
let Some((_, panic_expn)) = find_assert_args(cx, expr, macro_call.expn) else { return };
65+
panic_expn
66+
} else {
67+
let Some((_, _, panic_expn)) = find_assert_eq_args(cx, expr, macro_call.expn) else { return };
68+
panic_expn
69+
};
70+
71+
if let PanicExpn::Empty = panic_expn {
72+
span_lint_and_help(
73+
cx,
74+
MISSING_ASSERT_MESSAGE,
75+
macro_call.span,
76+
"assert without any message",
77+
None,
78+
"consider describing why the failing assert is problematic",
79+
);
80+
}
81+
}
82+
}

clippy_utils/src/macros.rs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ pub fn is_assert_macro(cx: &LateContext<'_>, def_id: DefId) -> bool {
213213
matches!(name, sym::assert_macro | sym::debug_assert_macro)
214214
}
215215

216+
#[derive(Debug)]
216217
pub enum PanicExpn<'a> {
217218
/// No arguments - `panic!()`
218219
Empty,
@@ -226,10 +227,7 @@ pub enum PanicExpn<'a> {
226227

227228
impl<'a> PanicExpn<'a> {
228229
pub fn parse(cx: &LateContext<'_>, expr: &'a Expr<'a>) -> Option<Self> {
229-
if !macro_backtrace(expr.span).any(|macro_call| is_panic(cx, macro_call.def_id)) {
230-
return None;
231-
}
232-
let ExprKind::Call(callee, [arg]) = &expr.kind else { return None };
230+
let ExprKind::Call(callee, [arg, rest @ ..]) = &expr.kind else { return None };
233231
let ExprKind::Path(QPath::Resolved(_, path)) = &callee.kind else { return None };
234232
let result = match path.segments.last().unwrap().ident.as_str() {
235233
"panic" if arg.span.ctxt() == expr.span.ctxt() => Self::Empty,
@@ -239,6 +237,21 @@ impl<'a> PanicExpn<'a> {
239237
Self::Display(e)
240238
},
241239
"panic_fmt" => Self::Format(FormatArgsExpn::parse(cx, arg)?),
240+
// Since Rust 1.52, `assert_{eq,ne}` macros expand to use:
241+
// `core::panicking::assert_failed(.., left_val, right_val, None | Some(format_args!(..)));`
242+
"assert_failed" => {
243+
// It should have 4 arguments in total (we already matched with the first argument,
244+
// so we're just checking for 3)
245+
if rest.len() != 3 {
246+
return None;
247+
}
248+
// `msg_arg` is either `None` (no custom message) or `Some(format_args!(..))` (custom message)
249+
let msg_arg = &rest[2];
250+
match msg_arg.kind {
251+
ExprKind::Call(_, [fmt_arg]) => Self::Format(FormatArgsExpn::parse(cx, fmt_arg)?),
252+
_ => Self::Empty,
253+
}
254+
},
242255
_ => return None,
243256
};
244257
Some(result)
@@ -251,7 +264,17 @@ pub fn find_assert_args<'a>(
251264
expr: &'a Expr<'a>,
252265
expn: ExpnId,
253266
) -> Option<(&'a Expr<'a>, PanicExpn<'a>)> {
254-
find_assert_args_inner(cx, expr, expn).map(|([e], p)| (e, p))
267+
find_assert_args_inner(cx, expr, expn).map(|([e], mut p)| {
268+
// `assert!(..)` expands to `core::panicking::panic("assertion failed: ...")` (which we map to
269+
// `PanicExpn::Str(..)`) and `assert!(.., "..")` expands to
270+
// `core::panicking::panic_fmt(format_args!(".."))` (which we map to `PanicExpn::Format(..)`).
271+
// So even we got `PanicExpn::Str(..)` that means there is no custom message provided
272+
if let PanicExpn::Str(_) = p {
273+
p = PanicExpn::Empty;
274+
}
275+
276+
(e, p)
277+
})
255278
}
256279

257280
/// Finds the arguments of an `assert_eq!` or `debug_assert_eq!` macro call within the macro
@@ -275,13 +298,12 @@ fn find_assert_args_inner<'a, const N: usize>(
275298
Some(inner_name) => find_assert_within_debug_assert(cx, expr, expn, Symbol::intern(inner_name))?,
276299
};
277300
let mut args = ArrayVec::new();
278-
let mut panic_expn = None;
279-
let _: Option<!> = for_each_expr(expr, |e| {
301+
let panic_expn = for_each_expr(expr, |e| {
280302
if args.is_full() {
281-
if panic_expn.is_none() && e.span.ctxt() != expr.span.ctxt() {
282-
panic_expn = PanicExpn::parse(cx, e);
303+
match PanicExpn::parse(cx, e) {
304+
Some(expn) => ControlFlow::Break(expn),
305+
None => ControlFlow::Continue(Descend::Yes),
283306
}
284-
ControlFlow::Continue(Descend::from(panic_expn.is_none()))
285307
} else if is_assert_arg(cx, e, expn) {
286308
args.push(e);
287309
ControlFlow::Continue(Descend::No)

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+
}

0 commit comments

Comments
 (0)