]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/large_enum_variant.rs
Auto merge of #103116 - TaKO8Ki:fix-103053, r=lcnr
[rust.git] / src / tools / clippy / clippy_lints / src / large_enum_variant.rs
1 //! lint when there is a large size difference between variants on an enum
2
3 use clippy_utils::source::snippet_with_applicability;
4 use clippy_utils::{diagnostics::span_lint_and_then, ty::approx_ty_size, ty::is_copy};
5 use rustc_errors::Applicability;
6 use rustc_hir::{Item, ItemKind};
7 use rustc_lint::{LateContext, LateLintPass};
8 use rustc_middle::lint::in_external_macro;
9 use rustc_middle::ty::{Adt, AdtDef, GenericArg, List, Ty};
10 use rustc_session::{declare_tool_lint, impl_lint_pass};
11 use rustc_span::source_map::Span;
12
13 declare_clippy_lint! {
14     /// ### What it does
15     /// Checks for large size differences between variants on
16     /// `enum`s.
17     ///
18     /// ### Why is this bad?
19     /// Enum size is bounded by the largest variant. Having one
20     /// large variant can penalize the memory layout of that enum.
21     ///
22     /// ### Known problems
23     /// This lint obviously cannot take the distribution of
24     /// variants in your running program into account. It is possible that the
25     /// smaller variants make up less than 1% of all instances, in which case
26     /// the overhead is negligible and the boxing is counter-productive. Always
27     /// measure the change this lint suggests.
28     ///
29     /// For types that implement `Copy`, the suggestion to `Box` a variant's
30     /// data would require removing the trait impl. The types can of course
31     /// still be `Clone`, but that is worse ergonomically. Depending on the
32     /// use case it may be possible to store the large data in an auxiliary
33     /// structure (e.g. Arena or ECS).
34     ///
35     /// The lint will ignore the impact of generic types to the type layout by
36     /// assuming every type parameter is zero-sized. Depending on your use case,
37     /// this may lead to a false positive.
38     ///
39     /// ### Example
40     /// ```rust
41     /// enum Test {
42     ///     A(i32),
43     ///     B([i32; 8000]),
44     /// }
45     /// ```
46     ///
47     /// Use instead:
48     /// ```rust
49     /// // Possibly better
50     /// enum Test2 {
51     ///     A(i32),
52     ///     B(Box<[i32; 8000]>),
53     /// }
54     /// ```
55     #[clippy::version = "pre 1.29.0"]
56     pub LARGE_ENUM_VARIANT,
57     perf,
58     "large size difference between variants on an enum"
59 }
60
61 #[derive(Copy, Clone)]
62 pub struct LargeEnumVariant {
63     maximum_size_difference_allowed: u64,
64 }
65
66 impl LargeEnumVariant {
67     #[must_use]
68     pub fn new(maximum_size_difference_allowed: u64) -> Self {
69         Self {
70             maximum_size_difference_allowed,
71         }
72     }
73 }
74
75 struct FieldInfo {
76     ind: usize,
77     size: u64,
78 }
79
80 struct VariantInfo {
81     ind: usize,
82     size: u64,
83     fields_size: Vec<FieldInfo>,
84 }
85
86 fn variants_size<'tcx>(
87     cx: &LateContext<'tcx>,
88     adt: AdtDef<'tcx>,
89     subst: &'tcx List<GenericArg<'tcx>>,
90 ) -> Vec<VariantInfo> {
91     let mut variants_size = adt
92         .variants()
93         .iter()
94         .enumerate()
95         .map(|(i, variant)| {
96             let mut fields_size = variant
97                 .fields
98                 .iter()
99                 .enumerate()
100                 .map(|(i, f)| FieldInfo {
101                     ind: i,
102                     size: approx_ty_size(cx, f.ty(cx.tcx, subst)),
103                 })
104                 .collect::<Vec<_>>();
105             fields_size.sort_by(|a, b| (a.size.cmp(&b.size)));
106
107             VariantInfo {
108                 ind: i,
109                 size: fields_size.iter().map(|info| info.size).sum(),
110                 fields_size,
111             }
112         })
113         .collect::<Vec<_>>();
114     variants_size.sort_by(|a, b| (b.size.cmp(&a.size)));
115     variants_size
116 }
117
118 impl_lint_pass!(LargeEnumVariant => [LARGE_ENUM_VARIANT]);
119
120 impl<'tcx> LateLintPass<'tcx> for LargeEnumVariant {
121     fn check_item(&mut self, cx: &LateContext<'tcx>, item: &Item<'tcx>) {
122         if in_external_macro(cx.tcx.sess, item.span) {
123             return;
124         }
125         if let ItemKind::Enum(ref def, _) = item.kind {
126             let ty = cx.tcx.type_of(item.def_id);
127             let (adt, subst) = match ty.kind() {
128                 Adt(adt, subst) => (adt, subst),
129                 _ => panic!("already checked whether this is an enum"),
130             };
131             if adt.variants().len() <= 1 {
132                 return;
133             }
134             let variants_size = variants_size(cx, *adt, subst);
135
136             let mut difference = variants_size[0].size - variants_size[1].size;
137             if difference > self.maximum_size_difference_allowed {
138                 let help_text = "consider boxing the large fields to reduce the total size of the enum";
139                 span_lint_and_then(
140                     cx,
141                     LARGE_ENUM_VARIANT,
142                     item.span,
143                     "large size difference between variants",
144                     |diag| {
145                         diag.span_label(
146                             item.span,
147                             format!("the entire enum is at least {} bytes", approx_ty_size(cx, ty)),
148                         );
149                         diag.span_label(
150                             def.variants[variants_size[0].ind].span,
151                             format!("the largest variant contains at least {} bytes", variants_size[0].size),
152                         );
153                         diag.span_label(
154                             def.variants[variants_size[1].ind].span,
155                             &if variants_size[1].fields_size.is_empty() {
156                                 "the second-largest variant carries no data at all".to_owned()
157                             } else {
158                                 format!(
159                                     "the second-largest variant contains at least {} bytes",
160                                     variants_size[1].size
161                                 )
162                             },
163                         );
164
165                         let fields = def.variants[variants_size[0].ind].data.fields();
166                         let mut applicability = Applicability::MaybeIncorrect;
167                         if is_copy(cx, ty) || maybe_copy(cx, ty) {
168                             diag.span_note(
169                                 item.ident.span,
170                                 "boxing a variant would require the type no longer be `Copy`",
171                             );
172                         } else {
173                             let sugg: Vec<(Span, String)> = variants_size[0]
174                                 .fields_size
175                                 .iter()
176                                 .rev()
177                                 .map_while(|val| {
178                                     if difference > self.maximum_size_difference_allowed {
179                                         difference = difference.saturating_sub(val.size);
180                                         Some((
181                                             fields[val.ind].ty.span,
182                                             format!(
183                                                 "Box<{}>",
184                                                 snippet_with_applicability(
185                                                     cx,
186                                                     fields[val.ind].ty.span,
187                                                     "..",
188                                                     &mut applicability
189                                                 )
190                                                 .into_owned()
191                                             ),
192                                         ))
193                                     } else {
194                                         None
195                                     }
196                                 })
197                                 .collect();
198
199                             if !sugg.is_empty() {
200                                 diag.multipart_suggestion(help_text, sugg, Applicability::MaybeIncorrect);
201                                 return;
202                             }
203                         }
204                         diag.span_help(def.variants[variants_size[0].ind].span, help_text);
205                     },
206                 );
207             }
208         }
209     }
210 }
211
212 fn maybe_copy<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
213     if let Adt(_def, substs) = ty.kind()
214         && substs.types().next().is_some()
215         && let Some(copy_trait) = cx.tcx.lang_items().copy_trait()
216     {
217         return cx.tcx.non_blanket_impls_for_ty(copy_trait, ty).next().is_some();
218     }
219     false
220 }