Skip to content

Commit 40ecb57

Browse files
committed
consider string literal in .field() calls as used
1 parent 4c2a57b commit 40ecb57

File tree

4 files changed

+77
-67
lines changed

4 files changed

+77
-67
lines changed

clippy_lints/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
961961
store.register_late_pass(|_| Box::new(tests_outside_test_module::TestsOutsideTestModule));
962962
store.register_late_pass(|_| Box::new(manual_slice_size_calculation::ManualSliceSizeCalculation));
963963
store.register_early_pass(|| Box::new(suspicious_doc_comments::SuspiciousDocComments));
964-
store.register_late_pass(|_| Box::new(missing_fields_in_debug::MissingFieldInDebug));
964+
store.register_late_pass(|_| Box::new(missing_fields_in_debug::MissingFieldsInDebug));
965965
// add lints here, do not remove this comment, it's used in `new_lint`
966966
}
967967

clippy_lints/src/missing_fields_in_debug.rs

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ use clippy_utils::{
66
ty::match_type,
77
visitors::{for_each_expr, Visitable},
88
};
9+
use rustc_ast::LitKind;
910
use rustc_data_structures::fx::FxHashSet;
1011
use rustc_hir::{
1112
def::{DefKind, Res},
12-
ImplItemKind, MatchSource, Node,
13+
Expr, ImplItemKind, MatchSource, Node,
1314
};
1415
use rustc_hir::{Block, PatKind};
1516
use rustc_hir::{ExprKind, Impl, ItemKind, QPath, TyKind};
@@ -18,7 +19,7 @@ use rustc_lint::{LateContext, LateLintPass};
1819
use rustc_middle::ty::Ty;
1920
use rustc_middle::ty::TypeckResults;
2021
use rustc_session::{declare_lint_pass, declare_tool_lint};
21-
use rustc_span::{sym, Span};
22+
use rustc_span::{sym, Span, Symbol};
2223

2324
declare_clippy_lint! {
2425
/// ### What it does
@@ -32,8 +33,8 @@ declare_clippy_lint! {
3233
/// for the times when the user intentionally wants to leave out certain fields (e.g. to hide implementation details).
3334
///
3435
/// ### Known problems
35-
/// This lint works based on the `DebugStruct` and `DebugTuple` helper types provided by
36-
/// the `Formatter`, so this won't detect `Debug` impls that use the `write!` macro.
36+
/// This lint works based on the `DebugStruct` helper types provided by the `Formatter`,
37+
/// so this won't detect `Debug` impls that use the `write!` macro.
3738
/// Oftentimes there is more logic to a `Debug` impl if it uses `write!` macro, so it tries
3839
/// to be on the conservative side and not lint in those cases in an attempt to prevent false positives.
3940
///
@@ -79,7 +80,7 @@ declare_clippy_lint! {
7980
pedantic,
8081
"missing fields in manual `Debug` implementation"
8182
}
82-
declare_lint_pass!(MissingFieldInDebug => [MISSING_FIELDS_IN_DEBUG]);
83+
declare_lint_pass!(MissingFieldsInDebug => [MISSING_FIELDS_IN_DEBUG]);
8384

8485
fn report_lints(cx: &LateContext<'_>, span: Span, span_notes: Vec<(Span, &'static str)>) {
8586
span_lint_and_then(
@@ -100,35 +101,49 @@ fn report_lints(cx: &LateContext<'_>, span: Span, span_notes: Vec<(Span, &'stati
100101
/// Checks if we should lint in a block of code
101102
///
102103
/// The way we check for this condition is by checking if there is
103-
/// a call to `Formatter::debug_struct` or `Formatter::debug_tuple`
104-
/// and no call to `.finish_non_exhaustive()`.
104+
/// a call to `Formatter::debug_struct` but no call to `.finish_non_exhaustive()`.
105105
fn should_lint<'tcx>(
106106
cx: &LateContext<'tcx>,
107107
typeck_results: &TypeckResults<'tcx>,
108108
block: impl Visitable<'tcx>,
109109
) -> bool {
110110
let mut has_finish_non_exhaustive = false;
111-
let mut has_debug_struct_tuple = false;
111+
let mut has_debug_struct = false;
112112

113-
let _: Option<!> = for_each_expr(block, |expr| {
114-
let ExprKind::MethodCall(path, recv, ..) = &expr.kind else {
115-
return ControlFlow::Continue(());
116-
};
117-
let recv_ty = typeck_results.expr_ty(recv).peel_refs();
118-
119-
if [sym::debug_struct, sym::debug_tuple].contains(&path.ident.name)
120-
&& match_type(cx, recv_ty, &paths::FORMATTER)
121-
{
122-
has_debug_struct_tuple = true;
123-
}
113+
for_each_expr(block, |expr| {
114+
if let ExprKind::MethodCall(path, recv, ..) = &expr.kind {
115+
let recv_ty = typeck_results.expr_ty(recv).peel_refs();
124116

125-
if path.ident.name.as_str() == "finish_non_exhaustive" && match_type(cx, recv_ty, &paths::DEBUG_STRUCT) {
126-
has_finish_non_exhaustive = true;
117+
if path.ident.name == sym::debug_struct && match_type(cx, recv_ty, &paths::FORMATTER) {
118+
has_debug_struct = true;
119+
} else if path.ident.name == sym!(finish_non_exhaustive) && match_type(cx, recv_ty, &paths::DEBUG_STRUCT) {
120+
has_finish_non_exhaustive = true;
121+
}
127122
}
128-
ControlFlow::Continue(())
123+
ControlFlow::<!, _>::Continue(())
129124
});
130125

131-
!has_finish_non_exhaustive && has_debug_struct_tuple
126+
!has_finish_non_exhaustive && has_debug_struct
127+
}
128+
129+
/// Checks if the given expression is a call to `DebugStruct::field`
130+
/// and the first argument to it is a string literal and if so, returns it
131+
fn as_field_call<'tcx>(
132+
cx: &LateContext<'tcx>,
133+
typeck_results: &TypeckResults<'tcx>,
134+
expr: &Expr<'_>,
135+
) -> Option<Symbol> {
136+
if let ExprKind::MethodCall(path, recv, [debug_field, _], _) = &expr.kind
137+
&& let recv_ty = typeck_results.expr_ty(recv).peel_refs()
138+
&& match_type(cx, recv_ty, &paths::DEBUG_STRUCT)
139+
&& path.ident.name == sym!(field)
140+
&& let ExprKind::Lit(lit) = &debug_field.kind
141+
&& let LitKind::Str(sym, ..) = lit.node
142+
{
143+
Some(sym)
144+
} else {
145+
None
146+
}
132147
}
133148

134149
/// Attempts to find unused fields assuming that the item is a struct
@@ -140,33 +155,37 @@ fn check_struct<'tcx>(
140155
item: &'tcx Item<'tcx>,
141156
data: &VariantData<'_>,
142157
) {
158+
let mut has_direct_field_access = false;
143159
let mut field_accesses = FxHashSet::default();
144160

145-
let _: Option<!> = for_each_expr(block, |expr| {
161+
for_each_expr(block, |expr| {
146162
if let ExprKind::Field(target, ident) = expr.kind
147163
&& let target_ty = typeck_results.expr_ty(target).peel_refs()
148164
&& target_ty == self_ty
149165
{
150-
field_accesses.insert(ident);
166+
has_direct_field_access = true;
167+
field_accesses.insert(ident.name);
168+
} else if let Some(sym) = as_field_call(cx, typeck_results, expr) {
169+
field_accesses.insert(sym);
151170
}
152-
ControlFlow::Continue(())
171+
ControlFlow::<!, _>::Continue(())
153172
});
154173

155174
let span_notes = data
156175
.fields()
157176
.iter()
158177
.filter_map(|field| {
159-
if field_accesses.contains(&field.ident) {
178+
if field_accesses.contains(&field.ident.name) {
160179
None
161180
} else {
162181
Some((field.span, "this field is unused"))
163182
}
164183
})
165184
.collect::<Vec<_>>();
166185

167-
// only lint if there's also at least one field access to allow patterns
186+
// only lint if there's also at least one direct field access to allow patterns
168187
// where one might have a newtype struct and uses fields from the wrapped type
169-
if !span_notes.is_empty() && !field_accesses.is_empty() {
188+
if !span_notes.is_empty() && has_direct_field_access {
170189
report_lints(cx, item.span, span_notes);
171190
}
172191
}
@@ -216,18 +235,22 @@ fn check_enum<'tcx>(
216235
});
217236

218237
let mut field_accesses = FxHashSet::default();
238+
let mut check_field_access = |sym| {
239+
arm.pat.each_binding(|_, _, _, pat_ident| {
240+
if sym == pat_ident.name {
241+
field_accesses.insert(pat_ident);
242+
}
243+
});
244+
};
219245

220-
let _: Option<!> = for_each_expr(arm.body, |expr| {
221-
if let ExprKind::Path(QPath::Resolved(_, path)) = expr.kind
222-
&& let Some(segment) = path.segments.first()
246+
for_each_expr(arm.body, |expr| {
247+
if let ExprKind::Path(QPath::Resolved(_, path)) = expr.kind && let Some(segment) = path.segments.first()
223248
{
224-
arm.pat.each_binding(|_, _, _, pat_ident| {
225-
if segment.ident == pat_ident {
226-
field_accesses.insert(pat_ident);
227-
}
228-
});
249+
check_field_access(segment.ident.name);
250+
} else if let Some(sym) = as_field_call(cx, typeck_results, expr) {
251+
check_field_access(sym);
229252
}
230-
ControlFlow::Continue(())
253+
ControlFlow::<!, _>::Continue(())
231254
});
232255

233256
arm.pat.each_binding(|_, _, span, pat_ident| {
@@ -242,7 +265,7 @@ fn check_enum<'tcx>(
242265
}
243266
}
244267

245-
impl<'tcx> LateLintPass<'tcx> for MissingFieldInDebug {
268+
impl<'tcx> LateLintPass<'tcx> for MissingFieldsInDebug {
246269
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx rustc_hir::Item<'tcx>) {
247270
// is this an `impl Debug for X` block?
248271
if let ItemKind::Impl(Impl { of_trait: Some(trait_ref), self_ty, items, .. }) = item.kind

tests/ui/missing_fields_in_debug.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ impl fmt::Debug for UnnamedStruct1Ignored {
5656

5757
struct UnnamedStructMultipleIgnored(String, Vec<u8>, i32);
5858

59+
// tuple structs are not linted
5960
impl fmt::Debug for UnnamedStructMultipleIgnored {
60-
// unused fields: 0, 2
6161
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
6262
formatter
6363
.debug_tuple("UnnamedStructMultipleIgnored")
@@ -257,4 +257,16 @@ impl fmt::Debug for HasInner {
257257
}
258258
}
259259

260+
// https://github.com/rust-lang/rust-clippy/pull/10616#discussion_r1170306053
261+
struct Foo {
262+
a: u8,
263+
b: u8,
264+
}
265+
266+
impl fmt::Debug for Foo {
267+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268+
f.debug_struct("Foo").field("a", &self.a).field("b", &()).finish()
269+
}
270+
}
271+
260272
fn main() {}

tests/ui/missing_fields_in_debug.stderr

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -49,31 +49,6 @@ LL | hidden4: ((((u8), u16), u32), u64),
4949
= help: consider including all fields in this `Debug` impl
5050
= help: consider calling `.finish_non_exhaustive()` if you intend to ignore fields
5151

52-
error: manual `Debug` impl does not include all fields
53-
--> $DIR/missing_fields_in_debug.rs:59:1
54-
|
55-
LL | / impl fmt::Debug for UnnamedStructMultipleIgnored {
56-
LL | | // unused fields: 0, 2
57-
LL | | fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58-
LL | | formatter
59-
... |
60-
LL | | }
61-
LL | | }
62-
| |_^
63-
|
64-
note: this field is unused
65-
--> $DIR/missing_fields_in_debug.rs:57:37
66-
|
67-
LL | struct UnnamedStructMultipleIgnored(String, Vec<u8>, i32);
68-
| ^^^^^^
69-
note: this field is unused
70-
--> $DIR/missing_fields_in_debug.rs:57:54
71-
|
72-
LL | struct UnnamedStructMultipleIgnored(String, Vec<u8>, i32);
73-
| ^^^
74-
= help: consider including all fields in this `Debug` impl
75-
= help: consider calling `.finish_non_exhaustive()` if you intend to ignore fields
76-
7752
error: manual `Debug` impl does not include all fields
7853
--> $DIR/missing_fields_in_debug.rs:90:1
7954
|
@@ -134,5 +109,5 @@ LL | Self::B { b, .. } => formatter.debug_struct("B").field("b", &b)
134109
= help: consider including all fields in this `Debug` impl
135110
= help: consider calling `.finish_non_exhaustive()` if you intend to ignore fields
136111

137-
error: aborting due to 6 previous errors
112+
error: aborting due to 5 previous errors
138113

0 commit comments

Comments
 (0)