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