Skip to content

Commit

Permalink
Auto merge of rust-lang#134737 - estebank:deive-lint-default-fields-b…
Browse files Browse the repository at this point in the history
…ase, r=<try>

Implement `default_overrides_default_fields` lint

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:14:1
   |
LL | / impl Default for A {
LL | |     fn default() -> Self {
LL | |         A {
LL | |             y: 0,
   | |                - this field has a default value
...  |
LL | | }
   | |_^
   |
   = help: use the default values in the `impl` with `Struct { mandatory_field, .. }` to avoid them diverging over time
note: the lint level is defined here
  --> $DIR/manual-default-impl-could-be-derived.rs:5:9
   |
LL | #![deny(default_overrides_default_fields)]
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

r? `@compiler-errors`

This is a simpler version of rust-lang#134441, detecting the simpler case when a field with a default should have not been specified in the manual `Default::default()`, instead using `..` for it. It doesn't provide any suggestions, nor the checks for "equivalences" nor whether the value used in the imp being used would be suitable as a default field value.
  • Loading branch information
bors committed Dec 27, 2024
2 parents 6d3db55 + 01307cf commit 43adf23
Show file tree
Hide file tree
Showing 4 changed files with 526 additions and 0 deletions.
185 changes: 185 additions & 0 deletions compiler/rustc_lint/src/default_could_be_derived.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
use rustc_data_structures::fx::FxHashMap;
use rustc_errors::Diag;
use rustc_hir as hir;
use rustc_middle::ty;
use rustc_session::{declare_lint, impl_lint_pass};
use rustc_span::Symbol;
use rustc_span::symbol::sym;

use crate::{LateContext, LateLintPass};

declare_lint! {
/// The `default_overrides_default_fields` lint checks for manual `impl` blocks of the
/// `Default` trait of types with default field values.
///
/// ### Example
///
/// ```rust,compile_fail
/// #![feature(default_field_values)]
/// struct Foo {
/// x: i32 = 101,
/// y: NonDefault,
/// }
///
/// struct NonDefault;
///
/// #[deny(default_overrides_default_fields)]
/// impl Default for Foo {
/// fn default() -> Foo {
/// Foo { x: 100, y: NonDefault }
/// }
/// }
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// Manually writing a `Default` implementation for a type that has
/// default field values runs the risk of diverging behavior between
/// `Type { .. }` and `<Type as Default>::default()`, which would be a
/// foot-gun for users of that type that would expect these to be
/// equivalent. If `Default` can't be derived due to some fields not
/// having a `Default` implementation, we encourage the use of `..` for
/// the fields that do have a default field value.
pub DEFAULT_OVERRIDES_DEFAULT_FIELDS,
Deny,
"detect `Default` impl that should use the type's default field values",
@feature_gate = default_field_values;
}

#[derive(Default)]
pub(crate) struct DefaultCouldBeDerived;

impl_lint_pass!(DefaultCouldBeDerived => [DEFAULT_OVERRIDES_DEFAULT_FIELDS]);

impl<'tcx> LateLintPass<'tcx> for DefaultCouldBeDerived {
fn check_impl_item(&mut self, cx: &LateContext<'_>, impl_item: &hir::ImplItem<'_>) {
// Look for manual implementations of `Default`.
let Some(default_def_id) = cx.tcx.get_diagnostic_item(sym::Default) else { return };
let hir::ImplItemKind::Fn(_sig, body_id) = impl_item.kind else { return };
let assoc = cx.tcx.associated_item(impl_item.owner_id);
let parent = assoc.container_id(cx.tcx);
if cx.tcx.has_attr(parent, sym::automatically_derived) {
// We don't care about what `#[derive(Default)]` produces in this lint.
return;
}
let Some(trait_ref) = cx.tcx.impl_trait_ref(parent) else { return };
let trait_ref = trait_ref.instantiate_identity();
if trait_ref.def_id != default_def_id {
return;
}
let ty = trait_ref.self_ty();
let ty::Adt(def, _) = ty.kind() else { return };

// We now know we have a manually written definition of a `<Type as Default>::default()`.

let hir = cx.tcx.hir();

let type_def_id = def.did();
let body = hir.body(body_id);

// 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;
};

// Keep a mapping of field name to `hir::FieldDef` for every field in the type. We'll use
// these to check for things like checking whether it has a default or using its span for
// suggestions.
let orig_fields = match hir.get_if_local(type_def_id) {
Some(hir::Node::Item(hir::Item {
kind:
hir::ItemKind::Struct(hir::VariantData::Struct { fields, recovered: _ }, _generics),
..
})) => fields.iter().map(|f| (f.ident.name, f)).collect::<FxHashMap<_, _>>(),
_ => return,
};

// We check `fn default()` body is a single ADT literal and get all the fields that are
// being set.
let hir::ExprKind::Struct(_qpath, fields, tail) = expr.kind else { return };

// We have a struct literal
//
// struct Foo {
// field: Type,
// }
//
// impl Default for Foo {
// fn default() -> Foo {
// Foo {
// field: val,
// }
// }
// }
//
// We would suggest `#[derive(Default)]` if `field` has a default value, regardless of what
// it is; we don't want to encourage divergent behavior between `Default::default()` and
// `..`.

if let hir::StructTailExpr::Base(_) = tail {
// This is *very* niche. We'd only get here if someone wrote
// impl Default for Ty {
// fn default() -> Ty {
// Ty { ..something() }
// }
// }
// where `something()` would have to be a call or path.
// We have nothing meaninful to do with this.
return;
}

// At least one of the fields with a default value have been overriden in
// the `Default` implementation. We suggest removing it and relying on `..`
// instead.
let any_default_field_given =
fields.iter().any(|f| orig_fields.get(&f.ident.name).and_then(|f| f.default).is_some());

if !any_default_field_given {
// None of the default fields were actually provided explicitly, so the manual impl
// doesn't override them (the user used `..`), so there's no risk of divergent behavior.
return;
}

let Some(local) = parent.as_local() else { return };
let hir_id = cx.tcx.local_def_id_to_hir_id(local);
let hir::Node::Item(item) = cx.tcx.hir_node(hir_id) else { return };
cx.tcx.node_span_lint(DEFAULT_OVERRIDES_DEFAULT_FIELDS, hir_id, item.span, |diag| {
mk_lint(diag, orig_fields, fields);
});
}
}

fn mk_lint(
diag: &mut Diag<'_, ()>,
orig_fields: FxHashMap<Symbol, &hir::FieldDef<'_>>,
fields: &[hir::ExprField<'_>],
) {
diag.primary_message("`Default` impl doesn't use the declared default field values");

// For each field in the struct expression
// - if the field in the type has a default value, it should be removed
// - elif the field is an expression that could be a default value, it should be used as the
// field's default value (FIXME: not done).
// - else, we wouldn't touch this field, it would remain in the manual impl
let mut removed_all_fields = true;
for field in fields {
if orig_fields.get(&field.ident.name).and_then(|f| f.default).is_some() {
diag.span_label(field.expr.span, "this field has a default value");
} else {
removed_all_fields = false;
}
}

diag.help(if removed_all_fields {
"to avoid divergence in behavior between `Struct { .. }` and \
`<Struct as Default>::default()`, derive the `Default`"
} else {
"use the default values in the `impl` with `Struct { mandatory_field, .. }` to avoid them \
diverging over time"
});
}
3 changes: 3 additions & 0 deletions compiler/rustc_lint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ mod async_fn_in_trait;
pub mod builtin;
mod context;
mod dangling;
mod default_could_be_derived;
mod deref_into_dyn_supertrait;
mod drop_forget_useless;
mod early;
Expand Down Expand Up @@ -85,6 +86,7 @@ use async_closures::AsyncClosureUsage;
use async_fn_in_trait::AsyncFnInTrait;
use builtin::*;
use dangling::*;
use default_could_be_derived::DefaultCouldBeDerived;
use deref_into_dyn_supertrait::*;
use drop_forget_useless::*;
use enum_intrinsics_non_enums::EnumIntrinsicsNonEnums;
Expand Down Expand Up @@ -189,6 +191,7 @@ late_lint_methods!(
BuiltinCombinedModuleLateLintPass,
[
ForLoopsOverFallibles: ForLoopsOverFallibles,
DefaultCouldBeDerived: DefaultCouldBeDerived::default(),
DerefIntoDynSupertrait: DerefIntoDynSupertrait,
DropForgetUseless: DropForgetUseless,
ImproperCTypesDeclarations: ImproperCTypesDeclarations,
Expand Down
Loading

0 comments on commit 43adf23

Please sign in to comment.