Skip to content

Commit f3dd1c6

Browse files
committed
move some strings into consts, more tests
1 parent 7dba964 commit f3dd1c6

File tree

4 files changed

+287
-59
lines changed

4 files changed

+287
-59
lines changed

clippy_lints/src/missing_field_in_debug.rs

Lines changed: 55 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{collections::HashSet, ops::ControlFlow};
22

33
use clippy_utils::{
44
diagnostics::span_lint_and_then,
5-
match_def_path,
5+
match_def_path, paths,
66
visitors::{for_each_expr, Visitable},
77
};
88
use if_chain::if_chain;
@@ -20,15 +20,25 @@ use rustc_span::{sym, Span};
2020

2121
declare_clippy_lint! {
2222
/// ### 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.
23+
/// Checks for manual [`core::fmt::Debug`] implementations that do not use all fields.
2424
///
2525
/// ### Why is this bad?
2626
/// A common mistake is to forget to update manual `Debug` implementations when adding a new field
2727
/// to a struct or a new variant to an enum.
2828
///
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)
29+
/// At the same time, it also acts as a style lint to suggest using [`core::fmt::DebugStruct::finish_non_exhaustive`]
3030
/// for the times when the user intentionally wants to leave out certain fields (e.g. to hide implementation details).
3131
///
32+
/// ### Known problems
33+
/// This lint works based on the [`DebugStruct`] and [`DebugTuple`] helper types provided by
34+
/// the [`Formatter`], so this won't detect `Debug` impls that use the `write!` macro.
35+
/// Oftentimes there is more logic to a `Debug` impl if it uses `write!` macro, so it tries
36+
/// to be on the conservative side and not lint in those cases in an attempt to prevent false positives.
37+
///
38+
/// This lint also does not look through function calls, so calling `.field(self.as_slice())` for example
39+
/// does not consider fields used inside of `as_slice()` as used by the `Debug` impl.
40+
///
41+
///
3242
/// ### Example
3343
/// ```rust
3444
/// use std::fmt;
@@ -70,28 +80,24 @@ declare_clippy_lint! {
7080
}
7181
declare_lint_pass!(MissingFieldInDebug => [MISSING_FIELD_IN_DEBUG]);
7282

83+
const WARN: &str = "manual `Debug` impl does not include all fields";
84+
const HELP_WILDCARD: &str = "unused field here due to wildcard `_`";
85+
const HELP_UNUSED_REST: &str = "more unused fields here due to rest pattern `..`";
86+
const HELP_FIELD_UNUSED: &str = "this field is unused";
87+
const HELP_FIELD_REFERENCE: &str = "the field referenced by this binding is unused";
88+
const HELP_INCLUDE_ALL_FIELDS: &str = "consider including all fields in this `Debug` impl";
89+
const HELP_FINISH_NON_EXHAUSTIVE: &str = "consider calling `.finish_non_exhaustive()` if you intend to ignore fields";
90+
7391
fn report_lints<'tcx, I>(cx: &LateContext<'tcx>, span: Span, span_notes: I)
7492
where
7593
I: Iterator<Item = (Span, &'static str)>,
7694
{
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+
span_lint_and_then(cx, MISSING_FIELD_IN_DEBUG, span, WARN, |diag| {
96+
for (span, note) in span_notes {
97+
diag.span_note(span, note);
98+
}
99+
diag.help(HELP_INCLUDE_ALL_FIELDS).help(HELP_FINISH_NON_EXHAUSTIVE);
100+
});
95101
}
96102

97103
/// Checks if we should lint in a block of code
@@ -103,34 +109,36 @@ fn should_lint<'tcx>(
103109
cx: &LateContext<'tcx>,
104110
typeck_results: &TypeckResults<'tcx>,
105111
block: impl Visitable<'tcx>,
106-
) -> ShouldLint {
112+
) -> bool {
107113
let mut has_finish_non_exhaustive = false;
108114
let mut has_debug_struct_tuple = false;
109115

110116
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-
{
117+
let ExprKind::MethodCall(path, recv, ..) = &expr.kind else {
118+
return ControlFlow::Continue(());
119+
};
120+
121+
if_chain! {
122+
if [sym::debug_struct, sym::debug_tuple].contains(&path.ident.name);
123+
if let Some(ty) = typeck_results.expr_ty(recv).peel_refs().ty_adt_def();
124+
if match_def_path(cx,ty.did(), &paths::FORMATTER);
125+
then {
116126
has_debug_struct_tuple = true;
117127
}
128+
}
118129

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-
{
130+
if_chain! {
131+
if path.ident.name.as_str() == "finish_non_exhaustive";
132+
if let Some(ty) = typeck_results.expr_ty(recv).peel_refs().ty_adt_def();
133+
if match_def_path(cx,ty.did(), &paths::DEBUG_STRUCT);
134+
then {
123135
has_finish_non_exhaustive = true;
124136
}
125137
}
126138
ControlFlow::Continue(())
127139
});
128140

129-
if !has_finish_non_exhaustive && has_debug_struct_tuple {
130-
ShouldLint::Yes
131-
} else {
132-
ShouldLint::No
133-
}
141+
!has_finish_non_exhaustive && has_debug_struct_tuple
134142
}
135143

136144
/// Attempts to find unused fields assuming that the item is a struct
@@ -142,10 +150,6 @@ fn check_struct<'tcx>(
142150
item: &'tcx Item<'tcx>,
143151
data: &VariantData<'_>,
144152
) {
145-
if let ShouldLint::No = should_lint(cx, typeck_results, block) {
146-
return;
147-
}
148-
149153
let mut field_accesses = HashSet::new();
150154

151155
let _: Option<!> = for_each_expr(block, |expr| {
@@ -165,7 +169,7 @@ fn check_struct<'tcx>(
165169
.iter()
166170
.filter_map(|field| {
167171
if !field_accesses.contains(&field.ident) {
168-
Some((field.span, "this field is unused"))
172+
Some((field.span, HELP_FIELD_UNUSED))
169173
} else {
170174
None
171175
}
@@ -182,6 +186,7 @@ fn check_struct<'tcx>(
182186
///
183187
/// Currently, only simple cases are detected where the user
184188
/// matches on `self` and calls `debug_struct` or `debug_tuple`
189+
/// inside of the arms
185190
fn check_enum<'tcx>(
186191
cx: &LateContext<'tcx>,
187192
typeck_results: &TypeckResults<'tcx>,
@@ -205,19 +210,18 @@ fn check_enum<'tcx>(
205210
};
206211

207212
let mut span_notes = Vec::new();
208-
let lint_function = should_lint(cx, typeck_results, block);
209213

210214
for arm in *arms {
211-
if let ShouldLint::No = lint_function && let ShouldLint::No = should_lint(cx, typeck_results, arm.body) {
215+
if !should_lint(cx, typeck_results, arm.body) {
212216
continue;
213217
}
214218

215219
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"));
220+
PatKind::Wild => span_notes.push((pat.span, HELP_WILDCARD)),
221+
PatKind::Tuple(_, rest) | PatKind::TupleStruct(.., rest) if rest.as_opt_usize().is_some() => {
222+
span_notes.push((pat.span, HELP_UNUSED_REST));
219223
},
220-
PatKind::Struct(.., true) => span_notes.push((pat.span, "more unused fields here due to dotdot pattern")),
224+
PatKind::Struct(.., true) => span_notes.push((pat.span, HELP_UNUSED_REST)),
221225
_ => {},
222226
});
223227

@@ -240,7 +244,7 @@ fn check_enum<'tcx>(
240244

241245
arm.pat.each_binding(|_, _, span, pat_ident| {
242246
if !field_accesses.contains(&pat_ident) {
243-
span_notes.push((span, "the field referenced by this binding is unused"));
247+
span_notes.push((span, HELP_FIELD_REFERENCE));
244248
}
245249
});
246250
}
@@ -271,9 +275,10 @@ impl<'tcx> LateLintPass<'tcx> for MissingFieldInDebug {
271275
if let Some(self_def_id) = self_adt.did().as_local();
272276
if let Some(Node::Item(self_item)) = cx.tcx.hir().find_by_def_id(self_def_id);
273277

278+
// NB: can't call cx.typeck_results() as we are not in a body
279+
let typeck_results = cx.tcx.typeck_body(*body_id);
280+
if should_lint(cx, typeck_results, block);
274281
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);
277282
match &self_item.kind {
278283
ItemKind::Struct(data, _) => check_struct(cx, typeck_results, block, &self_ty, item, data),
279284
ItemKind::Enum(..) => check_enum(cx, typeck_results, block, &self_ty, item),

clippy_utils/src/paths.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,5 @@ pub const WEAK_RC: [&str; 3] = ["alloc", "rc", "Weak"];
159159
pub const PTR_NON_NULL: [&str; 4] = ["core", "ptr", "non_null", "NonNull"];
160160
pub const INSTANT_NOW: [&str; 4] = ["std", "time", "Instant", "now"];
161161
pub const INSTANT: [&str; 3] = ["std", "time", "Instant"];
162+
pub const FORMATTER: [&str; 3] = ["core", "fmt", "Formatter"];
163+
pub const DEBUG_STRUCT: [&str; 4] = ["core", "fmt", "builders", "DebugStruct"];

tests/ui/missing_field_in_debug.rs

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct NamedStruct1Ignored {
99
}
1010

1111
impl fmt::Debug for NamedStruct1Ignored {
12+
// unused field: hidden
1213
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1314
formatter
1415
.debug_struct("NamedStruct1Ignored")
@@ -26,6 +27,7 @@ struct NamedStructMultipleIgnored {
2627
}
2728

2829
impl fmt::Debug for NamedStructMultipleIgnored {
30+
// unused fields: hidden, hidden2, hidden4
2931
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
3032
formatter
3133
.debug_struct("NamedStructMultipleIgnored")
@@ -37,6 +39,7 @@ impl fmt::Debug for NamedStructMultipleIgnored {
3739

3840
struct Unit;
3941

42+
// ok
4043
impl fmt::Debug for Unit {
4144
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
4245
formatter.debug_struct("Unit").finish()
@@ -54,6 +57,7 @@ impl fmt::Debug for UnnamedStruct1Ignored {
5457
struct UnnamedStructMultipleIgnored(String, Vec<u8>, i32);
5558

5659
impl fmt::Debug for UnnamedStructMultipleIgnored {
60+
// unused fields: 0, 2
5761
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
5862
formatter
5963
.debug_tuple("UnnamedStructMultipleIgnored")
@@ -67,6 +71,7 @@ struct NamedStructNonExhaustive {
6771
b: String,
6872
}
6973

74+
// ok
7075
impl fmt::Debug for NamedStructNonExhaustive {
7176
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
7277
formatter
@@ -81,7 +86,7 @@ struct MultiExprDebugImpl {
8186
b: String,
8287
}
8388

84-
// Debug impl split up across multiple expressions
89+
// ok
8590
impl fmt::Debug for MultiExprDebugImpl {
8691
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
8792
let mut f = formatter.debug_struct("MultiExprDebugImpl");
@@ -94,10 +99,11 @@ enum SingleVariantEnumUnnamed {
9499
A(u8),
95100
}
96101

102+
// ok
97103
impl fmt::Debug for SingleVariantEnumUnnamed {
98104
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99105
match self {
100-
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(), // OK
106+
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(),
101107
}
102108
}
103109
}
@@ -109,9 +115,10 @@ enum MultiVariantEnum {
109115
}
110116

111117
impl fmt::Debug for MultiVariantEnum {
118+
// match arm Self::B ignores `b`
112119
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113120
match self {
114-
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(), // OK
121+
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(),
115122
Self::B { a, b } => formatter.debug_struct("B").field("a", &a).finish(),
116123
Self::C => formatter.debug_struct("C").finish(),
117124
}
@@ -124,11 +131,12 @@ enum MultiVariantEnumOk {
124131
C,
125132
}
126133

134+
// ok
127135
impl fmt::Debug for MultiVariantEnumOk {
128136
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129137
match self {
130-
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(), // OK
131-
Self::B { a, b } => formatter.debug_struct("B").field("a", &a).field("b", &b).finish(), // OK
138+
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(),
139+
Self::B { a, b } => formatter.debug_struct("B").field("a", &a).field("b", &b).finish(),
132140
Self::C => formatter.debug_struct("C").finish(),
133141
}
134142
}
@@ -140,16 +148,73 @@ enum MultiVariantEnumNonExhaustive {
140148
C,
141149
}
142150

151+
// ok
143152
impl fmt::Debug for MultiVariantEnumNonExhaustive {
144153
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145154
match self {
146-
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(), // OK
147-
Self::B { a, b } => formatter.debug_struct("B").field("b", &b).finish_non_exhaustive(), // OK
155+
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(),
156+
Self::B { a, b } => formatter.debug_struct("B").field("b", &b).finish_non_exhaustive(),
148157
Self::C => formatter.debug_struct("C").finish(),
149158
}
150159
}
151160
}
152161

153-
fn main() {
154-
// test code goes here
162+
enum MultiVariantRest {
163+
A(u8),
164+
B { a: u8, b: String },
165+
C,
166+
}
167+
168+
impl fmt::Debug for MultiVariantRest {
169+
// `a` field ignored due to rest pattern
170+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171+
match self {
172+
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(),
173+
Self::B { b, .. } => formatter.debug_struct("B").field("b", &b).finish(),
174+
Self::C => formatter.debug_struct("C").finish(),
175+
}
176+
}
155177
}
178+
179+
enum MultiVariantRestNonExhaustive {
180+
A(u8),
181+
B { a: u8, b: String },
182+
C,
183+
}
184+
185+
// ok
186+
impl fmt::Debug for MultiVariantRestNonExhaustive {
187+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
188+
match self {
189+
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(),
190+
Self::B { b, .. } => formatter.debug_struct("B").field("b", &b).finish_non_exhaustive(),
191+
Self::C => formatter.debug_struct("C").finish(),
192+
}
193+
}
194+
}
195+
196+
enum Wildcard {
197+
A(u8),
198+
B(String),
199+
}
200+
201+
// ok
202+
impl fmt::Debug for Wildcard {
203+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
204+
match self {
205+
Self::A(n) => formatter.debug_tuple("A").field(&n).finish(),
206+
_ => todo!(),
207+
}
208+
}
209+
}
210+
211+
enum Empty {}
212+
213+
// ok
214+
impl fmt::Debug for Empty {
215+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
216+
match *self {}
217+
}
218+
}
219+
220+
fn main() {}

0 commit comments

Comments
 (0)