Skip to main content

wowlab_tidy/languages/rust/rules/hygiene/
missing_assert_message.rs

1#[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});