Skip to content

Commit 201617e

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 4a9b8c6 commit 201617e

12 files changed

+665
-75
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5549,6 +5549,7 @@ Released 2018-09-13
55495549
[`declare_interior_mutable_const`]: https://rust-lang.github.io/rust-clippy/master/index.html#declare_interior_mutable_const
55505550
[`default_constructed_unit_structs`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_constructed_unit_structs
55515551
[`default_instead_of_iter_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_instead_of_iter_empty
5552+
[`default_mismatches_new`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_mismatches_new
55525553
[`default_numeric_fallback`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_numeric_fallback
55535554
[`default_trait_access`]: https://rust-lang.github.io/rust-clippy/master/index.html#default_trait_access
55545555
[`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
@@ -572,6 +572,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
572572
crate::needless_update::NEEDLESS_UPDATE_INFO,
573573
crate::neg_cmp_op_on_partial_ord::NEG_CMP_OP_ON_PARTIAL_ORD_INFO,
574574
crate::neg_multiply::NEG_MULTIPLY_INFO,
575+
crate::new_without_default::DEFAULT_MISMATCHES_NEW_INFO,
575576
crate::new_without_default::NEW_WITHOUT_DEFAULT_INFO,
576577
crate::no_effect::NO_EFFECT_INFO,
577578
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: 224 additions & 52 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,61 @@ 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

111-
// Check if a Default implementation exists for the Self type, regardless of
112-
// generics
181+
let mut default_type = None;
182+
// Check if a Default implementation exists for the Self type, regardless of generics
113183
if let Some(ref impling_types) = self.impling_types
114184
&& let self_def = cx.tcx.type_of(self_def_id).instantiate_identity()
115185
&& let Some(self_def) = self_def.ty_adt_def()
116186
&& 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)
119187
{
120-
return;
188+
let self_id = cx.tcx.local_def_id_to_hir_id(self_local_did);
189+
default_type = impling_types.get(&self_id);
190+
if let Some(DefaultType::Manual) = default_type {
191+
// both `new` and `default` are manually implemented
192+
return;
193+
}
121194
}
122195

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-
);
196+
if default_type.is_none() {
197+
// there are no `Default` implementations for this type
198+
if !cx.effective_visibilities.is_reachable(impl_item.owner_id.def_id) {
199+
return;
200+
}
201+
suggest_new_without_default(cx, item, impl_item, id, self_ty, generics, impl_self_ty);
202+
} else if let hir::ExprKind::Block(block, _) = cx.tcx.hir_body(body_id).value.kind
203+
&& !is_unit_struct(cx, self_ty)
204+
// TODO: handle generics
205+
&& generics.params.is_empty()
206+
// check if `new` and `default` are equivalent
207+
&& let Some(span) = check_block_calls_default(cx, block)
208+
{
209+
suggest_default_mismatch_new(cx, span, id, block, self_ty, impl_self_ty);
210+
}
151211
}
152212
}
153213
}
@@ -156,16 +216,128 @@ impl<'tcx> LateLintPass<'tcx> for NewWithoutDefault {
156216
}
157217
}
158218

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

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)