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](https://github.com/rust-lang/rust-clippy/pull/4761), but I couldn't figure out how to re-open that one so here's a new one with the requested changes :S
This commit is contained in:
bors 2020-11-04 22:21:44 +00:00
commit 5b52474ae6
7 changed files with 505 additions and 67 deletions

View File

@ -1714,6 +1714,7 @@ Released 2018-09-13
[`extend_from_slice`]: https://rust-lang.github.io/rust-clippy/master/index.html#extend_from_slice
[`extra_unused_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#extra_unused_lifetimes
[`fallible_impl_from`]: https://rust-lang.github.io/rust-clippy/master/index.html#fallible_impl_from
[`field_reassign_with_default`]: https://rust-lang.github.io/rust-clippy/master/index.html#field_reassign_with_default
[`filetype_is_file`]: https://rust-lang.github.io/rust-clippy/master/index.html#filetype_is_file
[`filter_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#filter_map
[`filter_map_next`]: https://rust-lang.github.io/rust-clippy/master/index.html#filter_map_next

304
clippy_lints/src/default.rs Normal file
View File

@ -0,0 +1,304 @@
use crate::utils::{any_parent_is_automatically_derived, contains_name, match_def_path, paths, qpath_res, snippet};
use crate::utils::{span_lint_and_note, span_lint_and_sugg};
use if_chain::if_chain;
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::Applicability;
use rustc_hir::def::Res;
use rustc_hir::{Block, Expr, ExprKind, PatKind, QPath, Stmt, StmtKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty::{self, Adt, Ty};
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::symbol::{Ident, Symbol};
use rustc_span::Span;
declare_clippy_lint! {
/// **What it does:** Checks for literal calls to `Default::default()`.
///
/// **Why is this bad?** It's more clear to the reader to use the name of the type whose default is
/// being gotten than the generic `Default`.
///
/// **Known problems:** None.
///
/// **Example:**
/// ```rust
/// // Bad
/// let s: String = Default::default();
///
/// // Good
/// let s = String::default();
/// ```
pub DEFAULT_TRAIT_ACCESS,
pedantic,
"checks for literal calls to `Default::default()`"
}
declare_clippy_lint! {
/// **What it does:** Checks for immediate reassignment of fields initialized
/// with Default::default().
///
/// **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).
///
/// **Known problems:** Assignments to patterns that are of tuple type are not linted.
///
/// **Example:**
/// Bad:
/// ```
/// # #[derive(Default)]
/// # struct A { i: i32 }
/// let mut a: A = Default::default();
/// a.i = 42;
/// ```
/// Use instead:
/// ```
/// # #[derive(Default)]
/// # struct A { i: i32 }
/// let a = A {
/// i: 42,
/// .. Default::default()
/// };
/// ```
pub FIELD_REASSIGN_WITH_DEFAULT,
style,
"binding initialized with Default should have its fields set in the initializer"
}
#[derive(Default)]
pub struct Default {
// Spans linted by `field_reassign_with_default`.
reassigned_linted: FxHashSet<Span>,
}
impl_lint_pass!(Default => [DEFAULT_TRAIT_ACCESS, FIELD_REASSIGN_WITH_DEFAULT]);
impl LateLintPass<'_> for Default {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if_chain! {
// Avoid cases already linted by `field_reassign_with_default`
if !self.reassigned_linted.contains(&expr.span);
if let ExprKind::Call(ref path, ..) = expr.kind;
if !any_parent_is_automatically_derived(cx.tcx, expr.hir_id);
if let ExprKind::Path(ref qpath) = path.kind;
if let Some(def_id) = cx.qpath_res(qpath, path.hir_id).opt_def_id();
if match_def_path(cx, def_id, &paths::DEFAULT_TRAIT_METHOD);
// Detect and ignore <Foo as Default>::default() because these calls do explicitly name the type.
if let QPath::Resolved(None, _path) = qpath;
then {
let expr_ty = cx.typeck_results().expr_ty(expr);
if let ty::Adt(def, ..) = expr_ty.kind() {
// TODO: Work out a way to put "whatever the imported way of referencing
// this type in this file" rather than a fully-qualified type.
let replacement = format!("{}::default()", cx.tcx.def_path_str(def.did));
span_lint_and_sugg(
cx,
DEFAULT_TRAIT_ACCESS,
expr.span,
&format!("calling `{}` is more clear than this expression", replacement),
"try",
replacement,
Applicability::Unspecified, // First resolve the TODO above
);
}
}
}
}
fn check_block<'tcx>(&mut self, cx: &LateContext<'tcx>, block: &Block<'tcx>) {
// find all binding statements like `let mut _ = T::default()` where `T::default()` is the
// `default` method of the `Default` trait, and store statement index in current block being
// checked and the name of the bound variable
let binding_statements_using_default = enumerate_bindings_using_default(cx, block);
// start from the `let mut _ = _::default();` and look at all the following
// statements, see if they re-assign the fields of the binding
for (stmt_idx, binding_name, binding_type, span) in binding_statements_using_default {
// the last statement of a block cannot trigger the lint
if stmt_idx == block.stmts.len() - 1 {
break;
}
// find all "later statement"'s where the fields of the binding set as
// Default::default() get reassigned, unless the reassignment refers to the original binding
let mut first_assign = None;
let mut assigned_fields = Vec::new();
let mut cancel_lint = false;
for consecutive_statement in &block.stmts[stmt_idx + 1..] {
// interrupt if the statement is a let binding (`Local`) that shadows the original
// binding
if stmt_shadows_binding(consecutive_statement, binding_name) {
break;
}
// find out if and which field was set by this `consecutive_statement`
else if let Some((field_ident, assign_rhs)) =
field_reassigned_by_stmt(consecutive_statement, binding_name)
{
// interrupt and cancel lint if assign_rhs references the original binding
if contains_name(binding_name, assign_rhs) {
cancel_lint = true;
break;
}
// if the field was previously assigned, replace the assignment, otherwise insert the assignment
if let Some(prev) = assigned_fields
.iter_mut()
.find(|(field_name, _)| field_name == &field_ident.name)
{
*prev = (field_ident.name, assign_rhs);
} else {
assigned_fields.push((field_ident.name, assign_rhs));
}
// also set first instance of error for help message
if first_assign.is_none() {
first_assign = Some(consecutive_statement);
}
}
// interrupt also if no field was assigned, since we only want to look at consecutive statements
else {
break;
}
}
// if there are incorrectly assigned fields, do a span_lint_and_note to suggest
// construction using `Ty { fields, ..Default::default() }`
if !assigned_fields.is_empty() && !cancel_lint {
// take the original assignment as span
let stmt = &block.stmts[stmt_idx];
if let StmtKind::Local(preceding_local) = &stmt.kind {
// filter out fields like `= Default::default()`, because the FRU already covers them
let assigned_fields = assigned_fields
.into_iter()
.filter(|(_, rhs)| !is_expr_default(rhs, cx))
.collect::<Vec<(Symbol, &Expr<'_>)>>();
// if all fields of the struct are not assigned, add `.. Default::default()` to the suggestion.
let ext_with_default = !fields_of_type(binding_type)
.iter()
.all(|field| assigned_fields.iter().any(|(a, _)| a == &field.name));
let field_list = assigned_fields
.into_iter()
.map(|(field, rhs)| {
// extract and store the assigned value for help message
let value_snippet = snippet(cx, rhs.span, "..");
format!("{}: {}", field, value_snippet)
})
.collect::<Vec<String>>()
.join(", ");
let sugg = if ext_with_default {
if field_list.is_empty() {
format!("{}::default()", binding_type)
} else {
format!("{} {{ {}, ..Default::default() }}", binding_type, field_list)
}
} else {
format!("{} {{ {} }}", binding_type, field_list)
};
// span lint once per statement that binds default
span_lint_and_note(
cx,
FIELD_REASSIGN_WITH_DEFAULT,
first_assign.unwrap().span,
"field assignment outside of initializer for an instance created with Default::default()",
Some(preceding_local.span),
&format!(
"consider initializing the variable with `{}` and removing relevant reassignments",
sugg
),
);
self.reassigned_linted.insert(span);
}
}
}
}
}
/// Checks if the given expression is the `default` method belonging to the `Default` trait.
fn is_expr_default<'tcx>(expr: &'tcx Expr<'tcx>, cx: &LateContext<'tcx>) -> bool {
if_chain! {
if let ExprKind::Call(ref fn_expr, _) = &expr.kind;
if let ExprKind::Path(qpath) = &fn_expr.kind;
if let Res::Def(_, def_id) = qpath_res(cx, qpath, fn_expr.hir_id);
then {
// right hand side of assignment is `Default::default`
match_def_path(cx, def_id, &paths::DEFAULT_TRAIT_METHOD)
} else {
false
}
}
}
/// Returns the block indices, identifiers and types of bindings set as `Default::default()`, except
/// for when the pattern type is a tuple.
fn enumerate_bindings_using_default<'tcx>(
cx: &LateContext<'tcx>,
block: &Block<'tcx>,
) -> Vec<(usize, Symbol, Ty<'tcx>, Span)> {
block
.stmts
.iter()
.enumerate()
.filter_map(|(idx, stmt)| {
if_chain! {
// only take `let ...` statements
if let StmtKind::Local(ref local) = stmt.kind;
// only take bindings to identifiers
if let PatKind::Binding(_, _, ident, _) = local.pat.kind;
// that are not tuples
let ty = cx.typeck_results().pat_ty(local.pat);
if !matches!(ty.kind(), ty::Tuple(_));
// only when assigning `... = Default::default()`
if let Some(ref expr) = local.init;
if is_expr_default(expr, cx);
then {
Some((idx, ident.name, ty, expr.span))
} else {
None
}
}
})
.collect()
}
fn stmt_shadows_binding(this: &Stmt<'_>, shadowed: Symbol) -> bool {
if let StmtKind::Local(local) = &this.kind {
if let PatKind::Binding(_, _, ident, _) = local.pat.kind {
return ident.name == shadowed;
}
}
false
}
/// Returns the reassigned field and the assigning expression (right-hand side of assign).
fn field_reassigned_by_stmt<'tcx>(this: &Stmt<'tcx>, binding_name: Symbol) -> Option<(Ident, &'tcx Expr<'tcx>)> {
if_chain! {
// only take assignments
if let StmtKind::Semi(ref later_expr) = this.kind;
if let ExprKind::Assign(ref assign_lhs, ref assign_rhs, _) = later_expr.kind;
// only take assignments to fields where the left-hand side field is a field of
// the same binding as the previous statement
if let ExprKind::Field(ref binding, field_ident) = assign_lhs.kind;
if let ExprKind::Path(ref qpath) = binding.kind;
if let QPath::Resolved(_, path) = qpath;
if let Some(second_binding_name) = path.segments.last();
if second_binding_name.ident.name == binding_name;
then {
Some((field_ident, assign_rhs))
} else {
None
}
}
}
/// Returns the vec of fields for a struct and an empty vec for non-struct ADTs.
fn fields_of_type(ty: Ty<'_>) -> Vec<Ident> {
if let Adt(adt, _) = ty.kind() {
if adt.is_struct() {
let variant = &adt.non_enum_variant();
return variant.fields.iter().map(|f| f.ident).collect();
}
}
vec![]
}

View File

@ -1,62 +0,0 @@
use if_chain::if_chain;
use rustc_errors::Applicability;
use rustc_hir::{Expr, ExprKind, QPath};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::ty;
use rustc_session::{declare_lint_pass, declare_tool_lint};
use crate::utils::{any_parent_is_automatically_derived, match_def_path, paths, span_lint_and_sugg};
declare_clippy_lint! {
/// **What it does:** Checks for literal calls to `Default::default()`.
///
/// **Why is this bad?** It's more clear to the reader to use the name of the type whose default is
/// being gotten than the generic `Default`.
///
/// **Known problems:** None.
///
/// **Example:**
/// ```rust
/// // Bad
/// let s: String = Default::default();
///
/// // Good
/// let s = String::default();
/// ```
pub DEFAULT_TRAIT_ACCESS,
pedantic,
"checks for literal calls to `Default::default()`"
}
declare_lint_pass!(DefaultTraitAccess => [DEFAULT_TRAIT_ACCESS]);
impl<'tcx> LateLintPass<'tcx> for DefaultTraitAccess {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
if_chain! {
if let ExprKind::Call(ref path, ..) = expr.kind;
if !any_parent_is_automatically_derived(cx.tcx, expr.hir_id);
if let ExprKind::Path(ref qpath) = path.kind;
if let Some(def_id) = cx.qpath_res(qpath, path.hir_id).opt_def_id();
if match_def_path(cx, def_id, &paths::DEFAULT_TRAIT_METHOD);
// Detect and ignore <Foo as Default>::default() because these calls do explicitly name the type.
if let QPath::Resolved(None, _path) = qpath;
then {
let expr_ty = cx.typeck_results().expr_ty(expr);
if let ty::Adt(def, ..) = expr_ty.kind() {
// TODO: Work out a way to put "whatever the imported way of referencing
// this type in this file" rather than a fully-qualified type.
let replacement = format!("{}::default()", cx.tcx.def_path_str(def.did));
span_lint_and_sugg(
cx,
DEFAULT_TRAIT_ACCESS,
expr.span,
&format!("calling `{}` is more clear than this expression", replacement),
"try",
replacement,
Applicability::Unspecified, // First resolve the TODO above
);
}
}
}
}
}

View File

@ -176,7 +176,7 @@ macro_rules! declare_clippy_lint {
mod copy_iterator;
mod create_dir;
mod dbg_macro;
mod default_trait_access;
mod default;
mod dereference;
mod derive;
mod disallowed_method;
@ -539,7 +539,8 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
&copy_iterator::COPY_ITERATOR,
&create_dir::CREATE_DIR,
&dbg_macro::DBG_MACRO,
&default_trait_access::DEFAULT_TRAIT_ACCESS,
&default::DEFAULT_TRAIT_ACCESS,
&default::FIELD_REASSIGN_WITH_DEFAULT,
&dereference::EXPLICIT_DEREF_METHODS,
&derive::DERIVE_HASH_XOR_EQ,
&derive::DERIVE_ORD_XOR_PARTIAL_ORD,
@ -1055,7 +1056,6 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
store.register_late_pass(|| box neg_cmp_op_on_partial_ord::NoNegCompOpForPartialOrd);
store.register_late_pass(|| box unwrap::Unwrap);
store.register_late_pass(|| box duration_subsec::DurationSubsec);
store.register_late_pass(|| box default_trait_access::DefaultTraitAccess);
store.register_late_pass(|| box indexing_slicing::IndexingSlicing);
store.register_late_pass(|| box non_copy_const::NonCopyConst);
store.register_late_pass(|| box ptr_offset_with_cast::PtrOffsetWithCast);
@ -1106,6 +1106,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
let enum_variant_name_threshold = conf.enum_variant_name_threshold;
store.register_early_pass(move || box enum_variants::EnumVariantNames::new(enum_variant_name_threshold));
store.register_early_pass(|| box tabs_in_doc_comments::TabsInDocComments);
store.register_late_pass(|| box default::Default::default());
store.register_late_pass(|| box unused_self::UnusedSelf);
store.register_late_pass(|| box mutable_debug_assertion::DebugAssertWithMutCall);
store.register_late_pass(|| box exit::Exit);
@ -1219,7 +1220,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
LintId::of(&checked_conversions::CHECKED_CONVERSIONS),
LintId::of(&copies::SAME_FUNCTIONS_IN_IF_CONDITION),
LintId::of(&copy_iterator::COPY_ITERATOR),
LintId::of(&default_trait_access::DEFAULT_TRAIT_ACCESS),
LintId::of(&default::DEFAULT_TRAIT_ACCESS),
LintId::of(&dereference::EXPLICIT_DEREF_METHODS),
LintId::of(&derive::EXPL_IMPL_CLONE_ON_COPY),
LintId::of(&derive::UNSAFE_DERIVE_DESERIALIZE),
@ -1330,6 +1331,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
LintId::of(&comparison_chain::COMPARISON_CHAIN),
LintId::of(&copies::IFS_SAME_COND),
LintId::of(&copies::IF_SAME_THEN_ELSE),
LintId::of(&default::FIELD_REASSIGN_WITH_DEFAULT),
LintId::of(&derive::DERIVE_HASH_XOR_EQ),
LintId::of(&derive::DERIVE_ORD_XOR_PARTIAL_ORD),
LintId::of(&doc::MISSING_SAFETY_DOC),
@ -1591,6 +1593,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
LintId::of(&blocks_in_if_conditions::BLOCKS_IN_IF_CONDITIONS),
LintId::of(&collapsible_if::COLLAPSIBLE_IF),
LintId::of(&comparison_chain::COMPARISON_CHAIN),
LintId::of(&default::FIELD_REASSIGN_WITH_DEFAULT),
LintId::of(&doc::MISSING_SAFETY_DOC),
LintId::of(&doc::NEEDLESS_DOCTEST_MAIN),
LintId::of(&enum_variants::ENUM_VARIANT_NAMES),

View File

@ -359,7 +359,7 @@
group: "pedantic",
desc: "checks for literal calls to `Default::default()`",
deprecation: None,
module: "default_trait_access",
module: "default",
},
Lint {
name: "deprecated_cfg_attr",
@ -627,6 +627,13 @@
deprecation: None,
module: "fallible_impl_from",
},
Lint {
name: "field_reassign_with_default",
group: "style",
desc: "binding initialized with Default should have its fields set in the initializer",
deprecation: None,
module: "default",
},
Lint {
name: "filetype_is_file",
group: "restriction",

View File

@ -0,0 +1,110 @@
#![warn(clippy::field_reassign_with_default)]
#[derive(Default)]
struct A {
i: i32,
j: i64,
}
struct B {
i: i32,
j: i64,
}
/// Implements .next() that returns a different number each time.
struct SideEffect(i32);
impl SideEffect {
fn new() -> SideEffect {
SideEffect(0)
}
fn next(&mut self) -> i32 {
self.0 += 1;
self.0
}
}
fn main() {
// wrong, produces first error in stderr
let mut a: A = Default::default();
a.i = 42;
// right
let mut a: A = Default::default();
// right
let a = A {
i: 42,
..Default::default()
};
// right
let mut a: A = Default::default();
if a.i == 0 {
a.j = 12;
}
// right
let mut a: A = Default::default();
let b = 5;
// right
let mut b = 32;
let mut a: A = Default::default();
b = 2;
// right
let b: B = B { i: 42, j: 24 };
// right
let mut b: B = B { i: 42, j: 24 };
b.i = 52;
// right
let mut b = B { i: 15, j: 16 };
let mut a: A = Default::default();
b.i = 2;
// wrong, produces second error in stderr
let mut a: A = Default::default();
a.j = 43;
a.i = 42;
// wrong, produces third error in stderr
let mut a: A = Default::default();
a.i = 42;
a.j = 43;
a.j = 44;
// wrong, produces fourth error in stderr
let mut a = A::default();
a.i = 42;
// wrong, but does not produce an error in stderr, because we can't produce a correct kind of
// suggestion with current implementation
let mut c: (i32, i32) = Default::default();
c.0 = 42;
c.1 = 21;
// wrong, produces the fifth error in stderr
let mut a: A = Default::default();
a.i = Default::default();
// wrong, produces the sixth error in stderr
let mut a: A = Default::default();
a.i = Default::default();
a.j = 45;
// right, because an assignment refers to another field
let mut x = A::default();
x.i = 42;
x.j = 21 + x.i as i64;
// right, we bail out if there's a reassignment to the same variable, since there is a risk of
// side-effects affecting the outcome
let mut x = A::default();
let mut side_effect = SideEffect::new();
x.i = side_effect.next();
x.j = 2;
x.i = side_effect.next();
}

View File

@ -0,0 +1,75 @@
error: field assignment outside of initializer for an instance created with Default::default()
--> $DIR/field_reassign_with_default.rs:30:5
|
LL | a.i = 42;
| ^^^^^^^^^
|
= note: `-D clippy::field-reassign-with-default` implied by `-D warnings`
note: consider initializing the variable with `A { i: 42, ..Default::default() }` and removing relevant reassignments
--> $DIR/field_reassign_with_default.rs:29:5
|
LL | let mut a: A = Default::default();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: field assignment outside of initializer for an instance created with Default::default()
--> $DIR/field_reassign_with_default.rs:70:5
|
LL | a.j = 43;
| ^^^^^^^^^
|
note: consider initializing the variable with `A { j: 43, i: 42 }` and removing relevant reassignments
--> $DIR/field_reassign_with_default.rs:69:5
|
LL | let mut a: A = Default::default();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: field assignment outside of initializer for an instance created with Default::default()
--> $DIR/field_reassign_with_default.rs:75:5
|
LL | a.i = 42;
| ^^^^^^^^^
|
note: consider initializing the variable with `A { i: 42, j: 44 }` and removing relevant reassignments
--> $DIR/field_reassign_with_default.rs:74:5
|
LL | let mut a: A = Default::default();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: field assignment outside of initializer for an instance created with Default::default()
--> $DIR/field_reassign_with_default.rs:81:5
|
LL | a.i = 42;
| ^^^^^^^^^
|
note: consider initializing the variable with `A { i: 42, ..Default::default() }` and removing relevant reassignments
--> $DIR/field_reassign_with_default.rs:80:5
|
LL | let mut a = A::default();
| ^^^^^^^^^^^^^^^^^^^^^^^^^
error: field assignment outside of initializer for an instance created with Default::default()
--> $DIR/field_reassign_with_default.rs:91:5
|
LL | a.i = Default::default();
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: consider initializing the variable with `A::default()` and removing relevant reassignments
--> $DIR/field_reassign_with_default.rs:90:5
|
LL | let mut a: A = Default::default();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: field assignment outside of initializer for an instance created with Default::default()
--> $DIR/field_reassign_with_default.rs:95:5
|
LL | a.i = Default::default();
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: consider initializing the variable with `A { j: 45, ..Default::default() }` and removing relevant reassignments
--> $DIR/field_reassign_with_default.rs:94:5
|
LL | let mut a: A = Default::default();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to 6 previous errors