wowlab_tidy/languages/rust/rules/hygiene/
missing_assert_message.rs1#[cfg(test)]
2use googletest::prelude::*;
3use ra_ap_syntax::{AstNode, ast};
4
5use crate::{AstCtx, Example, Violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9 Example {
10 label: "assert without message",
11 code: "fn f() { assert!(true); }",
12 pass: false,
13 },
14 Example {
15 label: "assert with message",
16 code: "fn f() { assert!(true, \"reason\"); }",
17 pass: true,
18 },
19 Example {
20 label: "assert_eq without message",
21 code: "fn f() { assert_eq!(1, 2); }",
22 pass: false,
23 },
24 Example {
25 label: "assert_eq with message",
26 code: "fn f() { assert_eq!(1, 2, \"values differ\"); }",
27 pass: true,
28 },
29 Example {
30 label: "assert_ne without message",
31 code: "fn f() { assert_ne!(1, 1); }",
32 pass: false,
33 },
34 Example {
35 label: "assert_ne with message",
36 code: "fn f() { assert_ne!(1, 1, \"should differ\"); }",
37 pass: true,
38 },
39 Example {
40 label: "assert in test module",
41 code: "#[cfg(test)]\nmod tests {\n fn t() { assert!(true); }\n}",
42 pass: true,
43 },
44];
45
46crate::ast_rule!(
47 missing_assert_message,
48 "Require a message argument on `assert!`, `assert_eq!`, `assert_ne!`.",
49 "Assertions without messages produce opaque failures. A message explains what invariant was violated.",
50);
51
52const COMPARISON_ASSERT_ARGUMENTS: usize = 2;
53
54fn check_missing_assert_message(ctx: &AstCtx<'_>) -> Vec<Violation> {
55 ctx.nodes::<ast::MacroCall>()
56 .filter(|call| !ctx.is_in_test(call))
57 .filter_map(|call| {
58 let name = macro_name(&call)?;
59 let needed = match name.as_str() {
60 "assert" => 1,
61 "assert_eq" | "assert_ne" => COMPARISON_ASSERT_ARGUMENTS,
62 _ => return None,
63 };
64
65 (top_level_comma_count(&call) < needed).then(|| {
66 ctx.violation(
67 &call,
68 format!("{name}!() without a message — add a description of what failed"),
69 )
70 })
71 })
72 .collect()
73}
74
75fn macro_name(call: &ast::MacroCall) -> Option<String> {
76 call.path()?
77 .segment()?
78 .name_ref()
79 .map(|name| name.text().to_string())
80}
81
82fn top_level_comma_count(call: &ast::MacroCall) -> usize {
83 call.token_tree()
84 .into_iter()
85 .flat_map(|tree| tree.syntax().children_with_tokens())
86 .filter_map(ra_ap_syntax::NodeOrToken::into_token)
87 .filter(|token| token.text() == ",")
88 .count()
89}
90
91crate::tidy_ast_test!(check_missing_assert_message, {
92 crate::example_tests!(EXAMPLES, check_missing_assert_message);
93
94 #[gtest]
95 fn nested_commas_not_counted() -> Result<()> {
96 let v = run("fn f() { assert!(vec![1, 2].contains(&1)); }");
97 verify_eq!(v.len(), 1)?;
98
99 Ok(())
100 }
101});