Skip to content

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

WeiTheShinobi
Copy link
Contributor

Closes #14382

changelog: [struct_fields_rest_default]: add new lint

@rustbot
Copy link
Collaborator

rustbot commented Mar 15, 2025

r? @Manishearth

rustbot has assigned @Manishearth.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties label Mar 15, 2025
@WeiTheShinobi WeiTheShinobi force-pushed the struct_fields_default branch 2 times, most recently from 2a1f974 to 1a98f1f Compare March 15, 2025 14:22
@WeiTheShinobi WeiTheShinobi force-pushed the struct_fields_default branch from 1a98f1f to b11506a Compare March 15, 2025 14:35
Copy link
Contributor

@samueltardieu samueltardieu left a 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?

Comment on lines 58 to 65
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
Copy link
Contributor

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:

Suggested change
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)

Comment on lines 1 to 15
#[macro_export]
macro_rules! external_struct_rest_default {
() => {
#[derive(Default)]
struct ExternalDefault {
a: i32,
b: i32,
}

let _ = ExternalDefault {
a: 10,
..Default::default()
};
};
}
Copy link
Contributor

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())
Copy link
Contributor

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?

@Manishearth
Copy link
Member

I'll let @samueltardieu review and do the final approval

@WeiTheShinobi
Copy link
Contributor Author

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?

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.
I also fixed the problem related to the macro check.

Thank you!

@WeiTheShinobi WeiTheShinobi force-pushed the struct_fields_default branch from ab8de84 to 85c6663 Compare March 18, 2025 14:30
@WeiTheShinobi
Copy link
Contributor Author

WeiTheShinobi commented Mar 19, 2025

Or we can restrict only Default::default and Foo::default,
providing flexibility for developers to use special cases.
When developers use these cases, they may have a better understanding of what they are doing,
rather than just trying to be lazy.
not lint something like
..Foo::special_case()
Is it make sense?

Or developers can solve this problem using a function or macro, it is fine?

// only restrict `default()`
let foo = Foo {
    a: 1,
    b: 2,
    ..Foo::spec()
}
// if restrict all base value.
let mut foo = Foo::spec();
foo.a = 1;
foo.b = 2;

// or
let foo = Foo::spec(a, b);

let foo = Foo::New(/* args.. */);

@samueltardieu
Copy link
Contributor

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 Default::default() or Type::default() and see how it behaves.

Out of curiosity, do you know of any project which forbids this, or would want to? Or is it a "just in case" lint?

@WeiTheShinobi
Copy link
Contributor Author

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 Default::default() or Type::default() and see how it behaves.

Ok.

Out of curiosity, do you know of any project which forbids this, or would want to? Or is it a "just in case" lint?

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!

@WeiTheShinobi WeiTheShinobi force-pushed the struct_fields_default branch from a7bf8c7 to 554f903 Compare March 21, 2025 15:15
Comment on lines 63 to 64
&& let Some(did) = path_def_id(cx, func)
&& cx.tcx.is_diagnostic_item(sym::default_fn, did)
Copy link
Contributor

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():

Suggested change
&& 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, "..")
Copy link
Contributor

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",
Copy link
Contributor

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?

@WeiTheShinobi WeiTheShinobi force-pushed the struct_fields_default branch 2 times, most recently from 7d7365d to 7b8912c Compare March 21, 2025 16:44
@WeiTheShinobi WeiTheShinobi force-pushed the struct_fields_default branch from 7b8912c to 0dab1e3 Compare March 21, 2025 16:52
Copy link
Member

@flip1995 flip1995 left a 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()`",
Copy link
Member

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().

Copy link
Contributor Author

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"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.

@WeiTheShinobi WeiTheShinobi marked this pull request as draft March 23, 2025 07:06
@WeiTheShinobi WeiTheShinobi force-pushed the struct_fields_default branch from a8fd2c1 to 7f63851 Compare March 30, 2025 13:21
@WeiTheShinobi WeiTheShinobi marked this pull request as ready for review March 30, 2025 13:32
@rustbot
Copy link
Collaborator

rustbot commented Apr 16, 2025

☔ The latest upstream changes (possibly 8eed350) made this pull request unmergeable. Please resolve the merge conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-review Status: Awaiting review from the assignee but also interested parties
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Warn of ..Default::default()
5 participants