|
| 1 | +use clippy_utils::diagnostics::span_lint_and_then; |
| 2 | +use clippy_utils::macros::{find_assert_args, root_macro_call_first_node}; |
| 3 | +use clippy_utils::res::MaybeDef; |
| 4 | +use clippy_utils::source::walk_span_to_context; |
| 5 | +use clippy_utils::sugg::Sugg; |
| 6 | +use clippy_utils::sym; |
| 7 | +use clippy_utils::ty::implements_trait; |
| 8 | +use rustc_errors::Applicability; |
| 9 | +use rustc_hir::{Expr, ExprKind, LangItem, UnOp}; |
| 10 | +use rustc_lint::{LateContext, LateLintPass, LintContext}; |
| 11 | +use rustc_middle::ty::{self, Ty}; |
| 12 | +use rustc_session::declare_lint_pass; |
| 13 | +use rustc_span::Span; |
| 14 | + |
| 15 | +declare_clippy_lint! { |
| 16 | + /// ### What it does |
| 17 | + /// |
| 18 | + /// Checks assertions that only test whether a supported value is empty. |
| 19 | + /// |
| 20 | + /// The lint handles `assert!` and `debug_assert!` calls on strings, slices, arrays, and `Vec`. |
| 21 | + /// |
| 22 | + /// ### Why is this bad? |
| 23 | + /// |
| 24 | + /// A boolean assertion only reports that the emptiness check failed. It does not show what the |
| 25 | + /// asserted value contained. |
| 26 | + /// |
| 27 | + /// In CI or another remote test service, the failure output tells you that the value was |
| 28 | + /// unexpectedly empty or non-empty, but not which values were present. The next step is often |
| 29 | + /// to reproduce the failure locally, add temporary logging, or change the test so it exposes |
| 30 | + /// the value. That extra investigation can be much slower than fixing the problem from the |
| 31 | + /// original CI failure. |
| 32 | + /// |
| 33 | + /// The emptiness check also commonly appears before a deeper contents assertion: |
| 34 | + /// |
| 35 | + /// ```no_run |
| 36 | + /// # let items = vec!["baz"]; |
| 37 | + /// assert!(!items.is_empty()); |
| 38 | + /// assert_eq!(items[0], "bar"); |
| 39 | + /// ``` |
| 40 | + /// |
| 41 | + /// If the first assertion fails, the second assertion never runs, so the failure can hide the |
| 42 | + /// check that would have shown more useful context. |
| 43 | + /// |
| 44 | + /// Instead, compare the value with an empty value using `assert_eq!`, `assert_ne!`, |
| 45 | + /// `debug_assert_eq!`, or `debug_assert_ne!`. These macros print the asserted value on failure. |
| 46 | + /// |
| 47 | + /// ### Example |
| 48 | + /// |
| 49 | + /// ```no_run |
| 50 | + /// # let items = vec![1, 2, 3]; |
| 51 | + /// assert!(items.is_empty()); |
| 52 | + /// assert!(!items.is_empty()); |
| 53 | + /// ``` |
| 54 | + /// |
| 55 | + /// Use instead: |
| 56 | + /// |
| 57 | + /// ```no_run |
| 58 | + /// # let items = vec![1, 2, 3]; |
| 59 | + /// assert_eq!(items, [] as [i32; 0]); |
| 60 | + /// assert_ne!(items, [] as [i32; 0]); |
| 61 | + /// ``` |
| 62 | + #[clippy::version = "1.98.0"] |
| 63 | + pub ASSERT_IS_EMPTY, |
| 64 | + pedantic, |
| 65 | + "asserting on emptiness without showing the asserted value on failure" |
| 66 | +} |
| 67 | + |
| 68 | +declare_lint_pass!(AssertIsEmpty => [ASSERT_IS_EMPTY]); |
| 69 | + |
| 70 | +impl<'tcx> LateLintPass<'tcx> for AssertIsEmpty { |
| 71 | + /// Finds assertion conditions that only test emptiness. |
| 72 | + /// |
| 73 | + /// Matching assertions are rewritten as equality or inequality assertions against an empty |
| 74 | + /// value. The suggestion edits only the macro name and predicate so custom assertion messages |
| 75 | + /// remain in place. |
| 76 | + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { |
| 77 | + if !matches!(expr.kind, ExprKind::If(..)) { |
| 78 | + return; |
| 79 | + } |
| 80 | + |
| 81 | + let Some((macro_name, condition, assert_span)) = assert_call(cx, expr) else { |
| 82 | + return; |
| 83 | + }; |
| 84 | + let Some((assertion_kind, receiver)) = emptiness_assertion(condition) else { |
| 85 | + return; |
| 86 | + }; |
| 87 | + let Some((receiver_suffix, empty_value)) = assertion_suggestion(cx, receiver) else { |
| 88 | + return; |
| 89 | + }; |
| 90 | + |
| 91 | + emit_assertion_suggestion( |
| 92 | + cx, |
| 93 | + macro_name, |
| 94 | + assert_span, |
| 95 | + condition, |
| 96 | + assertion_kind, |
| 97 | + receiver, |
| 98 | + (receiver_suffix, &empty_value), |
| 99 | + ); |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +/// Extracts the source-level condition from `assert!` and `debug_assert!`. |
| 104 | +/// |
| 105 | +/// The returned macro name omits the trailing `!` so the diagnostic can build `assert_eq`, |
| 106 | +/// `assert_ne`, `debug_assert_eq`, or `debug_assert_ne` from the original macro. Returns `None` |
| 107 | +/// for other macros and for conditions from macro expansions, where rewriting the condition span |
| 108 | +/// would produce confusing or invalid suggestions. |
| 109 | +fn assert_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<(&'static str, &'tcx Expr<'tcx>, Span)> { |
| 110 | + let macro_call = root_macro_call_first_node(cx, expr)?; |
| 111 | + let macro_name = match cx.tcx.get_diagnostic_name(macro_call.def_id) { |
| 112 | + Some(sym::assert_macro) => "assert", |
| 113 | + Some(sym::debug_assert_macro) => "debug_assert", |
| 114 | + _ => return None, |
| 115 | + }; |
| 116 | + let (condition, _) = find_assert_args(cx, expr, macro_call.expn)?; |
| 117 | + if condition.span.from_expansion() { |
| 118 | + return None; |
| 119 | + } |
| 120 | + |
| 121 | + Some((macro_name, condition, macro_call.span)) |
| 122 | +} |
| 123 | + |
| 124 | +/// Returns the assertion kind and receiver for an emptiness predicate. |
| 125 | +/// |
| 126 | +/// `value.is_empty()` maps to an equality assertion against an empty value. `!value.is_empty()` |
| 127 | +/// maps to an inequality assertion. Returns `None` when the condition is neither form. |
| 128 | +fn emptiness_assertion<'tcx>(condition: &'tcx Expr<'tcx>) -> Option<(AssertionKind, &'tcx Expr<'tcx>)> { |
| 129 | + if let Some(receiver) = is_empty_receiver(condition) { |
| 130 | + return Some((AssertionKind::Eq, receiver)); |
| 131 | + } |
| 132 | + |
| 133 | + let ExprKind::Unary(UnOp::Not, inner) = condition.kind else { |
| 134 | + return None; |
| 135 | + }; |
| 136 | + |
| 137 | + is_empty_receiver(inner).map(|receiver| (AssertionKind::Ne, receiver)) |
| 138 | +} |
| 139 | + |
| 140 | +/// Returns the receiver when `expr` is a direct `value.is_empty()` call. |
| 141 | +/// |
| 142 | +/// Returns `None` for other method calls, negated expressions, and non-method expressions. |
| 143 | +fn is_empty_receiver<'tcx>(expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> { |
| 144 | + let ExprKind::MethodCall(method, receiver, [], _) = expr.kind else { |
| 145 | + return None; |
| 146 | + }; |
| 147 | + (method.ident.name == sym::is_empty).then_some(receiver) |
| 148 | +} |
| 149 | + |
| 150 | +/// Assertion macro polarity for the replacement assertion. |
| 151 | +/// |
| 152 | +/// The variants are named after the comparison macro suffixes rather than the original predicate |
| 153 | +/// shape because suggestions are the only consumer. |
| 154 | +#[derive(Clone, Copy, PartialEq, Eq)] |
| 155 | +enum AssertionKind { |
| 156 | + /// Use an equality assertion against an empty value. |
| 157 | + Eq, |
| 158 | + |
| 159 | + /// Use an inequality assertion against an empty value. |
| 160 | + Ne, |
| 161 | +} |
| 162 | + |
| 163 | +impl AssertionKind { |
| 164 | + /// Returns the assertion macro suffix for this emptiness predicate. |
| 165 | + /// |
| 166 | + /// Empty checks become equality assertions. Non-empty checks become inequality assertions. |
| 167 | + fn suffix(self) -> &'static str { |
| 168 | + match self { |
| 169 | + Self::Eq => "_eq", |
| 170 | + Self::Ne => "_ne", |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + /// Returns the expected state named in the diagnostic. |
| 175 | + fn expected_state(self) -> &'static str { |
| 176 | + match self { |
| 177 | + Self::Eq => "empty", |
| 178 | + Self::Ne => "not empty", |
| 179 | + } |
| 180 | + } |
| 181 | +} |
| 182 | + |
| 183 | +/// Builds replacement operands when the resulting assertion is useful. |
| 184 | +/// |
| 185 | +/// The replacement assertion must compile, compare the same value, and print useful failure |
| 186 | +/// output. Returns `None` for unsupported collection types and for element types that cannot be |
| 187 | +/// printed and compared by the replacement assertion. |
| 188 | +fn assertion_suggestion<'tcx>(cx: &LateContext<'tcx>, receiver: &'tcx Expr<'tcx>) -> Option<(&'static str, String)> { |
| 189 | + let receiver_ty = cx.typeck_results().expr_ty(receiver); |
| 190 | + let suggestion = suggestion_for_type(cx, receiver_ty)?; |
| 191 | + if type_is_printable_and_comparable(cx, receiver_ty.peel_refs()) { |
| 192 | + Some(suggestion) |
| 193 | + } else { |
| 194 | + None |
| 195 | + } |
| 196 | +} |
| 197 | + |
| 198 | +/// Returns the receiver suffix and empty value for this receiver type. |
| 199 | +/// |
| 200 | +/// Arrays and borrowed vectors are compared through slices because direct comparison with `[]` does |
| 201 | +/// not compile for those receiver types. Returns `None` when the receiver has no compact |
| 202 | +/// empty-literal comparison. |
| 203 | +fn suggestion_for_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<(&'static str, String)> { |
| 204 | + match ty.kind() { |
| 205 | + ty::Array(..) => Some((".as_slice()", "[]".to_string())), |
| 206 | + ty::Ref(_, inner, _) if matches!(inner.kind(), ty::Array(..)) => Some((".as_slice()", "[]".to_string())), |
| 207 | + ty::Ref(_, inner, _) if inner.is_diag_item(cx, sym::Vec) => Some((".as_slice()", "[]".to_string())), |
| 208 | + _ => suggestion_for_peeled_type(cx, ty.peel_refs()), |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +/// Returns the empty value for receivers that compare directly after peeling references. |
| 213 | +/// |
| 214 | +/// `String` and `str` compare against `""`. Slices compare against `[]`. `Vec<T>` compares |
| 215 | +/// against `[] as [T; 0]` so the empty value carries the element type. Returns `None` for other |
| 216 | +/// receiver types. |
| 217 | +fn suggestion_for_peeled_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<(&'static str, String)> { |
| 218 | + if ty.is_str() || ty.is_lang_item(cx, LangItem::String) { |
| 219 | + Some(("", "\"\"".to_string())) |
| 220 | + } else if matches!(ty.kind(), ty::Slice(..)) { |
| 221 | + Some(("", "[]".to_string())) |
| 222 | + } else if ty.is_diag_item(cx, sym::Vec) { |
| 223 | + Some(("", format!("[] as [{}; 0]", element_type(cx, ty)?))) |
| 224 | + } else { |
| 225 | + None |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +/// Returns whether the replacement assertion has useful failure output. |
| 230 | +/// |
| 231 | +/// Suggestions are limited to cases where the replacement assertion can both compare the value and |
| 232 | +/// print it on failure. Strings satisfy this directly; sequence-like values require printable, |
| 233 | +/// self-comparable elements. Returns `false` when either trait bound is missing. |
| 234 | +fn type_is_printable_and_comparable<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool { |
| 235 | + if ty.is_str() || ty.is_lang_item(cx, LangItem::String) { |
| 236 | + return true; |
| 237 | + } |
| 238 | + |
| 239 | + if let Some(element_ty) = element_type(cx, ty) |
| 240 | + && let Some(debug_trait) = cx.tcx.get_diagnostic_item(sym::Debug) |
| 241 | + && let Some(partial_eq_trait) = cx.tcx.get_diagnostic_item(sym::PartialEq) |
| 242 | + { |
| 243 | + implements_trait(cx, element_ty, debug_trait, &[]) |
| 244 | + && implements_trait(cx, element_ty, partial_eq_trait, &[element_ty.into()]) |
| 245 | + } else { |
| 246 | + false |
| 247 | + } |
| 248 | +} |
| 249 | + |
| 250 | +/// Extracts the element type from supported sequence-like values. |
| 251 | +/// |
| 252 | +/// The element type is used both for trait checks and for the typed empty-array suggestion required |
| 253 | +/// by `Vec<T>` suggestions. Returns `None` for non-sequence receiver types. |
| 254 | +fn element_type<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> { |
| 255 | + match ty.kind() { |
| 256 | + ty::Array(ty, _) | ty::Slice(ty) => Some(*ty), |
| 257 | + ty::Adt(_, args) if ty.is_diag_item(cx, sym::Vec) => args.types().next(), |
| 258 | + _ => None, |
| 259 | + } |
| 260 | +} |
| 261 | + |
| 262 | +/// Emits the rewrite that preserves custom assertion messages. |
| 263 | +/// |
| 264 | +/// Only the macro suffix and condition expression are replaced. Any message arguments after the |
| 265 | +/// condition remain untouched. |
| 266 | +fn emit_assertion_suggestion( |
| 267 | + cx: &LateContext<'_>, |
| 268 | + macro_name: &str, |
| 269 | + assert_span: Span, |
| 270 | + condition: &Expr<'_>, |
| 271 | + assertion_kind: AssertionKind, |
| 272 | + receiver: &Expr<'_>, |
| 273 | + suggestion: (&str, &str), |
| 274 | +) { |
| 275 | + let mut applicability = Applicability::MachineApplicable; |
| 276 | + let receiver_snip = |
| 277 | + Sugg::hir_with_context(cx, receiver, assert_span.ctxt(), "..", &mut applicability).maybe_paren(); |
| 278 | + let (receiver_suffix, empty_value) = suggestion; |
| 279 | + let receiver_snip = format!("{receiver_snip}{receiver_suffix}"); |
| 280 | + let assertion_suffix = assertion_kind.suffix(); |
| 281 | + let expected_state = assertion_kind.expected_state(); |
| 282 | + |
| 283 | + span_lint_and_then( |
| 284 | + cx, |
| 285 | + ASSERT_IS_EMPTY, |
| 286 | + assert_span, |
| 287 | + format!("used `{macro_name}!` to check that a value is {expected_state}"), |
| 288 | + |diag| { |
| 289 | + let macro_name_span = cx.sess().source_map().span_until_char(assert_span, '!'); |
| 290 | + let condition_span = walk_span_to_context(condition.span, assert_span.ctxt()).unwrap_or(condition.span); |
| 291 | + |
| 292 | + diag.multipart_suggestion( |
| 293 | + format!("use `{macro_name}{assertion_suffix}!` to show the value on failure"), |
| 294 | + vec![ |
| 295 | + (macro_name_span.shrink_to_hi(), assertion_suffix.to_string()), |
| 296 | + (condition_span, format!("{receiver_snip}, {empty_value}")), |
| 297 | + ], |
| 298 | + applicability, |
| 299 | + ); |
| 300 | + }, |
| 301 | + ); |
| 302 | +} |
0 commit comments