Skip to content

Commit 127cdff

Browse files
committed
Auto merge of #5911 - hegza:issue-568, r=ebroto
Add lint for 'field_reassign_with_default` #568 changelog: Add lint for field_reassign_with_default that checks if mutable object + field modification is used to edit a binding initialized with Default::default() instead of struct constructor. Fixes #568 Notes: - Checks for reassignment of one or more fields of a binding initialized with Default::default(). - Implemented using EarlyLintPass, might be future proofed better with LateLintPass. - Does not trigger if Default::default() is used via another type implementing Default. - This is a re-open of [PR#4761](#4761), but I couldn't figure out how to re-open that one so here's a new one with the requested changes :S
2 parents c2cf40c + 7b203f3 commit 127cdff

File tree

7 files changed

+505
-67
lines changed

7 files changed

+505
-67
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,7 @@ Released 2018-09-13
17141714
[`extend_from_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#extend_from_slice
17151715
[`extra_unused_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes
17161716
[`fallible_impl_from`]: https://rust-lang.github.io/rust-clippy/master/index.html#fallible_impl_from
1717+
[`field_reassign_with_default`]: https://rust-lang.github.io/rust-clippy/master/index.html#field_reassign_with_default
17171718
[`filetype_is_file`]: https://rust-lang.github.io/rust-clippy/master/index.html#filetype_is_file
17181719
[`filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#filter_map
17191720
[`filter_map_next`]: https://rust-lang.github.io/rust-clippy/master/index.html#filter_map_next

clippy_lints/src/default.rs

+304
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
use crate::utils::{any_parent_is_automatically_derived, contains_name, match_def_path, paths, qpath_res, snippet};
2+
use crate::utils::{span_lint_and_note, span_lint_and_sugg};
3+
use if_chain::if_chain;
4+
use rustc_data_structures::fx::FxHashSet;
5+
use rustc_errors::Applicability;
6+
use rustc_hir::def::Res;
7+
use rustc_hir::{Block, Expr, ExprKind, PatKind, QPath, Stmt, StmtKind};
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_middle::ty::{self, Adt, Ty};
10+
use rustc_session::{declare_tool_lint, impl_lint_pass};
11+
use rustc_span::symbol::{Ident, Symbol};
12+
use rustc_span::Span;
13+
14+
declare_clippy_lint! {
15+
/// **What it does:** Checks for literal calls to `Default::default()`.
16+
///
17+
/// **Why is this bad?** It's more clear to the reader to use the name of the type whose default is
18+
/// being gotten than the generic `Default`.
19+
///
20+
/// **Known problems:** None.
21+
///
22+
/// **Example:**
23+
/// ```rust
24+
/// // Bad
25+
/// let s: String = Default::default();
26+
///
27+
/// // Good
28+
/// let s = String::default();
29+
/// ```
30+
pub DEFAULT_TRAIT_ACCESS,
31+
pedantic,
32+
"checks for literal calls to `Default::default()`"
33+
}
34+
35+
declare_clippy_lint! {
36+
/// **What it does:** Checks for immediate reassignment of fields initialized
37+
/// with Default::default().
38+
///
39+
/// **Why is this bad?**It's more idiomatic to use the [functional update syntax](https://doc.rust-lang.org/reference/expressions/struct-expr.html#functional-update-syntax).
40+
///
41+
/// **Known problems:** Assignments to patterns that are of tuple type are not linted.
42+
///
43+
/// **Example:**
44+
/// Bad:
45+
/// ```
46+
/// # #[derive(Default)]
47+
/// # struct A { i: i32 }
48+
/// let mut a: A = Default::default();
49+
/// a.i = 42;
50+
/// ```
51+
/// Use instead:
52+
/// ```
53+
/// # #[derive(Default)]
54+
/// # struct A { i: i32 }
55+
/// let a = A {
56+
/// i: 42,
57+
/// .. Default::default()
58+
/// };
59+
/// ```
60+
pub FIELD_REASSIGN_WITH_DEFAULT,
61+
style,
62+
"binding initialized with Default should have its fields set in the initializer"
63+
}
64+
65+
#[derive(Default)]
66+
pub struct Default {
67+
// Spans linted by `field_reassign_with_default`.
68+
reassigned_linted: FxHashSet<Span>,
69+
}
70+
71+
impl_lint_pass!(Default => [DEFAULT_TRAIT_ACCESS, FIELD_REASSIGN_WITH_DEFAULT]);
72+
73+
impl LateLintPass<'_> for Default {
74+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
75+
if_chain! {
76+
// Avoid cases already linted by `field_reassign_with_default`
77+
if !self.reassigned_linted.contains(&expr.span);
78+
if let ExprKind::Call(ref path, ..) = expr.kind;
79+
if !any_parent_is_automatically_derived(cx.tcx, expr.hir_id);
80+
if let ExprKind::Path(ref qpath) = path.kind;
81+
if let Some(def_id) = cx.qpath_res(qpath, path.hir_id).opt_def_id();
82+
if match_def_path(cx, def_id, &paths::DEFAULT_TRAIT_METHOD);
83+
// Detect and ignore <Foo as Default>::default() because these calls do explicitly name the type.
84+
if let QPath::Resolved(None, _path) = qpath;
85+
then {
86+
let expr_ty = cx.typeck_results().expr_ty(expr);
87+
if let ty::Adt(def, ..) = expr_ty.kind() {
88+
// TODO: Work out a way to put "whatever the imported way of referencing
89+
// this type in this file" rather than a fully-qualified type.
90+
let replacement = format!("{}::default()", cx.tcx.def_path_str(def.did));
91+
span_lint_and_sugg(
92+
cx,
93+
DEFAULT_TRAIT_ACCESS,
94+
expr.span,
95+
&format!("calling `{}` is more clear than this expression", replacement),
96+
"try",
97+
replacement,
98+
Applicability::Unspecified, // First resolve the TODO above
99+
);
100+
}
101+
}
102+
}
103+
}
104+
105+
fn check_block<'tcx>(&mut self, cx: &LateContext<'tcx>, block: &Block<'tcx>) {
106+
// find all binding statements like `let mut _ = T::default()` where `T::default()` is the
107+
// `default` method of the `Default` trait, and store statement index in current block being
108+
// checked and the name of the bound variable
109+
let binding_statements_using_default = enumerate_bindings_using_default(cx, block);
110+
111+
// start from the `let mut _ = _::default();` and look at all the following
112+
// statements, see if they re-assign the fields of the binding
113+
for (stmt_idx, binding_name, binding_type, span) in binding_statements_using_default {
114+
// the last statement of a block cannot trigger the lint
115+
if stmt_idx == block.stmts.len() - 1 {
116+
break;
117+
}
118+
119+
// find all "later statement"'s where the fields of the binding set as
120+
// Default::default() get reassigned, unless the reassignment refers to the original binding
121+
let mut first_assign = None;
122+
let mut assigned_fields = Vec::new();
123+
let mut cancel_lint = false;
124+
for consecutive_statement in &block.stmts[stmt_idx + 1..] {
125+
// interrupt if the statement is a let binding (`Local`) that shadows the original
126+
// binding
127+
if stmt_shadows_binding(consecutive_statement, binding_name) {
128+
break;
129+
}
130+
// find out if and which field was set by this `consecutive_statement`
131+
else if let Some((field_ident, assign_rhs)) =
132+
field_reassigned_by_stmt(consecutive_statement, binding_name)
133+
{
134+
// interrupt and cancel lint if assign_rhs references the original binding
135+
if contains_name(binding_name, assign_rhs) {
136+
cancel_lint = true;
137+
break;
138+
}
139+
140+
// if the field was previously assigned, replace the assignment, otherwise insert the assignment
141+
if let Some(prev) = assigned_fields
142+
.iter_mut()
143+
.find(|(field_name, _)| field_name == &field_ident.name)
144+
{
145+
*prev = (field_ident.name, assign_rhs);
146+
} else {
147+
assigned_fields.push((field_ident.name, assign_rhs));
148+
}
149+
150+
// also set first instance of error for help message
151+
if first_assign.is_none() {
152+
first_assign = Some(consecutive_statement);
153+
}
154+
}
155+
// interrupt also if no field was assigned, since we only want to look at consecutive statements
156+
else {
157+
break;
158+
}
159+
}
160+
161+
// if there are incorrectly assigned fields, do a span_lint_and_note to suggest
162+
// construction using `Ty { fields, ..Default::default() }`
163+
if !assigned_fields.is_empty() && !cancel_lint {
164+
// take the original assignment as span
165+
let stmt = &block.stmts[stmt_idx];
166+
167+
if let StmtKind::Local(preceding_local) = &stmt.kind {
168+
// filter out fields like `= Default::default()`, because the FRU already covers them
169+
let assigned_fields = assigned_fields
170+
.into_iter()
171+
.filter(|(_, rhs)| !is_expr_default(rhs, cx))
172+
.collect::<Vec<(Symbol, &Expr<'_>)>>();
173+
174+
// if all fields of the struct are not assigned, add `.. Default::default()` to the suggestion.
175+
let ext_with_default = !fields_of_type(binding_type)
176+
.iter()
177+
.all(|field| assigned_fields.iter().any(|(a, _)| a == &field.name));
178+
179+
let field_list = assigned_fields
180+
.into_iter()
181+
.map(|(field, rhs)| {
182+
// extract and store the assigned value for help message
183+
let value_snippet = snippet(cx, rhs.span, "..");
184+
format!("{}: {}", field, value_snippet)
185+
})
186+
.collect::<Vec<String>>()
187+
.join(", ");
188+
189+
let sugg = if ext_with_default {
190+
if field_list.is_empty() {
191+
format!("{}::default()", binding_type)
192+
} else {
193+
format!("{} {{ {}, ..Default::default() }}", binding_type, field_list)
194+
}
195+
} else {
196+
format!("{} {{ {} }}", binding_type, field_list)
197+
};
198+
199+
// span lint once per statement that binds default
200+
span_lint_and_note(
201+
cx,
202+
FIELD_REASSIGN_WITH_DEFAULT,
203+
first_assign.unwrap().span,
204+
"field assignment outside of initializer for an instance created with Default::default()",
205+
Some(preceding_local.span),
206+
&format!(
207+
"consider initializing the variable with `{}` and removing relevant reassignments",
208+
sugg
209+
),
210+
);
211+
self.reassigned_linted.insert(span);
212+
}
213+
}
214+
}
215+
}
216+
}
217+
218+
/// Checks if the given expression is the `default` method belonging to the `Default` trait.
219+
fn is_expr_default<'tcx>(expr: &'tcx Expr<'tcx>, cx: &LateContext<'tcx>) -> bool {
220+
if_chain! {
221+
if let ExprKind::Call(ref fn_expr, _) = &expr.kind;
222+
if let ExprKind::Path(qpath) = &fn_expr.kind;
223+
if let Res::Def(_, def_id) = qpath_res(cx, qpath, fn_expr.hir_id);
224+
then {
225+
// right hand side of assignment is `Default::default`
226+
match_def_path(cx, def_id, &paths::DEFAULT_TRAIT_METHOD)
227+
} else {
228+
false
229+
}
230+
}
231+
}
232+
233+
/// Returns the block indices, identifiers and types of bindings set as `Default::default()`, except
234+
/// for when the pattern type is a tuple.
235+
fn enumerate_bindings_using_default<'tcx>(
236+
cx: &LateContext<'tcx>,
237+
block: &Block<'tcx>,
238+
) -> Vec<(usize, Symbol, Ty<'tcx>, Span)> {
239+
block
240+
.stmts
241+
.iter()
242+
.enumerate()
243+
.filter_map(|(idx, stmt)| {
244+
if_chain! {
245+
// only take `let ...` statements
246+
if let StmtKind::Local(ref local) = stmt.kind;
247+
// only take bindings to identifiers
248+
if let PatKind::Binding(_, _, ident, _) = local.pat.kind;
249+
// that are not tuples
250+
let ty = cx.typeck_results().pat_ty(local.pat);
251+
if !matches!(ty.kind(), ty::Tuple(_));
252+
// only when assigning `... = Default::default()`
253+
if let Some(ref expr) = local.init;
254+
if is_expr_default(expr, cx);
255+
then {
256+
Some((idx, ident.name, ty, expr.span))
257+
} else {
258+
None
259+
}
260+
}
261+
})
262+
.collect()
263+
}
264+
265+
fn stmt_shadows_binding(this: &Stmt<'_>, shadowed: Symbol) -> bool {
266+
if let StmtKind::Local(local) = &this.kind {
267+
if let PatKind::Binding(_, _, ident, _) = local.pat.kind {
268+
return ident.name == shadowed;
269+
}
270+
}
271+
false
272+
}
273+
274+
/// Returns the reassigned field and the assigning expression (right-hand side of assign).
275+
fn field_reassigned_by_stmt<'tcx>(this: &Stmt<'tcx>, binding_name: Symbol) -> Option<(Ident, &'tcx Expr<'tcx>)> {
276+
if_chain! {
277+
// only take assignments
278+
if let StmtKind::Semi(ref later_expr) = this.kind;
279+
if let ExprKind::Assign(ref assign_lhs, ref assign_rhs, _) = later_expr.kind;
280+
// only take assignments to fields where the left-hand side field is a field of
281+
// the same binding as the previous statement
282+
if let ExprKind::Field(ref binding, field_ident) = assign_lhs.kind;
283+
if let ExprKind::Path(ref qpath) = binding.kind;
284+
if let QPath::Resolved(_, path) = qpath;
285+
if let Some(second_binding_name) = path.segments.last();
286+
if second_binding_name.ident.name == binding_name;
287+
then {
288+
Some((field_ident, assign_rhs))
289+
} else {
290+
None
291+
}
292+
}
293+
}
294+
295+
/// Returns the vec of fields for a struct and an empty vec for non-struct ADTs.
296+
fn fields_of_type(ty: Ty<'_>) -> Vec<Ident> {
297+
if let Adt(adt, _) = ty.kind() {
298+
if adt.is_struct() {
299+
let variant = &adt.non_enum_variant();
300+
return variant.fields.iter().map(|f| f.ident).collect();
301+
}
302+
}
303+
vec![]
304+
}

clippy_lints/src/default_trait_access.rs

-62
This file was deleted.

0 commit comments

Comments
 (0)