-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Implement default_could_be_derived
and default_overrides_default_fields
lints
#134441
base: master
Are you sure you want to change the base?
Conversation
r? @Noratrieb rustbot has assigned @Noratrieb. Use |
CC @rust-lang/lang: introduction of lint on manual I have a draft text where I make the case that we should also expand this behavior to all
|
This comment has been minimized.
This comment has been minimized.
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 definitely needs to be T-lang nominated before it lands since it's warn by default. I also am frankly somewhat confused about the motivation for adding this directly as a rustc lint; this seems like something squarely in the territory of clippy.
As for the impl, it seems a bit complicated (like adding a new query doesn't seem totally obvious to me) but I tried to ask questions just to get my bearings first.
// We have a manual `impl Default for Ty {}` item, where `Ty` has no type parameters or is | ||
// an enum. | ||
|
||
for assoc in data.items { |
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.
everything nested here should be pulled into a function. it's indentation runaway like crazy lol.
The reason for the query is not necessarily self-evident. I'll try to explain the "equivalent" difficult to follow situation. Given an impl: impl<T> Default for Vec<T> {
fn default() -> Vec<T> {
Vec::new()
}
} we keep track of the impl<T> Default for Option<T> {
fn default() -> Option<T> {
Option::None
}
} Once we have that, we can check in the lint a situation like the following: struct Struct {
a: Option<i32>,
b: Vec<i32>,
}
impl Default for Struct {
fn default() -> Struct {
a: None,
b: Vec::new(),
}
} Because we know that The above should have instead have been written as #[derive(Default)]
struct Struct {
a: Option<i32>,
b: Vec<i32>,
} A query is needed to record this because we cannot inspect foreign crate HIR to find those "equivalences". The clippy lint that performs a check for all types today doesn't do that extra analysis (which would detect more uselessly manual
These are all gated behind |
You should actually gate the lints correctly in their lint definitions then. I think you can look at other feature gated lints (I think we still have some?). It was not obvious (or not true? idk) that this logic is gated behind the feature gate from first impression. |
This comment was marked as resolved.
This comment was marked as resolved.
Some changes occurred in src/tools/clippy cc @rust-lang/clippy |
1b107d7
to
143570b
Compare
This comment has been minimized.
This comment has been minimized.
let Some(Data { type_def_id, parent }) = self.data else { | ||
return; | ||
}; | ||
// FIXME: evaluate bodies with statements and evaluate bindings to see if they would be | ||
// derivable. | ||
let hir::ExprKind::Block(hir::Block { stmts: _, expr: Some(expr), .. }, None) = | ||
body.value.kind | ||
else { | ||
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.
I can remove all of these checks to a single method, but then I would end up returning a big tuple of unrelated fields. I leave it to you whether it is preferable to have shorter methods with descriptive names for these steps, or more linear logic with comments.
"`Default` impl that could be derived" | ||
}); | ||
|
||
let mut removals = vec![]; |
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 whole "field removal" span wrangling is not really great :-/
06d5f30
to
aab7aef
Compare
This comment has been minimized.
This comment has been minimized.
Some changes occurred in src/tools/rustfmt cc @rust-lang/rustfmt |
/// No trailing `..` or expression. The `Span` points at the trailing `,` or spot before `}`. | ||
None(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.
We can get away with not adding this span if we take the span between the end of the last field and the end of the struct expression (}
), but that makes things unnecessarily complicated.
for block_data in body.basic_blocks.iter() { | ||
// FIXME: make another pass on this logic. For example we should be confirming that the | ||
// place we assign to is `_0`. | ||
if block_data.statements.len() == 1 | ||
&& let mir::StatementKind::Assign(assign) = &block_data.statements[0].kind | ||
&& let mir::Rvalue::Aggregate(kind, _places) = &assign.1 | ||
&& let mir::AggregateKind::Adt(did, variant_index, _, _, _) = &**kind | ||
&& let def = cx.tcx.adt_def(did) | ||
&& let variant = &def.variant(*variant_index) | ||
&& variant.fields.is_empty() | ||
&& let Some((_, did)) = variant.ctor | ||
&& did == def_id | ||
{ | ||
return true; | ||
} else if block_data.statements.len() == 0 | ||
&& let Some(term) = &block_data.terminator | ||
{ | ||
match &term.kind { | ||
mir::TerminatorKind::Call { func: mir::Operand::Constant(c), .. } | ||
if let ty::FnDef(did, _args) = c.ty().kind() | ||
&& *did == def_id => | ||
{ | ||
return true; | ||
} | ||
mir::TerminatorKind::TailCall { func: mir::Operand::Constant(c), .. } | ||
if let ty::FnDef(did, _args) = c.ty().kind() | ||
&& *did == def_id => | ||
{ | ||
return true; |
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 is the iffiest part of this PR, IMO.
The commits are not logical units, only temporal work units to be squashed before merge, so it might be easier to review the full diff, but left unsquashed just in case it is useful to you. Edit: went ahead and squashed... I can reflog and publish the previous state if it would aid in the review. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…ields` lints Detect when a manual `Default` implementation isn't using the existing default field values and suggest using `..` instead: ``` error: `Default` impl doesn't use the declared default field values --> $DIR/manual-default-impl-could-be-derived.rs:13:1 | LL | / impl Default for A { LL | | fn default() -> Self { LL | | A { LL | | x: S, LL | | y: 0, | | - this field has a default value ... | LL | | } | |_^ | note: the lint level is defined here --> $DIR/manual-default-impl-could-be-derived.rs:4:35 | LL | #![deny(default_could_be_derived, default_overrides_default_fields)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use the default values in the `impl` to avoid them diverging over time | LL - x: S, LL - y: 0, LL + x: S, .. | ``` Detect when a manual `Default` implementation for a type containing at least one default field value has *all* fields that could be derived and suggest `#[derive(Default)]`: ``` error: `Default` impl that could be derived --> $DIR/manual-default-impl-could-be-derived.rs:27:1 | LL | struct B { | -------- all the fields in this struct have default values LL | x: S = S, | - default value LL | y: i32 = 1, | - default value ... LL | / impl Default for B { LL | | fn default() -> Self { LL | | B { LL | | x: S, ... | LL | | } | |_^ | note: the lint level is defined here --> $DIR/manual-default-impl-could-be-derived.rs:4:9 | LL | #![deny(default_could_be_derived, default_overrides_default_fields)] | ^^^^^^^^^^^^^^^^^^^^^^^^ help: to avoid divergence in behavior between `Struct { .. }` and `<Struct as Default>::default()`, derive the `Default` | LL ~ #[derive(Default)] struct B { | ``` Store a mapping between the `DefId` for an `impl Default for Ty {}` and the `DefId` of either a Unit variant/struct or an fn with no arguments that is called within `<Ty as Default>::default()`. When linting `impl`s, if it is for `Default`, we evaluate the contents of their `fn default()`. If it is *only* an ADT literal for `Self` and every field is either a "known to be defaulted" value (`0` or `false`), an explicit `Default::default()` call or a call or path to the same "equivalent" `DefId` from that field's type's `Default::default()` implementation.
4563d15
to
be8b02f
Compare
Since this logic is still pretty involved, plz give me a bit of time to understand it. Ping me if I've gone to long without a review. I'd appreciate if you give this a very thorough self review in the mean time, and make sure this logic is documented sufficiently. If you want to split this up to land the trivial parts first, you could split out just the part where you add the span to the non FRU variant to struct constructors in the AST. Up to you. |
Perhaps I've missed it, but I don't see an MSRV check in there, which would have this lint apply only to code that declares a current or newer minimum supported Rust version, thereby avoiding a lot of false positives. |
@llogiq: MSRV checks are a clippy-only thing afaict. |
We can probavly sort that out by the time stabilization of this feature rolls around, tbough, but I'd rather this PR not get more complicated. |
With the implementation as it is, library authors that want to preserve backwards compatibility and implement If we cannot avoid that, I suggest making the lint allow-by-default for the time being. |
@compiler-errors opened #134737 that keeps only the most fundamental lint ("is there a @llogiq note that these lints, as written, only affect structs that have default field values and a manual |
Detect when a manual
Default
implementation isn't using the existing default field values and suggest using..
instead:Detect when a manual
Default
implementation for a type containing at least one default field value has all fields that could be derived and suggest#[derive(Default)]
:Store a mapping between the
DefId
for animpl Default for Ty {}
and theDefId
of either a Unit variant/struct or an fn with no arguments that is called within<Ty as Default>::default()
.When linting
impl
s, if it is forDefault
, we evaluate the contents of theirfn default()
. If it is only an ADT literal forSelf
and every field is either a "known to be defaulted" value (0
orfalse
), an explicitDefault::default()
call or a call or path to the same "equivalent"DefId
from that field's type'sDefault::default()
implementation.