Skip to content

Commit 1807580

Browse files
committed
Auto merge of rust-lang#12772 - phi-gamma:redundant-struct-recreation, r=y21
add lint for recreation of an entire struct This lint makes Clippy warn about situations where an owned struct is essentially recreated by moving all its fields into a new instance of the struct. The lint is not machine-applicable because the source struct may have been partially moved. This lint originated in something I spotted during peer review. While working on their branch a colleague ended up with a commit where a function returned a struct that 1:1 replicated one of its owned inputs from its members. Initially I suspected they hadn’t run their code through Clippy but AFAICS there is no lint for this situation yet. changelog: new lint: [`redundant_owned_struct_recreation`] ### New lint checklist - \[+] Followed [lint naming conventions][lint_naming] - \[+] Added passing UI tests (including committed `.stderr` file) - \[+] `cargo test` passes locally - \[+] Executed `cargo dev update_lints` - \[+] Added lint documentation - \[+] Run `cargo dev fmt`
2 parents 7f0ed11 + 296dcf0 commit 1807580

4 files changed

+328
-43
lines changed

clippy_lints/src/unnecessary_struct_initialization.rs

+151-36
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ use clippy_utils::diagnostics::span_lint_and_sugg;
22
use clippy_utils::source::snippet;
33
use clippy_utils::ty::is_copy;
44
use clippy_utils::{get_parent_expr, path_to_local};
5-
use rustc_hir::{BindingMode, Expr, ExprKind, Node, PatKind, UnOp};
5+
use rustc_hir::{BindingMode, Expr, ExprField, ExprKind, Node, PatKind, Path, QPath, UnOp};
66
use rustc_lint::{LateContext, LateLintPass};
77
use rustc_session::declare_lint_pass;
88

99
declare_clippy_lint! {
1010
/// ### What it does
11-
/// Checks for initialization of a `struct` by copying a base without setting
12-
/// any field.
11+
/// Checks for initialization of an identical `struct` from another instance
12+
/// of the type, either by copying a base without setting any field or by
13+
/// moving all fields individually.
1314
///
1415
/// ### Why is this bad?
1516
/// Readability suffers from unnecessary struct building.
@@ -29,9 +30,14 @@ declare_clippy_lint! {
2930
/// let b = a;
3031
/// ```
3132
///
33+
/// The struct literal ``S { ..a }`` in the assignment to ``b`` could be replaced
34+
/// with just ``a``.
35+
///
3236
/// ### Known Problems
3337
/// Has false positives when the base is a place expression that cannot be
3438
/// moved out of, see [#10547](https://github.com/rust-lang/rust-clippy/issues/10547).
39+
///
40+
/// Empty structs are ignored by the lint.
3541
#[clippy::version = "1.70.0"]
3642
pub UNNECESSARY_STRUCT_INITIALIZATION,
3743
nursery,
@@ -41,42 +47,111 @@ declare_lint_pass!(UnnecessaryStruct => [UNNECESSARY_STRUCT_INITIALIZATION]);
4147

4248
impl LateLintPass<'_> for UnnecessaryStruct {
4349
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
44-
if let ExprKind::Struct(_, &[], Some(base)) = expr.kind {
45-
if let Some(parent) = get_parent_expr(cx, expr)
46-
&& let parent_ty = cx.typeck_results().expr_ty_adjusted(parent)
47-
&& parent_ty.is_any_ptr()
48-
{
49-
if is_copy(cx, cx.typeck_results().expr_ty(expr)) && path_to_local(base).is_some() {
50-
// When the type implements `Copy`, a reference to the new struct works on the
51-
// copy. Using the original would borrow it.
52-
return;
53-
}
54-
55-
if parent_ty.is_mutable_ptr() && !is_mutable(cx, base) {
56-
// The original can be used in a mutable reference context only if it is mutable.
57-
return;
58-
}
59-
}
50+
let ExprKind::Struct(_, fields, base) = expr.kind else {
51+
return;
52+
};
6053

61-
// TODO: do not propose to replace *XX if XX is not Copy
62-
if let ExprKind::Unary(UnOp::Deref, target) = base.kind
63-
&& matches!(target.kind, ExprKind::Path(..))
64-
&& !is_copy(cx, cx.typeck_results().expr_ty(expr))
65-
{
66-
// `*base` cannot be used instead of the struct in the general case if it is not Copy.
67-
return;
68-
}
54+
if expr.span.from_expansion() {
55+
// Prevent lint from hitting inside macro code
56+
return;
57+
}
58+
59+
let field_path = same_path_in_all_fields(cx, expr, fields);
60+
61+
let sugg = match (field_path, base) {
62+
(Some(&path), None) => {
63+
// all fields match, no base given
64+
path.span
65+
},
66+
(Some(path), Some(base)) if base_is_suitable(cx, expr, base) && path_matches_base(path, base) => {
67+
// all fields match, has base: ensure that the path of the base matches
68+
base.span
69+
},
70+
(None, Some(base)) if fields.is_empty() && base_is_suitable(cx, expr, base) => {
71+
// just the base, no explicit fields
72+
base.span
73+
},
74+
_ => return,
75+
};
76+
77+
span_lint_and_sugg(
78+
cx,
79+
UNNECESSARY_STRUCT_INITIALIZATION,
80+
expr.span,
81+
"unnecessary struct building",
82+
"replace with",
83+
snippet(cx, sugg, "..").into_owned(),
84+
rustc_errors::Applicability::MachineApplicable,
85+
);
86+
}
87+
}
88+
89+
fn base_is_suitable(cx: &LateContext<'_>, expr: &Expr<'_>, base: &Expr<'_>) -> bool {
90+
if !check_references(cx, expr, base) {
91+
return false;
92+
}
93+
94+
// TODO: do not propose to replace *XX if XX is not Copy
95+
if let ExprKind::Unary(UnOp::Deref, target) = base.kind
96+
&& matches!(target.kind, ExprKind::Path(..))
97+
&& !is_copy(cx, cx.typeck_results().expr_ty(expr))
98+
{
99+
// `*base` cannot be used instead of the struct in the general case if it is not Copy.
100+
return false;
101+
}
102+
true
103+
}
104+
105+
/// Check whether all fields of a struct assignment match.
106+
/// Returns a [Path] item that one can obtain a span from for the lint suggestion.
107+
///
108+
/// Conditions that must be satisfied to trigger this variant of the lint:
109+
///
110+
/// - source struct of the assignment must be of same type as the destination
111+
/// - names of destination struct fields must match the field names of the source
112+
///
113+
/// We don’t check here if all struct fields are assigned as the remainder may
114+
/// be filled in from a base struct.
115+
fn same_path_in_all_fields<'tcx>(
116+
cx: &LateContext<'_>,
117+
expr: &Expr<'_>,
118+
fields: &[ExprField<'tcx>],
119+
) -> Option<&'tcx Path<'tcx>> {
120+
let ty = cx.typeck_results().expr_ty(expr);
121+
122+
let mut found = None;
69123

70-
span_lint_and_sugg(
71-
cx,
72-
UNNECESSARY_STRUCT_INITIALIZATION,
73-
expr.span,
74-
"unnecessary struct building",
75-
"replace with",
76-
snippet(cx, base.span, "..").into_owned(),
77-
rustc_errors::Applicability::MachineApplicable,
78-
);
124+
for f in fields {
125+
// fields are assigned from expression
126+
if let ExprKind::Field(src_expr, ident) = f.expr.kind
127+
// expression type matches
128+
&& ty == cx.typeck_results().expr_ty(src_expr)
129+
// field name matches
130+
&& f.ident == ident
131+
// assigned from a path expression
132+
&& let ExprKind::Path(QPath::Resolved(None, src_path)) = src_expr.kind
133+
{
134+
let Some((_, p)) = found else {
135+
// this is the first field assignment in the list
136+
found = Some((src_expr, src_path));
137+
continue;
138+
};
139+
140+
if p.res == src_path.res {
141+
// subsequent field assignment with same origin struct as before
142+
continue;
143+
}
79144
}
145+
// source of field assignment doesn’t qualify
146+
return None;
147+
}
148+
149+
if let Some((src_expr, src_path)) = found
150+
&& check_references(cx, expr, src_expr)
151+
{
152+
Some(src_path)
153+
} else {
154+
None
80155
}
81156
}
82157

@@ -89,3 +164,43 @@ fn is_mutable(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
89164
true
90165
}
91166
}
167+
168+
fn check_references(cx: &LateContext<'_>, expr_a: &Expr<'_>, expr_b: &Expr<'_>) -> bool {
169+
if let Some(parent) = get_parent_expr(cx, expr_a)
170+
&& let parent_ty = cx.typeck_results().expr_ty_adjusted(parent)
171+
&& parent_ty.is_any_ptr()
172+
{
173+
if is_copy(cx, cx.typeck_results().expr_ty(expr_a)) && path_to_local(expr_b).is_some() {
174+
// When the type implements `Copy`, a reference to the new struct works on the
175+
// copy. Using the original would borrow it.
176+
return false;
177+
}
178+
179+
if parent_ty.is_mutable_ptr() && !is_mutable(cx, expr_b) {
180+
// The original can be used in a mutable reference context only if it is mutable.
181+
return false;
182+
}
183+
}
184+
185+
true
186+
}
187+
188+
/// When some fields are assigned from a base struct and others individually
189+
/// the lint applies only if the source of the field is the same as the base.
190+
/// This is enforced here by comparing the path of the base expression;
191+
/// needless to say the lint only applies if it (or whatever expression it is
192+
/// a reference of) actually has a path.
193+
fn path_matches_base(path: &Path<'_>, base: &Expr<'_>) -> bool {
194+
let base_path = match base.kind {
195+
ExprKind::Unary(UnOp::Deref, base_expr) => {
196+
if let ExprKind::Path(QPath::Resolved(_, base_path)) = base_expr.kind {
197+
base_path
198+
} else {
199+
return false;
200+
}
201+
},
202+
ExprKind::Path(QPath::Resolved(_, base_path)) => base_path,
203+
_ => return false,
204+
};
205+
path.res == base_path.res
206+
}

tests/ui/unnecessary_struct_initialization.fixed

+70
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ struct V {
2626
f: u32,
2727
}
2828

29+
struct W {
30+
f1: u32,
31+
f2: u32,
32+
}
33+
2934
impl Clone for V {
3035
fn clone(&self) -> Self {
3136
// Lint: `Self` implements `Copy`
@@ -68,4 +73,69 @@ fn main() {
6873

6974
// Should lint: the result of an expression is mutable and temporary
7075
let p = &mut *Box::new(T { f: 5 });
76+
77+
// Should lint: all fields of `q` would be consumed anyway
78+
let q = W { f1: 42, f2: 1337 };
79+
let r = q;
80+
81+
// Should not lint: not all fields of `t` from same source
82+
let s = W { f1: 1337, f2: 42 };
83+
let t = W { f1: s.f1, f2: r.f2 };
84+
85+
// Should not lint: different fields of `s` assigned
86+
let u = W { f1: s.f2, f2: s.f1 };
87+
88+
// Should lint: all fields of `v` would be consumed anyway
89+
let v = W { f1: 42, f2: 1337 };
90+
let w = v;
91+
92+
// Should not lint: source differs between fields and base
93+
let x = W { f1: 42, f2: 1337 };
94+
let y = W { f1: w.f1, ..x };
95+
96+
// Should lint: range desugars to struct
97+
let r1 = 0..5;
98+
let r2 = r1;
99+
100+
references();
101+
shorthand();
102+
}
103+
104+
fn references() {
105+
// Should not lint as `a` is not mutable
106+
let a = W { f1: 42, f2: 1337 };
107+
let b = &mut W { f1: a.f1, f2: a.f2 };
108+
109+
// Should lint as `d` is a shared reference
110+
let c = W { f1: 42, f2: 1337 };
111+
let d = &c;
112+
113+
// Should not lint as `e` is not mutable
114+
let e = W { f1: 42, f2: 1337 };
115+
let f = &mut W { f1: e.f1, ..e };
116+
117+
// Should lint as `h` is a shared reference
118+
let g = W { f1: 42, f2: 1337 };
119+
let h = &g;
120+
121+
// Should not lint as `j` is copy
122+
let i = V { f: 0x1701d };
123+
let j = &V { ..i };
124+
125+
// Should not lint as `k` is copy
126+
let k = V { f: 0x1701d };
127+
let l = &V { f: k.f };
128+
}
129+
130+
fn shorthand() {
131+
struct S1 {
132+
a: i32,
133+
b: i32,
134+
}
135+
136+
let a = 42;
137+
let s = S1 { a: 3, b: 4 };
138+
139+
// Should not lint: `a` is not from `s`
140+
let s = S1 { a, b: s.b };
71141
}

tests/ui/unnecessary_struct_initialization.rs

+70
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ struct V {
2626
f: u32,
2727
}
2828

29+
struct W {
30+
f1: u32,
31+
f2: u32,
32+
}
33+
2934
impl Clone for V {
3035
fn clone(&self) -> Self {
3136
// Lint: `Self` implements `Copy`
@@ -72,4 +77,69 @@ fn main() {
7277
let p = &mut T {
7378
..*Box::new(T { f: 5 })
7479
};
80+
81+
// Should lint: all fields of `q` would be consumed anyway
82+
let q = W { f1: 42, f2: 1337 };
83+
let r = W { f1: q.f1, f2: q.f2 };
84+
85+
// Should not lint: not all fields of `t` from same source
86+
let s = W { f1: 1337, f2: 42 };
87+
let t = W { f1: s.f1, f2: r.f2 };
88+
89+
// Should not lint: different fields of `s` assigned
90+
let u = W { f1: s.f2, f2: s.f1 };
91+
92+
// Should lint: all fields of `v` would be consumed anyway
93+
let v = W { f1: 42, f2: 1337 };
94+
let w = W { f1: v.f1, ..v };
95+
96+
// Should not lint: source differs between fields and base
97+
let x = W { f1: 42, f2: 1337 };
98+
let y = W { f1: w.f1, ..x };
99+
100+
// Should lint: range desugars to struct
101+
let r1 = 0..5;
102+
let r2 = r1.start..r1.end;
103+
104+
references();
105+
shorthand();
106+
}
107+
108+
fn references() {
109+
// Should not lint as `a` is not mutable
110+
let a = W { f1: 42, f2: 1337 };
111+
let b = &mut W { f1: a.f1, f2: a.f2 };
112+
113+
// Should lint as `d` is a shared reference
114+
let c = W { f1: 42, f2: 1337 };
115+
let d = &W { f1: c.f1, f2: c.f2 };
116+
117+
// Should not lint as `e` is not mutable
118+
let e = W { f1: 42, f2: 1337 };
119+
let f = &mut W { f1: e.f1, ..e };
120+
121+
// Should lint as `h` is a shared reference
122+
let g = W { f1: 42, f2: 1337 };
123+
let h = &W { f1: g.f1, ..g };
124+
125+
// Should not lint as `j` is copy
126+
let i = V { f: 0x1701d };
127+
let j = &V { ..i };
128+
129+
// Should not lint as `k` is copy
130+
let k = V { f: 0x1701d };
131+
let l = &V { f: k.f };
132+
}
133+
134+
fn shorthand() {
135+
struct S1 {
136+
a: i32,
137+
b: i32,
138+
}
139+
140+
let a = 42;
141+
let s = S1 { a: 3, b: 4 };
142+
143+
// Should not lint: `a` is not from `s`
144+
let s = S1 { a, b: s.b };
75145
}

0 commit comments

Comments
 (0)