| use std::ops::ControlFlow; |
| |
| use clippy_utils::diagnostics::span_lint_and_then; |
| use clippy_utils::is_path_lang_item; |
| use clippy_utils::ty::is_type_diagnostic_item; |
| use clippy_utils::visitors::{for_each_expr, Visitable}; |
| use rustc_ast::LitKind; |
| use rustc_data_structures::fx::FxHashSet; |
| use rustc_hir::def::{DefKind, Res}; |
| use rustc_hir::{ |
| Block, Expr, ExprKind, Impl, ImplItem, ImplItemKind, Item, ItemKind, LangItem, Node, QPath, TyKind, VariantData, |
| }; |
| use rustc_lint::{LateContext, LateLintPass}; |
| use rustc_middle::ty::{Ty, TypeckResults}; |
| use rustc_session::{declare_lint_pass, declare_tool_lint}; |
| use rustc_span::{sym, Span, Symbol}; |
| |
| declare_clippy_lint! { |
| /// ### What it does |
| /// Checks for manual [`core::fmt::Debug`](https://doc.rust-lang.org/core/fmt/trait.Debug.html) implementations that do not use all fields. |
| /// |
| /// ### Why is this bad? |
| /// A common mistake is to forget to update manual `Debug` implementations when adding a new field |
| /// to a struct or a new variant to an enum. |
| /// |
| /// At the same time, it also acts as a style lint to suggest using [`core::fmt::DebugStruct::finish_non_exhaustive`](https://doc.rust-lang.org/core/fmt/struct.DebugStruct.html#method.finish_non_exhaustive) |
| /// for the times when the user intentionally wants to leave out certain fields (e.g. to hide implementation details). |
| /// |
| /// ### Known problems |
| /// This lint works based on the `DebugStruct` helper types provided by the `Formatter`, |
| /// so this won't detect `Debug` impls that use the `write!` macro. |
| /// Oftentimes there is more logic to a `Debug` impl if it uses `write!` macro, so it tries |
| /// to be on the conservative side and not lint in those cases in an attempt to prevent false positives. |
| /// |
| /// This lint also does not look through function calls, so calling a function does not consider fields |
| /// used inside of that function as used by the `Debug` impl. |
| /// |
| /// Lastly, it also ignores tuple structs as their `DebugTuple` formatter does not have a `finish_non_exhaustive` |
| /// method, as well as enums because their exhaustiveness is already checked by the compiler when matching on the enum, |
| /// making it much less likely to accidentally forget to update the `Debug` impl when adding a new variant. |
| /// |
| /// ### Example |
| /// ```no_run |
| /// use std::fmt; |
| /// struct Foo { |
| /// data: String, |
| /// // implementation detail |
| /// hidden_data: i32 |
| /// } |
| /// impl fmt::Debug for Foo { |
| /// fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { |
| /// formatter |
| /// .debug_struct("Foo") |
| /// .field("data", &self.data) |
| /// .finish() |
| /// } |
| /// } |
| /// ``` |
| /// Use instead: |
| /// ```no_run |
| /// use std::fmt; |
| /// struct Foo { |
| /// data: String, |
| /// // implementation detail |
| /// hidden_data: i32 |
| /// } |
| /// impl fmt::Debug for Foo { |
| /// fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { |
| /// formatter |
| /// .debug_struct("Foo") |
| /// .field("data", &self.data) |
| /// .finish_non_exhaustive() |
| /// } |
| /// } |
| /// ``` |
| #[clippy::version = "1.70.0"] |
| pub MISSING_FIELDS_IN_DEBUG, |
| pedantic, |
| "missing fields in manual `Debug` implementation" |
| } |
| declare_lint_pass!(MissingFieldsInDebug => [MISSING_FIELDS_IN_DEBUG]); |
| |
| fn report_lints(cx: &LateContext<'_>, span: Span, span_notes: Vec<(Span, &'static str)>) { |
| span_lint_and_then( |
| cx, |
| MISSING_FIELDS_IN_DEBUG, |
| span, |
| "manual `Debug` impl does not include all fields", |
| |diag| { |
| for (span, note) in span_notes { |
| diag.span_note(span, note); |
| } |
| diag.help("consider including all fields in this `Debug` impl") |
| .help("consider calling `.finish_non_exhaustive()` if you intend to ignore fields"); |
| }, |
| ); |
| } |
| |
| /// Checks if we should lint in a block of code |
| /// |
| /// The way we check for this condition is by checking if there is |
| /// a call to `Formatter::debug_struct` but no call to `.finish_non_exhaustive()`. |
| fn should_lint<'tcx>( |
| cx: &LateContext<'tcx>, |
| typeck_results: &TypeckResults<'tcx>, |
| block: impl Visitable<'tcx>, |
| ) -> bool { |
| // Is there a call to `DebugStruct::finish_non_exhaustive`? Don't lint if there is. |
| let mut has_finish_non_exhaustive = false; |
| // Is there a call to `DebugStruct::debug_struct`? Do lint if there is. |
| let mut has_debug_struct = false; |
| |
| for_each_expr(block, |expr| { |
| if let ExprKind::MethodCall(path, recv, ..) = &expr.kind { |
| let recv_ty = typeck_results.expr_ty(recv).peel_refs(); |
| |
| if path.ident.name == sym::debug_struct && is_type_diagnostic_item(cx, recv_ty, sym::Formatter) { |
| has_debug_struct = true; |
| } else if path.ident.name == sym!(finish_non_exhaustive) |
| && is_type_diagnostic_item(cx, recv_ty, sym::DebugStruct) |
| { |
| has_finish_non_exhaustive = true; |
| } |
| } |
| ControlFlow::<!, _>::Continue(()) |
| }); |
| |
| !has_finish_non_exhaustive && has_debug_struct |
| } |
| |
| /// Checks if the given expression is a call to `DebugStruct::field` |
| /// and the first argument to it is a string literal and if so, returns it |
| /// |
| /// Example: `.field("foo", ....)` returns `Some("foo")` |
| fn as_field_call<'tcx>( |
| cx: &LateContext<'tcx>, |
| typeck_results: &TypeckResults<'tcx>, |
| expr: &Expr<'_>, |
| ) -> Option<Symbol> { |
| if let ExprKind::MethodCall(path, recv, [debug_field, _], _) = &expr.kind |
| && let recv_ty = typeck_results.expr_ty(recv).peel_refs() |
| && is_type_diagnostic_item(cx, recv_ty, sym::DebugStruct) |
| && path.ident.name == sym::field |
| && let ExprKind::Lit(lit) = &debug_field.kind |
| && let LitKind::Str(sym, ..) = lit.node |
| { |
| Some(sym) |
| } else { |
| None |
| } |
| } |
| |
| /// Attempts to find unused fields assuming that the item is a struct |
| fn check_struct<'tcx>( |
| cx: &LateContext<'tcx>, |
| typeck_results: &TypeckResults<'tcx>, |
| block: &'tcx Block<'tcx>, |
| self_ty: Ty<'tcx>, |
| item: &'tcx Item<'tcx>, |
| data: &VariantData<'_>, |
| ) { |
| // Is there a "direct" field access anywhere (i.e. self.foo)? |
| // We don't want to lint if there is not, because the user might have |
| // a newtype struct and use fields from the wrapped type only. |
| let mut has_direct_field_access = false; |
| let mut field_accesses = FxHashSet::default(); |
| |
| for_each_expr(block, |expr| { |
| if let ExprKind::Field(target, ident) = expr.kind |
| && let target_ty = typeck_results.expr_ty_adjusted(target).peel_refs() |
| && target_ty == self_ty |
| { |
| field_accesses.insert(ident.name); |
| has_direct_field_access = true; |
| } else if let Some(sym) = as_field_call(cx, typeck_results, expr) { |
| field_accesses.insert(sym); |
| } |
| ControlFlow::<!, _>::Continue(()) |
| }); |
| |
| let span_notes = data |
| .fields() |
| .iter() |
| .filter_map(|field| { |
| if field_accesses.contains(&field.ident.name) || is_path_lang_item(cx, field.ty, LangItem::PhantomData) { |
| None |
| } else { |
| Some((field.span, "this field is unused")) |
| } |
| }) |
| .collect::<Vec<_>>(); |
| |
| // only lint if there's also at least one direct field access to allow patterns |
| // where one might have a newtype struct and uses fields from the wrapped type |
| if !span_notes.is_empty() && has_direct_field_access { |
| report_lints(cx, item.span, span_notes); |
| } |
| } |
| |
| impl<'tcx> LateLintPass<'tcx> for MissingFieldsInDebug { |
| fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx rustc_hir::Item<'tcx>) { |
| // is this an `impl Debug for X` block? |
| if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), self_ty, items, .. }) = item.kind |
| && let Res::Def(DefKind::Trait, trait_def_id) = trait_ref.path.res |
| && let TyKind::Path(QPath::Resolved(_, self_path)) = &self_ty.kind |
| // make sure that the self type is either a struct, an enum or a union |
| // this prevents ICEs such as when self is a type parameter or a primitive type |
| // (see #10887, #11063) |
| && let Res::Def(DefKind::Struct | DefKind::Enum | DefKind::Union, self_path_did) = self_path.res |
| && cx.match_def_path(trait_def_id, &[sym::core, sym::fmt, sym::Debug]) |
| // don't trigger if this impl was derived |
| && !cx.tcx.has_attr(item.owner_id, sym::automatically_derived) |
| && !item.span.from_expansion() |
| // find `Debug::fmt` function |
| && let Some(fmt_item) = items.iter().find(|i| i.ident.name == sym::fmt) |
| && let ImplItem { kind: ImplItemKind::Fn(_, body_id), .. } = cx.tcx.hir().impl_item(fmt_item.id) |
| && let body = cx.tcx.hir().body(*body_id) |
| && let ExprKind::Block(block, _) = body.value.kind |
| // inspect `self` |
| && let self_ty = cx.tcx.type_of(self_path_did).skip_binder().peel_refs() |
| && let Some(self_adt) = self_ty.ty_adt_def() |
| && let Some(self_def_id) = self_adt.did().as_local() |
| && let Some(Node::Item(self_item)) = cx.tcx.hir().find_by_def_id(self_def_id) |
| // NB: can't call cx.typeck_results() as we are not in a body |
| && let typeck_results = cx.tcx.typeck_body(*body_id) |
| && should_lint(cx, typeck_results, block) |
| { |
| // we intentionally only lint structs, see lint description |
| if let ItemKind::Struct(data, _) = &self_item.kind { |
| check_struct(cx, typeck_results, block, self_ty, item, data); |
| } |
| } |
| } |
| } |