Skip to content

Commit 7dba964

Browse files
committed
new lint: missing_field_in_debug
1 parent 592ea39 commit 7dba964

File tree

5 files changed

+444
-0
lines changed

5 files changed

+444
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4718,6 +4718,7 @@ Released 2018-09-13
47184718
[`missing_docs_in_private_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_docs_in_private_items
47194719
[`missing_enforced_import_renames`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_enforced_import_renames
47204720
[`missing_errors_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_errors_doc
4721+
[`missing_field_in_debug`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_field_in_debug
47214722
[`missing_inline_in_public_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_inline_in_public_items
47224723
[`missing_panics_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc
47234724
[`missing_safety_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_safety_doc

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
428428
crate::missing_const_for_fn::MISSING_CONST_FOR_FN_INFO,
429429
crate::missing_doc::MISSING_DOCS_IN_PRIVATE_ITEMS_INFO,
430430
crate::missing_enforced_import_rename::MISSING_ENFORCED_IMPORT_RENAMES_INFO,
431+
crate::missing_field_in_debug::MISSING_FIELD_IN_DEBUG_INFO,
431432
crate::missing_inline::MISSING_INLINE_IN_PUBLIC_ITEMS_INFO,
432433
crate::missing_trait_methods::MISSING_TRAIT_METHODS_INFO,
433434
crate::mixed_read_write_in_expression::DIVERGING_SUB_EXPRESSION_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ mod missing_assert_message;
202202
mod missing_const_for_fn;
203203
mod missing_doc;
204204
mod missing_enforced_import_rename;
205+
mod missing_field_in_debug;
205206
mod missing_inline;
206207
mod missing_trait_methods;
207208
mod mixed_read_write_in_expression;
@@ -960,6 +961,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
960961
store.register_late_pass(|_| Box::new(tests_outside_test_module::TestsOutsideTestModule));
961962
store.register_late_pass(|_| Box::new(manual_slice_size_calculation::ManualSliceSizeCalculation));
962963
store.register_early_pass(|| Box::new(suspicious_doc_comments::SuspiciousDocComments));
964+
store.register_late_pass(|_| Box::new(missing_field_in_debug::MissingFieldInDebug));
963965
// add lints here, do not remove this comment, it's used in `new_lint`
964966
}
965967

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
use std::{collections::HashSet, ops::ControlFlow};
2+
3+
use clippy_utils::{
4+
diagnostics::span_lint_and_then,
5+
match_def_path,
6+
visitors::{for_each_expr, Visitable},
7+
};
8+
use if_chain::if_chain;
9+
use rustc_hir::{
10+
def::{DefKind, Res},
11+
ImplItemKind, MatchSource, Node,
12+
};
13+
use rustc_hir::{Block, PatKind};
14+
use rustc_hir::{ExprKind, Impl, ItemKind, QPath, TyKind};
15+
use rustc_hir::{ImplItem, Item, VariantData};
16+
use rustc_lint::{LateContext, LateLintPass};
17+
use rustc_middle::ty::TypeckResults;
18+
use rustc_session::{declare_lint_pass, declare_tool_lint};
19+
use rustc_span::{sym, Span};
20+
21+
declare_clippy_lint! {
22+
/// ### What it does
23+
/// Checks for manual [`core::fmt::Debug`](https://doc.rust-lang.org/stable/core/fmt/trait.Debug.html) implementations that do not use all fields.
24+
///
25+
/// ### Why is this bad?
26+
/// A common mistake is to forget to update manual `Debug` implementations when adding a new field
27+
/// to a struct or a new variant to an enum.
28+
///
29+
/// 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/stable/core/fmt/struct.DebugStruct.html#method.finish_non_exhaustive)
30+
/// for the times when the user intentionally wants to leave out certain fields (e.g. to hide implementation details).
31+
///
32+
/// ### Example
33+
/// ```rust
34+
/// use std::fmt;
35+
/// struct Foo {
36+
/// data: String,
37+
/// // implementation detail
38+
/// hidden_data: i32
39+
/// }
40+
/// impl fmt::Debug for Foo {
41+
/// fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42+
/// formatter
43+
/// .debug_struct("Foo")
44+
/// .field("data", &self.data)
45+
/// .finish()
46+
/// }
47+
/// }
48+
/// ```
49+
/// Use instead:
50+
/// ```rust
51+
/// use std::fmt;
52+
/// struct Foo {
53+
/// data: String,
54+
/// // implementation detail
55+
/// hidden_data: i32
56+
/// }
57+
/// impl fmt::Debug for Foo {
58+
/// fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59+
/// formatter
60+
/// .debug_struct("Foo")
61+
/// .field("data", &self.data)
62+
/// .finish_non_exhaustive()
63+
/// }
64+
/// }
65+
/// ```
66+
#[clippy::version = "1.70.0"]
67+
pub MISSING_FIELD_IN_DEBUG,
68+
pedantic,
69+
"missing field in manual `Debug` implementation"
70+
}
71+
declare_lint_pass!(MissingFieldInDebug => [MISSING_FIELD_IN_DEBUG]);
72+
73+
fn report_lints<'tcx, I>(cx: &LateContext<'tcx>, span: Span, span_notes: I)
74+
where
75+
I: Iterator<Item = (Span, &'static str)>,
76+
{
77+
span_lint_and_then(
78+
cx,
79+
MISSING_FIELD_IN_DEBUG,
80+
span,
81+
"manual `Debug` impl does not include all fields",
82+
|diag| {
83+
for (span, note) in span_notes {
84+
diag.span_note(span, note);
85+
}
86+
diag.help("consider including all fields in this `Debug` impl")
87+
.help("consider calling `.finish_non_exhaustive()` if you intend to ignore fields");
88+
},
89+
);
90+
}
91+
92+
enum ShouldLint {
93+
Yes,
94+
No,
95+
}
96+
97+
/// Checks if we should lint in a block of code
98+
///
99+
/// The way we check for this condition is by checking if there is
100+
/// a call to `Formatter::debug_struct` or `Formatter::debug_tuple`
101+
/// and no call to `.finish_non_exhaustive()`.
102+
fn should_lint<'tcx>(
103+
cx: &LateContext<'tcx>,
104+
typeck_results: &TypeckResults<'tcx>,
105+
block: impl Visitable<'tcx>,
106+
) -> ShouldLint {
107+
let mut has_finish_non_exhaustive = false;
108+
let mut has_debug_struct_tuple = false;
109+
110+
let _: Option<!> = for_each_expr(block, |expr| {
111+
if let ExprKind::MethodCall(path, recv, ..) = &expr.kind {
112+
if [sym::debug_struct, sym::debug_tuple].contains(&path.ident.name)
113+
&& let Some(ty) = typeck_results.expr_ty(recv).peel_refs().ty_adt_def()
114+
&& match_def_path(cx,ty.did(), &["core", "fmt", "Formatter"])
115+
{
116+
has_debug_struct_tuple = true;
117+
}
118+
119+
if path.ident.name.as_str() == "finish_non_exhaustive"
120+
&& let Some(ty) = typeck_results.expr_ty(recv).peel_refs().ty_adt_def()
121+
&& match_def_path(cx,ty.did(), &["core", "fmt", "builders", "DebugStruct"])
122+
{
123+
has_finish_non_exhaustive = true;
124+
}
125+
}
126+
ControlFlow::Continue(())
127+
});
128+
129+
if !has_finish_non_exhaustive && has_debug_struct_tuple {
130+
ShouldLint::Yes
131+
} else {
132+
ShouldLint::No
133+
}
134+
}
135+
136+
/// Attempts to find unused fields assuming that the item is a struct
137+
fn check_struct<'tcx>(
138+
cx: &LateContext<'tcx>,
139+
typeck_results: &TypeckResults<'tcx>,
140+
block: &'tcx Block<'tcx>,
141+
self_ty: &rustc_middle::ty::Ty<'tcx>,
142+
item: &'tcx Item<'tcx>,
143+
data: &VariantData<'_>,
144+
) {
145+
if let ShouldLint::No = should_lint(cx, typeck_results, block) {
146+
return;
147+
}
148+
149+
let mut field_accesses = HashSet::new();
150+
151+
let _: Option<!> = for_each_expr(block, |expr| {
152+
if_chain! {
153+
if let ExprKind::Field(target, ident) = expr.kind;
154+
let target_ty = typeck_results.expr_ty(target).peel_refs();
155+
if target_ty == *self_ty;
156+
then {
157+
field_accesses.insert(ident);
158+
}
159+
}
160+
ControlFlow::Continue(())
161+
});
162+
163+
let span_notes = data
164+
.fields()
165+
.iter()
166+
.filter_map(|field| {
167+
if !field_accesses.contains(&field.ident) {
168+
Some((field.span, "this field is unused"))
169+
} else {
170+
None
171+
}
172+
})
173+
.collect::<Vec<_>>();
174+
175+
if !span_notes.is_empty() {
176+
report_lints(cx, item.span, span_notes.into_iter());
177+
}
178+
}
179+
180+
/// Attempts to find unused fields in variants assuming that
181+
/// the item is an enum.
182+
///
183+
/// Currently, only simple cases are detected where the user
184+
/// matches on `self` and calls `debug_struct` or `debug_tuple`
185+
fn check_enum<'tcx>(
186+
cx: &LateContext<'tcx>,
187+
typeck_results: &TypeckResults<'tcx>,
188+
block: &'tcx Block<'tcx>,
189+
self_ty: &rustc_middle::ty::Ty<'tcx>,
190+
item: &'tcx Item<'tcx>,
191+
) {
192+
let Some(arms) = for_each_expr(block, |expr| {
193+
if_chain! {
194+
if let ExprKind::Match(val, arms, MatchSource::Normal) = &expr.kind;
195+
if let match_ty = typeck_results.expr_ty(val).peel_refs();
196+
if match_ty == *self_ty;
197+
then {
198+
ControlFlow::Break(arms)
199+
} else {
200+
ControlFlow::Continue(())
201+
}
202+
}
203+
}) else {
204+
return;
205+
};
206+
207+
let mut span_notes = Vec::new();
208+
let lint_function = should_lint(cx, typeck_results, block);
209+
210+
for arm in *arms {
211+
if let ShouldLint::No = lint_function && let ShouldLint::No = should_lint(cx, typeck_results, arm.body) {
212+
continue;
213+
}
214+
215+
arm.pat.walk_always(|pat| match &pat.kind {
216+
PatKind::Wild => span_notes.push((pat.span, "wildcard used here")),
217+
PatKind::Tuple(_, dotdot) | PatKind::TupleStruct(.., dotdot) if dotdot.as_opt_usize().is_some() => {
218+
span_notes.push((pat.span, "more unused fields here due to dotdot pattern"));
219+
},
220+
PatKind::Struct(.., true) => span_notes.push((pat.span, "more unused fields here due to dotdot pattern")),
221+
_ => {},
222+
});
223+
224+
let mut field_accesses = HashSet::new();
225+
226+
let _: Option<!> = for_each_expr(arm.body, |expr| {
227+
if_chain! {
228+
if let ExprKind::Path(QPath::Resolved(_, path)) = &expr.kind;
229+
if let Some(segment) = path.segments.first();
230+
then {
231+
arm.pat.each_binding(|_, _, _, pat_ident| {
232+
if segment.ident == pat_ident {
233+
field_accesses.insert(pat_ident);
234+
}
235+
});
236+
}
237+
}
238+
ControlFlow::Continue(())
239+
});
240+
241+
arm.pat.each_binding(|_, _, span, pat_ident| {
242+
if !field_accesses.contains(&pat_ident) {
243+
span_notes.push((span, "the field referenced by this binding is unused"));
244+
}
245+
});
246+
}
247+
248+
if !span_notes.is_empty() {
249+
report_lints(cx, item.span, span_notes.into_iter());
250+
}
251+
}
252+
253+
impl<'tcx> LateLintPass<'tcx> for MissingFieldInDebug {
254+
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx rustc_hir::Item<'tcx>) {
255+
if_chain! {
256+
// is this an `impl Debug for X` block?
257+
if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), self_ty, items, .. }) = item.kind;
258+
if let Res::Def(DefKind::Trait, trait_def_id) = trait_ref.path.res;
259+
if let TyKind::Path(QPath::Resolved(_, self_path)) = &self_ty.kind;
260+
if cx.match_def_path(trait_def_id, &[sym::core, sym::fmt, sym::Debug]);
261+
262+
// find `Debug::fmt` function
263+
if let Some(fmt_item) = items.iter().find(|i| i.ident.name == sym::fmt);
264+
if let ImplItem { kind: ImplItemKind::Fn(_, body_id), .. } = cx.tcx.hir().impl_item(fmt_item.id);
265+
let body = cx.tcx.hir().body(*body_id);
266+
if let ExprKind::Block(block, _) = body.value.kind;
267+
268+
// inspect `self`
269+
let self_ty = cx.tcx.type_of(self_path.res.def_id()).0.peel_refs();
270+
if let Some(self_adt) = self_ty.ty_adt_def();
271+
if let Some(self_def_id) = self_adt.did().as_local();
272+
if let Some(Node::Item(self_item)) = cx.tcx.hir().find_by_def_id(self_def_id);
273+
274+
then {
275+
// NB: can't call cx.typeck_results() as we are not in a body
276+
let typeck_results = cx.tcx.typeck_body(*body_id);
277+
match &self_item.kind {
278+
ItemKind::Struct(data, _) => check_struct(cx, typeck_results, block, &self_ty, item, data),
279+
ItemKind::Enum(..) => check_enum(cx, typeck_results, block, &self_ty, item),
280+
_ => {}
281+
}
282+
}
283+
}
284+
}
285+
}

0 commit comments

Comments
 (0)