-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New lint: struct_fields_rest_default
#14412
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: struct_fields_rest_default
#14412
Conversation
r? @Manishearth rustbot has assigned @Manishearth. Use |
2a1f974
to
1a98f1f
Compare
1a98f1f
to
b11506a
Compare
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 current code would have a false positive for the extra test case to add in your main
function:
{
struct Default;
impl Default {
fn default() -> Foo {
Foo { a: 0, b: 0, c: 0 }
}
}
// should not lint
let _ = Foo { a: 0, ..Default::default() };
}
Hence the proposal to switch to a late lint.
But also, why restrict this check to default()
? Wouldn't any base value have the same issue if the goal is to force the developer to initialize all the fields?
if let ExprKind::Struct(struct_expr) = &expr.kind | ||
&& let StructRest::Base(base) = &struct_expr.rest | ||
&& !base.span.in_external_macro(cx.sess().source_map()) | ||
&& let ExprKind::Call(call, _) = &base.kind | ||
&& let ExprKind::Path(_, path) = &call.kind | ||
&& let [part1, part2] = path.segments.as_slice() | ||
&& part1.ident.name == rustc_span::sym::Default | ||
&& part2.ident.name == rustc_span::kw::Default |
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.
This will match even if the Default
that is in scope is not the one from the standard library. If you had used a late lint, you would be able to match against the real Default::default()
diagnostic item as paths would be resolved. Also, you would be able to match against Foo::default()
in your tests, which is the same thing as Default::default()
.
With a late lint, you could use something like the shorter:
if let ExprKind::Struct(struct_expr) = &expr.kind | |
&& let StructRest::Base(base) = &struct_expr.rest | |
&& !base.span.in_external_macro(cx.sess().source_map()) | |
&& let ExprKind::Call(call, _) = &base.kind | |
&& let ExprKind::Path(_, path) = &call.kind | |
&& let [part1, part2] = path.segments.as_slice() | |
&& part1.ident.name == rustc_span::sym::Default | |
&& part2.ident.name == rustc_span::kw::Default | |
if let ExprKind::Struct(_, _, StructTailExpr::Base(base)) = &expr.kind | |
&& !base.span.in_external_macro(cx.sess().source_map()) | |
&& let ExprKind::Call(func, _) = base.kind | |
&& let Some(did) = path_def_id(cx, func) | |
&& cx.tcx.is_diagnostic_item(sym::default_fn, did) |
tests/ui/auxiliary/external_macro.rs
Outdated
#[macro_export] | ||
macro_rules! external_struct_rest_default { | ||
() => { | ||
#[derive(Default)] | ||
struct ExternalDefault { | ||
a: i32, | ||
b: i32, | ||
} | ||
|
||
let _ = ExternalDefault { | ||
a: 10, | ||
..Default::default() | ||
}; | ||
}; | ||
} |
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't you use proc_macros::external!{ … }
in your tests, without defining a dedicated external macro?
fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) { | ||
if let ExprKind::Struct(struct_expr) = &expr.kind | ||
&& let StructRest::Base(base) = &struct_expr.rest | ||
&& !base.span.in_external_macro(cx.sess().source_map()) |
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.
Any reason to exclude only external macros for the base? Shouldn't also the check be excluded if expr
comes from expansion?
I'll let @samueltardieu review and do the final approval |
That makes a lot of sense! So I modified the lint to reject any base value used, and lint no need to switch to a late lint. Thank you! |
ab8de84
to
85c6663
Compare
Or we can restrict only Or developers can solve this problem using a function or macro, it is fine?
|
Indeed, right now it looks like the lint would catch many legitimate cases: https://github.com/rust-lang/rust-clippy/actions/runs/13925788373?pr=14412. Maybe try restricting this to Out of curiosity, do you know of any project which forbids this, or would want to? Or is it a "just in case" lint? |
Ok.
I'm not sure, but I've encountered bugs caused by this kind of situation before. I'll use it in our team's project. Thank you! |
a7bf8c7
to
554f903
Compare
&& let Some(did) = path_def_id(cx, func) | ||
&& cx.tcx.is_diagnostic_item(sym::default_fn, did) |
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.
For a more compact test, you can use clippy_utils::is_trait_item()
:
&& let Some(did) = path_def_id(cx, func) | |
&& cx.tcx.is_diagnostic_item(sym::default_fn, did) | |
&& is_trait_item(cx, func, sym::Default) |
base.span, | ||
format!( | ||
"should not use `..{}` to omit rest of struct field initialization", | ||
snippet(cx, 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.
If the snippet cannot be obtained, "" would probably be clearer than ".." in an error message.
snippet(cx, base.span, "..") | ||
), | ||
Some(expr.span), | ||
"each field's initial value should be explicitly specified", |
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.
Doesn't that no longer fit when another default expression is given?
7d7365d
to
7b8912c
Compare
7b8912c
to
0dab1e3
Compare
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.
Some additional comments.
snippet(cx, base.span, "") | ||
), | ||
Some(expr.span), | ||
"explicitly specify all fields or use other base value instead of `..*::default()`", |
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 this lint should only trigger, if the Default
implementation is derived, but not when it is manually implemented. Manually implemented it isn't different from using something like Foo::get_foo()
.
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.
Sure.
STRUCT_FIELDS_REST_DEFAULT, | ||
base.span, | ||
format!( | ||
"should not use `..{}` to omit rest of struct field initialization", |
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.
"should not use `..{}` to omit rest of struct field initialization", | |
"usage of `..{}` to initialize struct fields", |
The error message should state what the code does, not what should (not) be done. This belongs into the help/note/sugg message, as you done below.
a8fd2c1
to
7f63851
Compare
☔ The latest upstream changes (possibly 8eed350) made this pull request unmergeable. Please resolve the merge conflicts. |
Closes #14382
changelog: [
struct_fields_rest_default
]: add new lint