]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/large_enum_variant.rs
Rollup merge of #103637 - ChrisDenton:stdio-uwp, r=thomcc
[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.owner_id);
127             let Adt(adt, subst) = ty.kind() else {
128                 panic!("already checked whether this is an enum")
129             };
130             if adt.variants().len() <= 1 {
131                 return;
132             }
133             let variants_size = variants_size(cx, *adt, subst);
134
135             let mut difference = variants_size[0].size - variants_size[1].size;
136             if difference > self.maximum_size_difference_allowed {
137                 let help_text = "consider boxing the large fields to reduce the total size of the enum";
138                 span_lint_and_then(
139                     cx,
140                     LARGE_ENUM_VARIANT,
141                     item.span,
142                     "large size difference between variants",
143                     |diag| {
144                         diag.span_label(
145                             item.span,
146                             format!("the entire enum is at least {} bytes", approx_ty_size(cx, ty)),
147                         );
148                         diag.span_label(
149                             def.variants[variants_size[0].ind].span,
150                             format!("the largest variant contains at least {} bytes", variants_size[0].size),
151                         );
152                         diag.span_label(
153                             def.variants[variants_size[1].ind].span,
154                             &if variants_size[1].fields_size.is_empty() {
155                                 "the second-largest variant carries no data at all".to_owned()
156                             } else {
157                                 format!(
158                                     "the second-largest variant contains at least {} bytes",
159                                     variants_size[1].size
160                                 )
161                             },
162                         );
163
164                         let fields = def.variants[variants_size[0].ind].data.fields();
165                         let mut applicability = Applicability::MaybeIncorrect;
166                         if is_copy(cx, ty) || maybe_copy(cx, ty) {
167                             diag.span_note(
168                                 item.ident.span,
169                                 "boxing a variant would require the type no longer be `Copy`",
170                             );
171                         } else {
172                             let sugg: Vec<(Span, String)> = variants_size[0]
173                                 .fields_size
174                                 .iter()
175                                 .rev()
176                                 .map_while(|val| {
177                                     if difference > self.maximum_size_difference_allowed {
178                                         difference = difference.saturating_sub(val.size);
179                                         Some((
180                                             fields[val.ind].ty.span,
181                                             format!(
182                                                 "Box<{}>",
183                                                 snippet_with_applicability(
184                                                     cx,
185                                                     fields[val.ind].ty.span,
186                                                     "..",
187                                                     &mut applicability
188                                                 )
189                                                 .into_owned()
190                                             ),
191                                         ))
192                                     } else {
193                                         None
194                                     }
195                                 })
196                                 .collect();
197
198                             if !sugg.is_empty() {
199                                 diag.multipart_suggestion(help_text, sugg, Applicability::MaybeIncorrect);
200                                 return;
201                             }
202                         }
203                         diag.span_help(def.variants[variants_size[0].ind].span, help_text);
204                     },
205                 );
206             }
207         }
208     }
209 }
210
211 fn maybe_copy<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
212     if let Adt(_def, substs) = ty.kind()
213         && substs.types().next().is_some()
214         && let Some(copy_trait) = cx.tcx.lang_items().copy_trait()
215     {
216         return cx.tcx.non_blanket_impls_for_ty(copy_trait, ty).next().is_some();
217     }
218     false
219 }