| use clippy_utils::diagnostics::span_lint_and_then; |
| use clippy_utils::macros::{find_assert_args, root_macro_call_first_node}; |
| use clippy_utils::res::MaybeDef as _; |
| use clippy_utils::source::walk_span_to_context; |
| use clippy_utils::sugg::Sugg; |
| use clippy_utils::sym; |
| use clippy_utils::ty::implements_trait; |
| use rustc_errors::Applicability; |
| use rustc_hir::{Expr, ExprKind, LangItem, UnOp}; |
| use rustc_lint::{LateContext, LateLintPass, LintContext as _}; |
| use rustc_middle::ty::{self, Ty}; |
| use rustc_session::declare_lint_pass; |
| use rustc_span::Span; |
| |
| declare_clippy_lint! { |
| /// ### What it does |
| /// |
| /// Checks assertions that only test whether a supported value is empty. |
| /// |
| /// The lint handles `assert!` and `debug_assert!` calls on strings, slices, arrays, and `Vec`. |
| /// |
| /// ### Why is this bad? |
| /// |
| /// A boolean assertion only reports that the emptiness check failed. It does not show what the |
| /// asserted value contained. |
| /// |
| /// In CI or another remote test service, the failure output tells you that the value was |
| /// unexpectedly empty or non-empty, but not which values were present. The next step is often |
| /// to reproduce the failure locally, add temporary logging, or change the test so it exposes |
| /// the value. That extra investigation can be much slower than fixing the problem from the |
| /// original CI failure. |
| /// |
| /// The emptiness check also commonly appears before a deeper contents assertion: |
| /// |
| /// ```no_run |
| /// # let items = vec!["baz"]; |
| /// assert!(!items.is_empty()); |
| /// assert_eq!(items[0], "bar"); |
| /// ``` |
| /// |
| /// If the first assertion fails, the second assertion never runs, so the failure can hide the |
| /// check that would have shown more useful context. |
| /// |
| /// Instead, compare the value with an empty value using `assert_eq!`, `assert_ne!`, |
| /// `debug_assert_eq!`, or `debug_assert_ne!`. These macros print the asserted value on failure. |
| /// |
| /// ### Known problems |
| /// |
| /// Printing the asserted value can be undesirable outside tests, especially when the value may |
| /// be very large or contain sensitive information. If the assertion failure should not reveal |
| /// the value, keep the boolean assertion and allow this lint at that assertion. |
| /// |
| /// ### Example |
| /// |
| /// ```no_run |
| /// # let items = vec![1, 2, 3]; |
| /// assert!(items.is_empty()); |
| /// assert!(!items.is_empty()); |
| /// ``` |
| /// |
| /// Use instead: |
| /// |
| /// ```no_run |
| /// # let items = vec![1, 2, 3]; |
| /// assert_eq!(items, [] as [i32; 0]); |
| /// assert_ne!(items, [] as [i32; 0]); |
| /// ``` |
| #[clippy::version = "1.98.0"] |
| pub ASSERT_IS_EMPTY, |
| pedantic, |
| "asserting on emptiness without showing the asserted value on failure" |
| } |
| |
| declare_lint_pass!(AssertIsEmpty => [ASSERT_IS_EMPTY]); |
| |
| impl<'tcx> LateLintPass<'tcx> for AssertIsEmpty { |
| /// Finds assertion conditions that only test emptiness. |
| /// |
| /// Matching assertions without a custom message are rewritten as equality or inequality |
| /// assertions against an empty value. |
| fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { |
| // `assert!` and `debug_assert!` expand to `if`; skip other expressions before walking the |
| // macro backtrace. |
| if !matches!(expr.kind, ExprKind::If(..)) { |
| return; |
| } |
| |
| let Some((macro_name, condition, assert_span)) = assert_call(cx, expr) else { |
| return; |
| }; |
| let Some((assertion_kind, receiver)) = emptiness_assertion(condition) else { |
| return; |
| }; |
| let Some((receiver_suffix, empty_value)) = assertion_suggestion(cx, receiver) else { |
| return; |
| }; |
| |
| emit_assertion_suggestion( |
| cx, |
| macro_name, |
| assert_span, |
| condition, |
| assertion_kind, |
| receiver, |
| (receiver_suffix, &empty_value), |
| ); |
| } |
| } |
| |
| /// Extracts the source-level condition from `assert!` and `debug_assert!`. |
| /// |
| /// The returned macro name omits the trailing `!` so the diagnostic can build `assert_eq`, |
| /// `assert_ne`, `debug_assert_eq`, or `debug_assert_ne` from the original macro. Returns `None` |
| /// for other macros, assertions with a custom message parameter, and conditions from macro |
| /// expansions, where rewriting the condition span would produce confusing or invalid suggestions. |
| fn assert_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<(&'static str, &'tcx Expr<'tcx>, Span)> { |
| let macro_call = root_macro_call_first_node(cx, expr)?; |
| let macro_name = match cx.tcx.get_diagnostic_name(macro_call.def_id) { |
| Some(sym::assert_macro) => "assert", |
| Some(sym::debug_assert_macro) => "debug_assert", |
| _ => return None, |
| }; |
| let (condition, panic_expn) = find_assert_args(cx, expr, macro_call.expn)?; |
| if !panic_expn.is_default_message() { |
| return None; |
| } |
| |
| if condition.span.from_expansion() { |
| return None; |
| } |
| |
| Some((macro_name, condition, macro_call.span)) |
| } |
| |
| /// Returns the assertion kind and receiver for an emptiness predicate. |
| /// |
| /// `value.is_empty()` maps to an equality assertion against an empty value. `!value.is_empty()` |
| /// maps to an inequality assertion. Returns `None` when the condition is neither form. |
| fn emptiness_assertion<'tcx>(condition: &'tcx Expr<'tcx>) -> Option<(AssertionKind, &'tcx Expr<'tcx>)> { |
| if let Some(receiver) = is_empty_receiver(condition) { |
| return Some((AssertionKind::Eq, receiver)); |
| } |
| |
| let ExprKind::Unary(UnOp::Not, inner) = condition.kind else { |
| return None; |
| }; |
| |
| is_empty_receiver(inner).map(|receiver| (AssertionKind::Ne, receiver)) |
| } |
| |
| /// Returns the receiver when `expr` is a direct `value.is_empty()` call. |
| /// |
| /// Returns `None` for other method calls, negated expressions, and non-method expressions. |
| fn is_empty_receiver<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> { |
| let ExprKind::MethodCall(method, receiver, [], _) = expr.kind else { |
| return None; |
| }; |
| (method.ident.name == sym::is_empty).then_some(receiver) |
| } |
| |
| /// Assertion macro polarity for the replacement assertion. |
| /// |
| /// The variants are named after the comparison macro suffixes rather than the original predicate |
| /// shape because suggestions are the only consumer. |
| #[derive(Clone, Copy, PartialEq, Eq)] |
| enum AssertionKind { |
| /// Use an equality assertion against an empty value. |
| Eq, |
| |
| /// Use an inequality assertion against an empty value. |
| Ne, |
| } |
| |
| impl AssertionKind { |
| /// Returns the assertion macro suffix for this emptiness predicate. |
| /// |
| /// Empty checks become equality assertions. Non-empty checks become inequality assertions. |
| fn suffix(self) -> &'static str { |
| match self { |
| Self::Eq => "_eq", |
| Self::Ne => "_ne", |
| } |
| } |
| |
| /// Returns the expected state named in the diagnostic. |
| fn expected_state(self) -> &'static str { |
| match self { |
| Self::Eq => "empty", |
| Self::Ne => "not empty", |
| } |
| } |
| } |
| |
| /// Builds replacement operands when the resulting assertion is useful. |
| /// |
| /// The replacement assertion must compile, compare the same value, and print useful failure |
| /// output. Returns `None` for unsupported collection types and for element types that cannot be |
| /// printed and compared by the replacement assertion. |
| fn assertion_suggestion<'tcx>(cx: &LateContext<'tcx>, receiver: &'tcx Expr<'tcx>) -> Option<(&'static str, String)> { |
| let receiver_ty = cx.typeck_results().expr_ty(receiver); |
| let suggestion = suggestion_for_type(cx, receiver_ty)?; |
| if type_is_printable_and_comparable(cx, receiver_ty.peel_refs()) { |
| Some(suggestion) |
| } else { |
| None |
| } |
| } |
| |
| /// Returns the receiver suffix and empty value for this receiver type. |
| /// |
| /// Arrays and borrowed vectors are compared through slices because direct comparison with `[]` does |
| /// not compile for those receiver types. Returns `None` when the receiver has no compact |
| /// empty-literal comparison. |
| fn suggestion_for_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<(&'static str, String)> { |
| match ty.kind() { |
| ty::Array(..) => Some((".as_slice()", "[]".to_string())), |
| ty::Ref(_, inner, _) if matches!(inner.kind(), ty::Array(..)) => Some((".as_slice()", "[]".to_string())), |
| ty::Ref(_, inner, _) if inner.is_diag_item(cx, sym::Vec) => Some((".as_slice()", "[]".to_string())), |
| _ => suggestion_for_peeled_type(cx, ty.peel_refs()), |
| } |
| } |
| |
| /// Returns the empty value for receivers that compare directly after peeling references. |
| /// |
| /// `String` and `str` compare against `""`. Slices compare against `[]`. `Vec<T>` compares |
| /// against `[] as [T; 0]` so the empty value carries the element type. Returns `None` for other |
| /// receiver types. |
| fn suggestion_for_peeled_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<(&'static str, String)> { |
| if ty.is_str() || ty.is_lang_item(cx, LangItem::String) { |
| Some(("", "\"\"".to_string())) |
| } else if matches!(ty.kind(), ty::Slice(..)) { |
| Some(("", "[]".to_string())) |
| } else if ty.is_diag_item(cx, sym::Vec) { |
| Some(("", format!("[] as [{}; 0]", element_type(cx, ty)?))) |
| } else { |
| None |
| } |
| } |
| |
| /// Returns whether the replacement assertion has useful failure output. |
| /// |
| /// Suggestions are limited to cases where the replacement assertion can both compare the value and |
| /// print it on failure. Strings satisfy this directly; sequence-like values require printable, |
| /// self-comparable elements. Returns `false` when either trait bound is missing. |
| fn type_is_printable_and_comparable<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { |
| if ty.is_str() || ty.is_lang_item(cx, LangItem::String) { |
| return true; |
| } |
| |
| if let Some(element_ty) = element_type(cx, ty) |
| && let Some(debug_trait) = cx.tcx.get_diagnostic_item(sym::Debug) |
| && let Some(partial_eq_trait) = cx.tcx.get_diagnostic_item(sym::PartialEq) |
| { |
| implements_trait(cx, element_ty, debug_trait, &[]) |
| && implements_trait(cx, element_ty, partial_eq_trait, &[element_ty.into()]) |
| } else { |
| false |
| } |
| } |
| |
| /// Extracts the element type from supported sequence-like values. |
| /// |
| /// The element type is used both for trait checks and for the typed empty-array suggestion required |
| /// by `Vec<T>` suggestions. Returns `None` for non-sequence receiver types. |
| fn element_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> { |
| match ty.kind() { |
| ty::Array(ty, _) | ty::Slice(ty) => Some(*ty), |
| ty::Adt(_, args) if ty.is_diag_item(cx, sym::Vec) => args.types().next(), |
| _ => None, |
| } |
| } |
| |
| /// Emits the rewrite from a boolean assertion to a comparison assertion. |
| fn emit_assertion_suggestion( |
| cx: &LateContext<'_>, |
| macro_name: &str, |
| assert_span: Span, |
| condition: &Expr<'_>, |
| assertion_kind: AssertionKind, |
| receiver: &Expr<'_>, |
| suggestion: (&str, &str), |
| ) { |
| let mut applicability = Applicability::MachineApplicable; |
| let receiver_snip = |
| Sugg::hir_with_context(cx, receiver, assert_span.ctxt(), "..", &mut applicability).maybe_paren(); |
| let (receiver_suffix, empty_value) = suggestion; |
| let receiver_snip = format!("{receiver_snip}{receiver_suffix}"); |
| let assertion_suffix = assertion_kind.suffix(); |
| let expected_state = assertion_kind.expected_state(); |
| |
| span_lint_and_then( |
| cx, |
| ASSERT_IS_EMPTY, |
| assert_span, |
| format!("used `{macro_name}!` to check that a value is {expected_state}"), |
| |diag| { |
| let macro_name_span = cx.sess().source_map().span_until_char(assert_span, '!'); |
| let condition_span = walk_span_to_context(condition.span, assert_span.ctxt()).unwrap_or(condition.span); |
| |
| diag.multipart_suggestion( |
| format!("use `{macro_name}{assertion_suffix}!` to show the value on failure"), |
| vec![ |
| (macro_name_span.shrink_to_hi(), assertion_suffix.to_string()), |
| (condition_span, format!("{receiver_snip}, {empty_value}")), |
| ], |
| applicability, |
| ); |
| }, |
| ); |
| } |