-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New lint: explicit_struct_update
#15110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
New lint: explicit_struct_update
#15110
Conversation
rustbot has assigned @samueltardieu. Use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is a very interesting lint.
Can you please add tests in which part of the expression for the initialization comes from a macro, or the copy happens in a macro? You don't seem to deal with macros at all, how should the lint behave in this case?
let struct_indent = snippet_indent(cx, expr.span).unwrap_or_default(); | ||
let field_indent = format!("{struct_indent} "); | ||
let struct_type = snippet(cx, path.span(), ""); | ||
let struct_fields = non_update_fields_spans | ||
.iter() | ||
.fold(String::new(), |mut acc, &field_span| { | ||
acc.push_str(&field_indent); | ||
acc.push_str(&snippet(cx, field_span, "")); | ||
acc.push_str(",\n"); | ||
acc | ||
}); | ||
let struct_update_snip = snippet(cx, update_base.span, ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should either use the snippet with applicability variants (so that applicability can be adjusted), or return early if you cannot obtain a snippet (or the indentation of a snippet). This should never happen in practice, but it is not desirable to see a MachineApplicable
applied when obtaining snippets may have failed.
|
||
span_lint_and_sugg( | ||
cx, | ||
EXPLICIT_STRUCT_UPDATE, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lint name should probably use the plural form, as hinted in the guide.
if hir::is_range_literal(expr) { | ||
// range literals are lowered to structs, false positive | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a test for this?
I wonder if it would not be more efficient to just check, below, that path
is not a QPath::LangItem(…)
, as it would indicate that the code comes from lowering and can probably not be modified anyway.
// collect the fields that are being initialized with the same field from another struct of the same | ||
// type | ||
let update_fields = fields.iter().try_fold( | ||
Vec::new(), | ||
|mut acc: Vec<(&rustc_hir::Expr<'_>, &rustc_hir::Expr<'_>)>, f| { | ||
if let ExprKind::Field(base_expr, field_ident) = f.expr.kind { | ||
if let Some(last) = acc.last() { | ||
match (last.1.kind, base_expr.kind) { | ||
( | ||
ExprKind::Path(hir::QPath::Resolved(_, hir::Path { res: res_a, .. })), | ||
ExprKind::Path(hir::QPath::Resolved(_, hir::Path { res: res_b, .. })), | ||
) if res_a != res_b => return None, /* if we detect instantiation from multiple bases, we */ | ||
// don't want to lint | ||
_ => (), | ||
} | ||
} | ||
|
||
if cx.typeck_results().expr_ty(base_expr) == ty && f.ident == field_ident { | ||
// accumulate the expressions mapping to the actual field expression, and the expression of the | ||
// base struct, we do this so we can determine if the base struct is the same for all | ||
acc.push((f.expr, base_expr)); | ||
} | ||
} | ||
|
||
Some(acc) | ||
}, | ||
); | ||
|
||
let (update_base, update_fields): (_, Vec<_>) = match update_fields { | ||
// we only care about the field expressions at this point | ||
Some(fields) if fields.len() > 1 => (fields[0].1, fields.iter().map(|x| x.0.hir_id).collect()), | ||
// no lint if there's no fields or multiple bases | ||
_ => return, | ||
}; | ||
|
||
// the field assignments we are keeping | ||
let non_update_fields_spans: Vec<_> = fields | ||
.iter() | ||
.filter_map(|f| { | ||
if update_fields.contains(&f.expr.hir_id) { | ||
None | ||
} else { | ||
Some(f.span) | ||
} | ||
}) | ||
.collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could do this more concisely, with something like:
// collect the fields that are being initialized with the same field from another struct of the same | |
// type | |
let update_fields = fields.iter().try_fold( | |
Vec::new(), | |
|mut acc: Vec<(&rustc_hir::Expr<'_>, &rustc_hir::Expr<'_>)>, f| { | |
if let ExprKind::Field(base_expr, field_ident) = f.expr.kind { | |
if let Some(last) = acc.last() { | |
match (last.1.kind, base_expr.kind) { | |
( | |
ExprKind::Path(hir::QPath::Resolved(_, hir::Path { res: res_a, .. })), | |
ExprKind::Path(hir::QPath::Resolved(_, hir::Path { res: res_b, .. })), | |
) if res_a != res_b => return None, /* if we detect instantiation from multiple bases, we */ | |
// don't want to lint | |
_ => (), | |
} | |
} | |
if cx.typeck_results().expr_ty(base_expr) == ty && f.ident == field_ident { | |
// accumulate the expressions mapping to the actual field expression, and the expression of the | |
// base struct, we do this so we can determine if the base struct is the same for all | |
acc.push((f.expr, base_expr)); | |
} | |
} | |
Some(acc) | |
}, | |
); | |
let (update_base, update_fields): (_, Vec<_>) = match update_fields { | |
// we only care about the field expressions at this point | |
Some(fields) if fields.len() > 1 => (fields[0].1, fields.iter().map(|x| x.0.hir_id).collect()), | |
// no lint if there's no fields or multiple bases | |
_ => return, | |
}; | |
// the field assignments we are keeping | |
let non_update_fields_spans: Vec<_> = fields | |
.iter() | |
.filter_map(|f| { | |
if update_fields.contains(&f.expr.hir_id) { | |
None | |
} else { | |
Some(f.span) | |
} | |
}) | |
.collect(); | |
// collect the fields that are being initialized with the same field from another struct of the same | |
// type | |
let (mut update_base, mut update_fields, mut non_update_fields_spans) = (None, vec![], vec![]); | |
for field in fields { | |
if let ExprKind::Field(base_expr, field_ident) = field.expr.kind | |
&& let ExprKind::Path(hir::QPath::Resolved(_, hir::Path { res, .. })) = base_expr.kind | |
&& field_ident == field.ident | |
&& cx.typeck_results().expr_ty(base_expr) == ty | |
{ | |
if matches!(update_base.replace((res, base_expr.span)), Some((base_res, _)) if base_res != res) { | |
// Initialization from multiple bases | |
return; | |
} | |
update_fields.push(field.hir_id); | |
} else { | |
non_update_fields_spans.push(field.span); | |
} | |
} | |
let update_base_span = match update_base { | |
Some((_, span)) if update_fields.len() > 1 => span, | |
_ => return, | |
}; |
Adds a new lint called
explicit_struct_update
that checks if struct field instantiation for 2 or more fields can be replaced with struct update syntax. For example:becomes
I've given it the category of
complexity
, but I believepedantic
could also fit. I'm also not sure if this is the best name.changelog: [
explicit_struct_update
]: new lint