Skip to content

Commit 75f2c69

Browse files
author
Henri Lunnikivi
committed
Catch more than one assignment in field_reassign_with_default
1 parent 2371879 commit 75f2c69

File tree

3 files changed

+86
-36
lines changed

3 files changed

+86
-36
lines changed

clippy_lints/src/field_reassign_with_default.rs

+67-34
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::utils::{snippet, span_lint_and_note, match_path_ast, paths};
22
use if_chain::if_chain;
3-
use rustc_ast::ast::{BindingMode, Block, ExprKind, Mutability, PatKind, StmtKind};
3+
use rustc_ast::ast::{Block, ExprKind, PatKind, StmtKind};
44
use rustc_lint::{EarlyContext, EarlyLintPass};
55
use rustc_session::{declare_lint_pass, declare_tool_lint};
66
use rustc_span::symbol::Symbol;
@@ -50,8 +50,8 @@ impl EarlyLintPass for FieldReassignWithDefault {
5050
if_chain! {
5151
// only take `let ...` statements
5252
if let StmtKind::Local(ref local) = stmt.kind;
53-
// only take `... mut binding ...`
54-
if let PatKind::Ident(BindingMode::ByValue(Mutability::Mut), binding, _) = local.pat.kind;
53+
// only take bindings to identifiers
54+
if let PatKind::Ident(_, binding, _) = local.pat.kind;
5555
// only when assigning `... = Default::default()`
5656
if let Some(ref expr) = local.init;
5757
if let ExprKind::Call(ref fn_expr, _) = &expr.kind;
@@ -68,48 +68,81 @@ impl EarlyLintPass for FieldReassignWithDefault {
6868
})
6969
.collect::<Vec<_>>();
7070

71-
// look at all the following statements for the binding statements and see if they reassign
72-
// the fields of the binding
71+
// start from the `let mut binding = Default::default();` and look at all the following
72+
// statements, see if they re-assign the fields of the binding
7373
for (stmt_idx, binding_name) in binding_statements_using_default {
7474
// last statement of block cannot trigger the lint
7575
if stmt_idx == block.stmts.len() - 1 {
7676
break;
7777
}
7878

79-
// find "later statement"'s where the fields of the binding set by Default::default()
80-
// get reassigned
81-
let later_stmt = &block.stmts[stmt_idx + 1];
82-
if_chain! {
83-
// only take assignments
84-
if let StmtKind::Semi(ref later_expr) = later_stmt.kind;
85-
if let ExprKind::Assign(ref later_lhs, ref later_rhs, _) = later_expr.kind;
86-
// only take assignments to fields where the field refers to the same binding as the previous statement
87-
if let ExprKind::Field(ref binding_name_candidate, later_field_ident) = later_lhs.kind;
88-
if let ExprKind::Path( _, ref path ) = binding_name_candidate.kind;
89-
if let Some(second_binding_name) = path.segments.last();
90-
if second_binding_name.ident.name == binding_name;
91-
then {
92-
// take the preceding statement as span
93-
let stmt = &block.stmts[stmt_idx];
94-
if let StmtKind::Local(preceding_local) = &stmt.kind {
95-
// reorganize the latter assigment statement into `field` and `value` for the lint
96-
let field = later_field_ident.name.as_str();
97-
let value_snippet = snippet(cx, later_rhs.span, "..");
98-
if let Some(ty) = &preceding_local.ty {
99-
let ty_snippet = snippet(cx, ty.span, "_");
79+
// find all "later statement"'s where the fields of the binding set as
80+
// Default::default() get reassigned
81+
let mut first_assign = None;
82+
let mut assigned_fields = vec![];
83+
for post_defassign_stmt in &block.stmts[stmt_idx + 1..] {
84+
// interrupt if the statement is a let binding (`Local`) that shadows the original
85+
// binding
86+
if let StmtKind::Local(local) = &post_defassign_stmt.kind {
87+
if let PatKind::Ident(_, id, _) = local.pat.kind {
88+
if id.name == binding_name {
89+
break;
90+
}
91+
}
92+
}
93+
// statement kinds other than `StmtKind::Local` are valid, because they cannot
94+
// shadow a binding
95+
else {
96+
if_chain!{
97+
// only take assignments
98+
if let StmtKind::Semi(ref later_expr) = post_defassign_stmt.kind;
99+
if let ExprKind::Assign(ref assign_lhs, ref assign_rhs, _) = later_expr.kind;
100+
// only take assignments to fields where the left-hand side field is a field of
101+
// the same binding as the previous statement
102+
if let ExprKind::Field(ref binding, later_field_ident) = assign_lhs.kind;
103+
if let ExprKind::Path( _, ref path ) = binding.kind;
104+
if let Some(second_binding_name) = path.segments.last();
105+
if second_binding_name.ident.name == binding_name;
106+
then {
107+
// extract and store the name of the field and the assigned value for help message
108+
let field = later_field_ident.name;
109+
let value_snippet = snippet(cx, assign_rhs.span, "..");
110+
assigned_fields.push((field, value_snippet));
100111

101-
span_lint_and_note(
102-
cx,
103-
FIELD_REASSIGN_WITH_DEFAULT,
104-
later_stmt.span,
105-
"field assignment outside of initializer for an instance created with Default::default()",
106-
Some(preceding_local.span),
107-
&format!("consider initializing the variable immutably with `{} {{ {}: {}, ..Default::default() }}`", ty_snippet, field, value_snippet),
108-
);
112+
// also set first instance of error for help message
113+
if first_assign.is_none() {
114+
first_assign = Some(post_defassign_stmt);
115+
}
109116
}
110117
}
111118
}
112119
}
120+
121+
// if there are incorrectly assigned fields, do a span_lint_and_note to suggest
122+
// immutable construction using `Ty { fields, ..Default::default() }`
123+
if !assigned_fields.is_empty() {
124+
// take the preceding statement as span
125+
let stmt = &block.stmts[stmt_idx];
126+
if let StmtKind::Local(preceding_local) = &stmt.kind {
127+
if let Some(ty) = &preceding_local.ty {
128+
let ty_snippet = snippet(cx, ty.span, "_");
129+
130+
let field_list = assigned_fields.into_iter().map(|(field, value)| format!("{}: {}", field, value)).collect::<Vec<String>>().join(", ");
131+
132+
let sugg = format!("{} {{ {}, ..Default::default() }}", ty_snippet, field_list);
133+
134+
// span lint once per statement that binds default
135+
span_lint_and_note(
136+
cx,
137+
FIELD_REASSIGN_WITH_DEFAULT,
138+
first_assign.unwrap_or_else(|| unreachable!()).span,
139+
"field assignment outside of initializer for an instance created with Default::default()",
140+
Some(preceding_local.span),
141+
&format!("consider initializing the variable immutably with `{}`", sugg),
142+
);
143+
}
144+
}
145+
}
113146
}
114147
}
115148
}

tests/ui/field_reassign_with_default.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ struct B {
1212
}
1313

1414
fn main() {
15-
// wrong
15+
// wrong, produces first error in stderr
1616
let mut a: A = Default::default();
1717
a.i = 42;
1818

@@ -51,4 +51,9 @@ fn main() {
5151
let mut b = B { i: 15, j: 16 };
5252
let mut a: A = Default::default();
5353
b.i = 2;
54+
55+
// wrong, produces second error in stderr
56+
let mut a: A = Default::default();
57+
a.i = 42;
58+
a.j = 43;
5459
}

tests/ui/field_reassign_with_default.stderr

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,17 @@ note: consider initializing the variable immutably with `A { i: 42, ..Default::d
1111
LL | let mut a: A = Default::default();
1212
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1313

14-
error: aborting due to previous error
14+
error: field assignment outside of initializer for an instance created with Default::default()
15+
--> $DIR/field_reassign_with_default.rs:57:5
16+
|
17+
LL | a.i = 42;
18+
| ^^^^^^^^^
19+
|
20+
note: consider initializing the variable immutably with `A { i: 42, j: 43, ..Default::default() }`
21+
--> $DIR/field_reassign_with_default.rs:56:5
22+
|
23+
LL | let mut a: A = Default::default();
24+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
25+
26+
error: aborting due to 2 previous errors
1527

0 commit comments

Comments
 (0)