about summary refs log tree commit diff
path: root/clippy_lints/src/missing_assert_message.rs
diff options
context:
space:
mode:
authorBurak Varlı <burakvar@amazon.co.uk>2023-02-16 14:50:51 +0000
committerunexge <unexge@gmail.com>2023-03-08 08:50:21 +0000
commitea2547b8c63ce4e410c53e7fa55b127c81721454 (patch)
treea65810bf757d1bf63c092f44f667bfe267259de7 /clippy_lints/src/missing_assert_message.rs
parent41fa24cef89c71430040332b42036f4ec839c4e0 (diff)
downloadrust-ea2547b8c63ce4e410c53e7fa55b127c81721454.tar.gz
rust-ea2547b8c63ce4e410c53e7fa55b127c81721454.zip
Add `missing_assert_message` lint
Co-authored-by: Weihang Lo <me@weihanglo.tw>
Diffstat (limited to 'clippy_lints/src/missing_assert_message.rs')
-rw-r--r--clippy_lints/src/missing_assert_message.rs123
1 files changed, 123 insertions, 0 deletions
diff --git a/clippy_lints/src/missing_assert_message.rs b/clippy_lints/src/missing_assert_message.rs
new file mode 100644
index 00000000000..a884b22370d
--- /dev/null
+++ b/clippy_lints/src/missing_assert_message.rs
@@ -0,0 +1,123 @@
+use clippy_utils::diagnostics::span_lint;
+use rustc_ast::ast;
+use rustc_ast::{
+    token::{Token, TokenKind},
+    tokenstream::TokenTree,
+};
+use rustc_lint::{EarlyContext, EarlyLintPass};
+use rustc_session::{declare_tool_lint, impl_lint_pass};
+use rustc_span::sym;
+
+declare_clippy_lint! {
+    /// ### What it does
+    /// Checks assertions that doesn't have a custom panic message.
+    ///
+    /// ### Why is this bad?
+    /// If the assertion fails, a custom message may make it easier to debug what went wrong.
+    ///
+    /// ### Example
+    /// ```rust
+    /// let threshold = 50;
+    /// let num = 42;
+    /// assert!(num < threshold);
+    /// ```
+    /// Use instead:
+    /// ```rust
+    /// let threshold = 50;
+    /// let num = 42;
+    /// assert!(num < threshold, "{num} is lower than threshold ({threshold})");
+    /// ```
+    #[clippy::version = "1.69.0"]
+    pub MISSING_ASSERT_MESSAGE,
+    pedantic,
+    "checks assertions that doesn't have a custom panic message"
+}
+
+#[derive(Default, Clone, Debug)]
+pub struct MissingAssertMessage {
+    // This field will be greater than zero if we are inside a `#[test]` or `#[cfg(test)]`
+    test_deepnes: usize,
+}
+
+impl_lint_pass!(MissingAssertMessage => [MISSING_ASSERT_MESSAGE]);
+
+impl EarlyLintPass for MissingAssertMessage {
+    fn check_mac(&mut self, cx: &EarlyContext<'_>, mac_call: &ast::MacCall) {
+        if self.test_deepnes != 0 {
+            return;
+        }
+
+        let Some(last_segment) = mac_call.path.segments.last() else { return; };
+        let num_separators_needed = match last_segment.ident.as_str() {
+            "assert" | "debug_assert" => 1,
+            "assert_eq" | "assert_ne" | "debug_assert_eq" | "debug_assert_ne" => 2,
+            _ => return,
+        };
+        let num_separators = num_commas_on_arguments(mac_call);
+
+        if num_separators < num_separators_needed {
+            span_lint(
+                cx,
+                MISSING_ASSERT_MESSAGE,
+                mac_call.span(),
+                "assert without any message",
+            );
+        }
+    }
+
+    fn check_item(&mut self, _: &EarlyContext<'_>, item: &ast::Item) {
+        if item.attrs.iter().any(is_a_test_attribute) {
+            self.test_deepnes += 1;
+        }
+    }
+
+    fn check_item_post(&mut self, _: &EarlyContext<'_>, item: &ast::Item) {
+        if item.attrs.iter().any(is_a_test_attribute) {
+            self.test_deepnes -= 1;
+        }
+    }
+}
+
+// Returns number of commas (excluding trailing comma) from `MacCall`'s arguments.
+fn num_commas_on_arguments(mac_call: &ast::MacCall) -> usize {
+    let mut num_separators = 0;
+    let mut is_trailing = false;
+    for tt in mac_call.args.tokens.trees() {
+        match tt {
+            TokenTree::Token(
+                Token {
+                    kind: TokenKind::Comma,
+                    span: _,
+                },
+                _,
+            ) => {
+                num_separators += 1;
+                is_trailing = true;
+            },
+            _ => {
+                is_trailing = false;
+            },
+        }
+    }
+    if is_trailing {
+        num_separators -= 1;
+    }
+    num_separators
+}
+
+// Returns true if the attribute is either a `#[test]` or a `#[cfg(test)]`.
+fn is_a_test_attribute(attr: &ast::Attribute) -> bool {
+    if attr.has_name(sym::test) {
+        return true;
+    }
+
+    if attr.has_name(sym::cfg)
+        && let Some(items) = attr.meta_item_list()
+        && let [item] = &*items
+        && item.has_name(sym::test)
+    {
+        true
+    } else {
+        false
+    }
+}