Skip to content

Commit fc5efa3

Browse files
committed
Implement default_mismatches_new lint
### What it does If a type has an auto-derived `Default` trait and a `fn new() -> Self`, this lint checks if the `new()` method performs custom logic rather than simply calling the `default()` method. ### Why is this bad? Users expect the `new()` method to be equivalent to `default()`, so if the `Default` trait is auto-derived, the `new()` method should not perform custom logic. Otherwise, there is a risk of different behavior between the two instantiation methods. ### Example ```rust #[derive(Default)] struct MyStruct(i32); impl MyStruct { fn new() -> Self { Self(42) } } ``` Users are unlikely to notice that `MyStruct::new()` and `MyStruct::default()` would produce different results. The `new()` method should use auto-derived `default()` instead to be consistent: ```rust #[derive(Default)] struct MyStruct(i32); impl MyStruct { fn new() -> Self { Self::default() } } ``` Alternatively, if the `new()` method requires a non-default initialization, implement a custom `Default`. This also allows you to mark the `new()` implementation as `const`: ```rust struct MyStruct(i32); impl MyStruct { const fn new() -> Self { Self(42) } } impl Default for MyStruct { fn default() -> Self { Self::new() } } ```
1 parent e2d9b9a commit fc5efa3

12 files changed

+642
-73
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5507,6 +5507,7 @@ Released 2018-09-13
55075507
[`declare_interior_mutable_const`]: https://rust-lang.github.io/rust-clippy/master/index.html#declare_interior_mutable_const
55085508
[`default_constructed_unit_structs`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_constructed_unit_structs
55095509
[`default_instead_of_iter_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_instead_of_iter_empty
5510+
[`default_mismatches_new`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_mismatches_new
55105511
[`default_numeric_fallback`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_numeric_fallback
55115512
[`default_trait_access`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_trait_access
55125513
[`default_union_representation`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_union_representation

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
569569
crate::needless_update::NEEDLESS_UPDATE_INFO,
570570
crate::neg_cmp_op_on_partial_ord::NEG_CMP_OP_ON_PARTIAL_ORD_INFO,
571571
crate::neg_multiply::NEG_MULTIPLY_INFO,
572+
crate::new_without_default::DEFAULT_MISMATCHES_NEW_INFO,
572573
crate::new_without_default::NEW_WITHOUT_DEFAULT_INFO,
573574
crate::no_effect::NO_EFFECT_INFO,
574575
crate::no_effect::NO_EFFECT_UNDERSCORE_BINDING_INFO,

clippy_lints/src/default_constructed_unit_structs.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,30 +46,23 @@ declare_clippy_lint! {
4646
}
4747
declare_lint_pass!(DefaultConstructedUnitStructs => [DEFAULT_CONSTRUCTED_UNIT_STRUCTS]);
4848

49-
fn is_alias(ty: hir::Ty<'_>) -> bool {
50-
if let hir::TyKind::Path(ref qpath) = ty.kind {
51-
is_ty_alias(qpath)
52-
} else {
53-
false
54-
}
55-
}
56-
5749
impl LateLintPass<'_> for DefaultConstructedUnitStructs {
5850
fn check_expr<'tcx>(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
5951
if let ExprKind::Call(fn_expr, &[]) = expr.kind
6052
// make sure we have a call to `Default::default`
6153
&& let ExprKind::Path(ref qpath @ hir::QPath::TypeRelative(base, _)) = fn_expr.kind
6254
// make sure this isn't a type alias:
6355
// `<Foo as Bar>::Assoc` cannot be used as a constructor
64-
&& !is_alias(*base)
56+
&& !matches!(base.kind, hir::TyKind::Path(ref qpath) if is_ty_alias(qpath))
6557
&& let Res::Def(_, def_id) = cx.qpath_res(qpath, fn_expr.hir_id)
6658
&& cx.tcx.is_diagnostic_item(sym::default_fn, def_id)
6759
// make sure we have a struct with no fields (unit struct)
6860
&& let ty::Adt(def, ..) = cx.typeck_results().expr_ty(expr).kind()
6961
&& def.is_struct()
7062
&& let var @ ty::VariantDef { ctor: Some((hir::def::CtorKind::Const, _)), .. } = def.non_enum_variant()
7163
&& !var.is_field_list_non_exhaustive()
72-
&& !expr.span.from_expansion() && !qpath.span().from_expansion()
64+
&& !expr.span.from_expansion()
65+
&& !qpath.span().from_expansion()
7366
{
7467
span_lint_and_sugg(
7568
cx,

clippy_lints/src/new_without_default.rs

Lines changed: 223 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
use clippy_utils::diagnostics::span_lint_hir_and_then;
2-
use clippy_utils::return_ty;
3-
use clippy_utils::source::snippet;
2+
use clippy_utils::source::{snippet, trim_span};
43
use clippy_utils::sugg::DiagExt;
4+
use clippy_utils::{is_default_equivalent_call, return_ty};
55
use rustc_errors::Applicability;
66
use rustc_hir as hir;
7-
use rustc_hir::HirIdSet;
7+
use rustc_hir::HirIdMap;
88
use rustc_lint::{LateContext, LateLintPass, LintContext};
9+
use rustc_middle::ty::{Adt, Ty, VariantDef};
910
use rustc_session::impl_lint_pass;
10-
use rustc_span::sym;
11+
use rustc_span::{BytePos, Pos as _, Span, sym};
1112

1213
declare_clippy_lint! {
1314
/// ### What it does
@@ -48,12 +49,75 @@ declare_clippy_lint! {
4849
"`pub fn new() -> Self` method without `Default` implementation"
4950
}
5051

52+
declare_clippy_lint! {
53+
/// ### What it does
54+
/// If a type has an auto-derived `Default` trait and a `fn new() -> Self`,
55+
/// this lint checks if the `new()` method performs custom logic rather
56+
/// than simply calling the `default()` method.
57+
///
58+
/// ### Why is this bad?
59+
/// Users expect the `new()` method to be equivalent to `default()`,
60+
/// so if the `Default` trait is auto-derived, the `new()` method should
61+
/// not perform custom logic. Otherwise, there is a risk of different
62+
/// behavior between the two instantiation methods.
63+
///
64+
/// ### Example
65+
/// ```no_run
66+
/// #[derive(Default)]
67+
/// struct MyStruct(i32);
68+
/// impl MyStruct {
69+
/// fn new() -> Self {
70+
/// Self(42)
71+
/// }
72+
/// }
73+
/// ```
74+
///
75+
/// Users are unlikely to notice that `MyStruct::new()` and `MyStruct::default()` would produce
76+
/// different results. The `new()` method should use auto-derived `default()` instead to be consistent:
77+
///
78+
/// ```no_run
79+
/// #[derive(Default)]
80+
/// struct MyStruct(i32);
81+
/// impl MyStruct {
82+
/// fn new() -> Self {
83+
/// Self::default()
84+
/// }
85+
/// }
86+
/// ```
87+
///
88+
/// Alternatively, if the `new()` method requires a non-default initialization, implement a custom `Default`.
89+
/// This also allows you to mark the `new()` implementation as `const`:
90+
///
91+
/// ```no_run
92+
/// struct MyStruct(i32);
93+
/// impl MyStruct {
94+
/// const fn new() -> Self {
95+
/// Self(42)
96+
/// }
97+
/// }
98+
/// impl Default for MyStruct {
99+
/// fn default() -> Self {
100+
/// Self::new()
101+
/// }
102+
/// }
103+
#[clippy::version = "1.86.0"]
104+
pub DEFAULT_MISMATCHES_NEW,
105+
suspicious,
106+
"`fn new() -> Self` method does not forward to auto-derived `Default` implementation"
107+
}
108+
109+
#[derive(Debug, Clone, Copy)]
110+
enum DefaultType {
111+
AutoDerived,
112+
Manual,
113+
}
114+
51115
#[derive(Clone, Default)]
52116
pub struct NewWithoutDefault {
53-
impling_types: Option<HirIdSet>,
117+
impling_types: Option<HirIdMap<DefaultType>>,
54118
}
55119

56-
impl_lint_pass!(NewWithoutDefault => [NEW_WITHOUT_DEFAULT]);
120+
impl_lint_pass!(NewWithoutDefault => [NEW_WITHOUT_DEFAULT, DEFAULT_MISMATCHES_NEW]);
57121

58122
impl<'tcx> LateLintPass<'tcx> for NewWithoutDefault {
59123
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) {
@@ -71,7 +135,7 @@ impl<'tcx> LateLintPass<'tcx> for NewWithoutDefault {
71135
if impl_item.span.in_external_macro(cx.sess().source_map()) {
72136
return;
73137
}
74-
if let hir::ImplItemKind::Fn(ref sig, _) = impl_item.kind {
138+
if let hir::ImplItemKind::Fn(ref sig, body_id) = impl_item.kind {
75139
let name = impl_item.ident.name;
76140
let id = impl_item.owner_id;
77141
if sig.header.is_unsafe() {
@@ -89,65 +153,62 @@ impl<'tcx> LateLintPass<'tcx> for NewWithoutDefault {
89153
}
90154
if sig.decl.inputs.is_empty()
91155
&& name == sym::new
92-
&& cx.effective_visibilities.is_reachable(impl_item.owner_id.def_id)
93156
&& let self_def_id = cx.tcx.hir().get_parent_item(id.into())
94157
&& let self_ty = cx.tcx.type_of(self_def_id).instantiate_identity()
95158
&& self_ty == return_ty(cx, id)
96159
&& let Some(default_trait_id) = cx.tcx.get_diagnostic_item(sym::Default)
97160
{
98161
if self.impling_types.is_none() {
99-
let mut impls = HirIdSet::default();
162+
let mut impls = HirIdMap::default();
100163
cx.tcx.for_each_impl(default_trait_id, |d| {
101164
let ty = cx.tcx.type_of(d).instantiate_identity();
102165
if let Some(ty_def) = ty.ty_adt_def() {
103166
if let Some(local_def_id) = ty_def.did().as_local() {
104-
impls.insert(cx.tcx.local_def_id_to_hir_id(local_def_id));
167+
impls.insert(
168+
cx.tcx.local_def_id_to_hir_id(local_def_id),
169+
if cx.tcx.is_builtin_derived(d) {
170+
DefaultType::AutoDerived
171+
} else {
172+
DefaultType::Manual
173+
},
174+
);
105175
}
106176
}
107177
});
108178
self.impling_types = Some(impls);
109179
}
110180

181+
let mut default_type = None;
111182
// Check if a Default implementation exists for the Self type, regardless of
112183
// generics
113184
if let Some(ref impling_types) = self.impling_types
114185
&& let self_def = cx.tcx.type_of(self_def_id).instantiate_identity()
115186
&& let Some(self_def) = self_def.ty_adt_def()
116187
&& let Some(self_local_did) = self_def.did().as_local()
117-
&& let self_id = cx.tcx.local_def_id_to_hir_id(self_local_did)
118-
&& impling_types.contains(&self_id)
119188
{
120-
return;
189+
let self_id = cx.tcx.local_def_id_to_hir_id(self_local_did);
190+
default_type = impling_types.get(&self_id);
191+
if let Some(DefaultType::Manual) = default_type {
192+
// both `new` and `default` are manually implemented
193+
return;
194+
}
121195
}
122196

123-
let generics_sugg = snippet(cx, generics.span, "");
124-
let where_clause_sugg = if generics.has_where_clause_predicates {
125-
format!("\n{}\n", snippet(cx, generics.where_clause_span, ""))
126-
} else {
127-
String::new()
128-
};
129-
let self_ty_fmt = self_ty.to_string();
130-
let self_type_snip = snippet(cx, impl_self_ty.span, &self_ty_fmt);
131-
span_lint_hir_and_then(
132-
cx,
133-
NEW_WITHOUT_DEFAULT,
134-
id.into(),
135-
impl_item.span,
136-
format!("you should consider adding a `Default` implementation for `{self_type_snip}`"),
137-
|diag| {
138-
diag.suggest_prepend_item(
139-
cx,
140-
item.span,
141-
"try adding this",
142-
&create_new_without_default_suggest_msg(
143-
&self_type_snip,
144-
&generics_sugg,
145-
&where_clause_sugg,
146-
),
147-
Applicability::MachineApplicable,
148-
);
149-
},
150-
);
197+
if default_type.is_none() {
198+
// there are no `Default` implementations for this type
199+
if !cx.effective_visibilities.is_reachable(impl_item.owner_id.def_id) {
200+
return;
201+
}
202+
suggest_new_without_default(cx, item, impl_item, id, self_ty, generics, impl_self_ty);
203+
} else if let hir::ExprKind::Block(block, _) = cx.tcx.hir().body(body_id).value.kind
204+
&& !is_unit_struct(cx, self_ty)
205+
{
206+
// this type has an automatically derived `Default` implementation
207+
// check if `new` and `default` are equivalent
208+
if let Some(span) = check_block_calls_default(cx, block) {
209+
suggest_default_mismatch_new(cx, span, id, block, self_ty, impl_self_ty);
210+
}
211+
}
151212
}
152213
}
153214
}
@@ -156,16 +217,128 @@ impl<'tcx> LateLintPass<'tcx> for NewWithoutDefault {
156217
}
157218
}
158219

159-
fn create_new_without_default_suggest_msg(
160-
self_type_snip: &str,
161-
generics_sugg: &str,
162-
where_clause_sugg: &str,
163-
) -> String {
164-
#[rustfmt::skip]
165-
format!(
166-
"impl{generics_sugg} Default for {self_type_snip}{where_clause_sugg} {{
220+
// Check if Self is a unit struct, and avoid any kind of suggestions
221+
// FIXME: this was copied from DefaultConstructedUnitStructs,
222+
// and should be refactored into a common function
223+
fn is_unit_struct(_cx: &LateContext<'_>, ty: Ty<'_>) -> bool {
224+
if let Adt(def, ..) = ty.kind()
225+
&& def.is_struct()
226+
&& let var @ VariantDef {
227+
ctor: Some((hir::def::CtorKind::Const, _)),
228+
..
229+
} = def.non_enum_variant()
230+
&& !var.is_field_list_non_exhaustive()
231+
{
232+
true
233+
} else {
234+
false
235+
}
236+
}
237+
238+
/// Check if a block contains one of these:
239+
/// - Empty block with an expr (e.g., `{ Self::default() }`)
240+
/// - One statement (e.g., `{ return Self::default(); }`)
241+
fn check_block_calls_default(cx: &LateContext<'_>, block: &hir::Block<'_>) -> Option<Span> {
242+
if let Some(expr) = block.expr
243+
&& block.stmts.is_empty()
244+
&& check_expr_call_default(cx, expr)
245+
{
246+
// Block only has a trailing expression, e.g. `Self::default()`
247+
return None;
248+
} else if let [hir::Stmt { kind, .. }] = block.stmts
249+
&& let hir::StmtKind::Expr(expr) | hir::StmtKind::Semi(expr) = kind
250+
&& let hir::ExprKind::Ret(Some(ret_expr)) = expr.kind
251+
&& check_expr_call_default(cx, ret_expr)
252+
{
253+
// Block has a single statement, e.g. `return Self::default();`
254+
return None;
255+
}
256+
257+
// trim first and last character, and trim spaces
258+
let mut span = block.span;
259+
span = span.with_lo(span.lo() + BytePos::from_usize(1));
260+
span = span.with_hi(span.hi() - BytePos::from_usize(1));
261+
span = trim_span(cx.sess().source_map(), span);
262+
263+
Some(span)
264+
}
265+
266+
/// Check for `Self::default()` call syntax or equivalent
267+
fn check_expr_call_default(cx: &LateContext<'_>, expr: &hir::Expr<'_>) -> bool {
268+
if let hir::ExprKind::Call(callee, &[]) = expr.kind
269+
// FIXME: does this include `Self { }` style calls, which is equivalent,
270+
// but not the same as `Self::default()`?
271+
// FIXME: what should the whole_call_expr (3rd arg) be?
272+
&& is_default_equivalent_call(cx, callee, None)
273+
{
274+
true
275+
} else {
276+
false
277+
}
278+
}
279+
280+
fn suggest_default_mismatch_new<'tcx>(
281+
cx: &LateContext<'tcx>,
282+
span: Span,
283+
id: rustc_hir::OwnerId,
284+
block: &rustc_hir::Block<'_>,
285+
self_ty: Ty<'tcx>,
286+
impl_self_ty: &rustc_hir::Ty<'_>,
287+
) {
288+
let self_ty_fmt = self_ty.to_string();
289+
let self_type_snip = snippet(cx, impl_self_ty.span, &self_ty_fmt);
290+
span_lint_hir_and_then(
291+
cx,
292+
DEFAULT_MISMATCHES_NEW,
293+
id.into(),
294+
block.span,
295+
format!("you should consider delegating to the auto-derived `Default` for `{self_type_snip}`"),
296+
|diag| {
297+
// This would replace any comments, and we could work around the first comment,
298+
// but in case of a block of code with multiple statements and comment lines,
299+
// we can't do much. For now, we always mark this as a MaybeIncorrect suggestion.
300+
diag.span_suggestion(span, "try using this", "Self::default()", Applicability::MaybeIncorrect);
301+
},
302+
);
303+
}
304+
305+
fn suggest_new_without_default<'tcx>(
306+
cx: &LateContext<'tcx>,
307+
item: &hir::Item<'_>,
308+
impl_item: &hir::ImplItem<'_>,
309+
id: hir::OwnerId,
310+
self_ty: Ty<'tcx>,
311+
generics: &hir::Generics<'_>,
312+
impl_self_ty: &hir::Ty<'_>,
313+
) {
314+
let generics_sugg = snippet(cx, generics.span, "");
315+
let where_clause_sugg = if generics.has_where_clause_predicates {
316+
format!("\n{}\n", snippet(cx, generics.where_clause_span, ""))
317+
} else {
318+
String::new()
319+
};
320+
let self_ty_fmt = self_ty.to_string();
321+
let self_type_snip = snippet(cx, impl_self_ty.span, &self_ty_fmt);
322+
span_lint_hir_and_then(
323+
cx,
324+
NEW_WITHOUT_DEFAULT,
325+
id.into(),
326+
impl_item.span,
327+
format!("you should consider adding a `Default` implementation for `{self_type_snip}`"),
328+
|diag| {
329+
diag.suggest_prepend_item(
330+
cx,
331+
item.span,
332+
"try adding this",
333+
&format!(
334+
"impl{generics_sugg} Default for {self_type_snip}{where_clause_sugg} {{
167335
fn default() -> Self {{
168336
Self::new()
169337
}}
170-
}}")
338+
}}"
339+
),
340+
Applicability::MachineApplicable,
341+
);
342+
},
343+
);
171344
}

tests/ui/default_constructed_unit_structs.fixed

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![allow(unused)]
1+
#![allow(unused, clippy::default_mismatches_new)]
22
#![warn(clippy::default_constructed_unit_structs)]
33
use std::marker::PhantomData;
44

0 commit comments

Comments
 (0)