Skip to content

Commit da55789

Browse files
committed
Implement new_without_default lint
### What it does Checks for public types with a `pub fn new() -> Self` method and no implementation of [`Default`](https://doc.rust-lang.org/std/default/trait.Default.html). ### Why is this bad? The user might expect to be able to use [`Default`](https://doc.rust-lang.org/std/default/trait.Default.html) as the type can be constructed without arguments. ### Example ```rust pub struct Foo(Bar); impl Foo { pub fn new() -> Self { Foo(Bar::new()) } } ``` To fix the lint, add a `Default` implementation that delegates to `new`: ```rust pub struct Foo(Bar); impl Default for Foo { fn default() -> Self { Foo::new() } } ```
1 parent e2d9b9a commit da55789

12 files changed

+647
-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: 228 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,74 @@ 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` implementation and a `new` method with
55+
/// no parameters, this lint checks if the `new` method simply calls the `default`
56+
/// method.
57+
///
58+
/// ### Why is this bad?
59+
/// The `new` method should use auto-derived `default()` instead to be consistent.
60+
/// Otherwise, there is a risk of inconsistency between the two methods, even though
61+
/// most users will expect them to be equivalent.
62+
///
63+
/// ### Example
64+
/// ```no_run
65+
/// #[derive(Default)]
66+
/// struct MyStruct(i32);
67+
/// impl MyStruct {
68+
/// fn new() -> Self {
69+
/// Self(42)
70+
/// }
71+
/// }
72+
/// ```
73+
///
74+
/// Users are unlikely to notice that `MyStruct::new()` and `MyStruct::default()` would produce
75+
/// different results. The `new()` method should use auto-derived `default()` instead to be consistent:
76+
///
77+
/// ```no_run
78+
/// #[derive(Default)]
79+
/// struct MyStruct(i32);
80+
/// impl MyStruct {
81+
/// fn new() -> Self {
82+
/// Self::default()
83+
/// }
84+
/// }
85+
/// ```
86+
///
87+
/// Alternatively, if `new` method requires non-default initialization, implement a custom `Default`.
88+
/// This also allows you to mark the `new()` implementation as `const`:
89+
///
90+
/// ```no_run
91+
/// struct MyStruct(i32);
92+
/// impl MyStruct {
93+
/// const fn new() -> Self {
94+
/// Self(42)
95+
/// }
96+
/// }
97+
/// impl Default for MyStruct {
98+
/// fn default() -> Self {
99+
/// Self::new()
100+
/// }
101+
/// }
102+
#[clippy::version = "1.86.0"]
103+
pub DEFAULT_MISMATCHES_NEW,
104+
suspicious,
105+
"`fn new() -> Self` method that does not match the `Default` implementation"
106+
}
107+
108+
#[derive(Debug, Clone, Copy)]
109+
enum DefaultType {
110+
AutoDerived,
111+
Manual,
112+
}
113+
51114
#[derive(Clone, Default)]
52115
pub struct NewWithoutDefault {
53-
impling_types: Option<HirIdSet>,
116+
impling_types: Option<HirIdMap<DefaultType>>,
54117
}
55118

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

58121
impl<'tcx> LateLintPass<'tcx> for NewWithoutDefault {
59122
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) {
@@ -71,7 +134,7 @@ impl<'tcx> LateLintPass<'tcx> for NewWithoutDefault {
71134
if impl_item.span.in_external_macro(cx.sess().source_map()) {
72135
return;
73136
}
74-
if let hir::ImplItemKind::Fn(ref sig, _) = impl_item.kind {
137+
if let hir::ImplItemKind::Fn(ref sig, body_id) = impl_item.kind {
75138
let name = impl_item.ident.name;
76139
let id = impl_item.owner_id;
77140
if sig.header.is_unsafe() {
@@ -89,65 +152,62 @@ impl<'tcx> LateLintPass<'tcx> for NewWithoutDefault {
89152
}
90153
if sig.decl.inputs.is_empty()
91154
&& name == sym::new
92-
&& cx.effective_visibilities.is_reachable(impl_item.owner_id.def_id)
93155
&& let self_def_id = cx.tcx.hir().get_parent_item(id.into())
94156
&& let self_ty = cx.tcx.type_of(self_def_id).instantiate_identity()
95157
&& self_ty == return_ty(cx, id)
96158
&& let Some(default_trait_id) = cx.tcx.get_diagnostic_item(sym::Default)
97159
{
98160
if self.impling_types.is_none() {
99-
let mut impls = HirIdSet::default();
161+
let mut impls = HirIdMap::default();
100162
cx.tcx.for_each_impl(default_trait_id, |d| {
101163
let ty = cx.tcx.type_of(d).instantiate_identity();
102164
if let Some(ty_def) = ty.ty_adt_def() {
103165
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));
166+
impls.insert(
167+
cx.tcx.local_def_id_to_hir_id(local_def_id),
168+
if cx.tcx.is_builtin_derived(d) {
169+
DefaultType::AutoDerived
170+
} else {
171+
DefaultType::Manual
172+
},
173+
);
105174
}
106175
}
107176
});
108177
self.impling_types = Some(impls);
109178
}
110179

180+
let mut default_type = None;
111181
// Check if a Default implementation exists for the Self type, regardless of
112182
// 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+
{
205+
// this type has an automatically derived `Default` implementation
206+
// check if `new` and `default` are equivalent
207+
if let Some(span) = check_block_calls_default(cx, block) {
208+
suggest_default_mismatch_new(cx, span, id, block, self_ty, impl_self_ty);
209+
}
210+
}
151211
}
152212
}
153213
}
@@ -156,16 +216,134 @@ 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+
{
244+
// Check the block's trailing expression (e.g., `Self::default()`)
245+
if check_expr_call_default(cx, expr) {
246+
return None;
247+
}
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+
{
252+
if check_expr_call_default(cx, ret_expr) {
253+
return None;
254+
}
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(
301+
span,
302+
"try using this",
303+
&"Self::default()",
304+
Applicability::MaybeIncorrect,
305+
);
306+
},
307+
);
308+
}
309+
310+
fn suggest_new_without_default<'tcx>(
311+
cx: &LateContext<'tcx>,
312+
item: &hir::Item<'_>,
313+
impl_item: &hir::ImplItem<'_>,
314+
id: hir::OwnerId,
315+
self_ty: Ty<'tcx>,
316+
generics: &hir::Generics<'_>,
317+
impl_self_ty: &hir::Ty<'_>,
318+
) {
319+
let generics_sugg = snippet(cx, generics.span, "");
320+
let where_clause_sugg = if generics.has_where_clause_predicates {
321+
format!("\n{}\n", snippet(cx, generics.where_clause_span, ""))
322+
} else {
323+
String::new()
324+
};
325+
let self_ty_fmt = self_ty.to_string();
326+
let self_type_snip = snippet(cx, impl_self_ty.span, &self_ty_fmt);
327+
span_lint_hir_and_then(
328+
cx,
329+
NEW_WITHOUT_DEFAULT,
330+
id.into(),
331+
impl_item.span,
332+
format!("you should consider adding a `Default` implementation for `{self_type_snip}`"),
333+
|diag| {
334+
diag.suggest_prepend_item(
335+
cx,
336+
item.span,
337+
"try adding this",
338+
&format!(
339+
"impl{generics_sugg} Default for {self_type_snip}{where_clause_sugg} {{
167340
fn default() -> Self {{
168341
Self::new()
169342
}}
170-
}}")
343+
}}"
344+
),
345+
Applicability::MachineApplicable,
346+
);
347+
},
348+
);
171349
}

0 commit comments

Comments
 (0)