]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/large_enum_variant.rs
Merge commit 'fdb84cbfd25908df5683f8f62388f663d9260e39' into clippyup
[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 auxiliary
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     /// 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 impl_lint_pass!(LargeEnumVariant => [LARGE_ENUM_VARIANT]);
87
88 impl<'tcx> LateLintPass<'tcx> for LargeEnumVariant {
89     fn check_item(&mut self, cx: &LateContext<'tcx>, item: &Item<'tcx>) {
90         if in_external_macro(cx.tcx.sess, item.span) {
91             return;
92         }
93         if let ItemKind::Enum(ref def, _) = item.kind {
94             let ty = cx.tcx.type_of(item.def_id);
95             let adt = ty.ty_adt_def().expect("already checked whether this is an enum");
96             if adt.variants().len() <= 1 {
97                 return;
98             }
99             let mut variants_size: Vec<VariantInfo> = Vec::new();
100             for (i, variant) in adt.variants().iter().enumerate() {
101                 let mut fields_size = Vec::new();
102                 for (i, f) in variant.fields.iter().enumerate() {
103                     let ty = cx.tcx.type_of(f.did);
104                     // don't lint variants which have a field of generic type.
105                     match cx.layout_of(ty) {
106                         Ok(l) => {
107                             let fsize = l.size.bytes();
108                             fields_size.push(FieldInfo { ind: i, size: fsize });
109                         },
110                         Err(_) => {
111                             return;
112                         },
113                     }
114                 }
115                 let size: u64 = fields_size.iter().map(|info| info.size).sum();
116
117                 variants_size.push(VariantInfo {
118                     ind: i,
119                     size,
120                     fields_size,
121                 });
122             }
123
124             variants_size.sort_by(|a, b| (b.size.cmp(&a.size)));
125
126             let mut difference = variants_size[0].size - variants_size[1].size;
127             if difference > self.maximum_size_difference_allowed {
128                 let help_text = "consider boxing the large fields to reduce the total size of the enum";
129                 span_lint_and_then(
130                     cx,
131                     LARGE_ENUM_VARIANT,
132                     def.variants[variants_size[0].ind].span,
133                     "large size difference between variants",
134                     |diag| {
135                         diag.span_label(
136                             def.variants[variants_size[0].ind].span,
137                             &format!("this variant is {} bytes", variants_size[0].size),
138                         );
139                         diag.span_note(
140                             def.variants[variants_size[1].ind].span,
141                             &format!("and the second-largest variant is {} bytes:", variants_size[1].size),
142                         );
143
144                         let fields = def.variants[variants_size[0].ind].data.fields();
145                         variants_size[0].fields_size.sort_by(|a, b| (a.size.cmp(&b.size)));
146                         let mut applicability = Applicability::MaybeIncorrect;
147                         if is_copy(cx, ty) || maybe_copy(cx, ty) {
148                             diag.span_note(
149                                 item.ident.span,
150                                 "boxing a variant would require the type no longer be `Copy`",
151                             );
152                         } else {
153                             let sugg: Vec<(Span, String)> = variants_size[0]
154                                 .fields_size
155                                 .iter()
156                                 .rev()
157                                 .map_while(|val| {
158                                     if difference > self.maximum_size_difference_allowed {
159                                         difference = difference.saturating_sub(val.size);
160                                         Some((
161                                             fields[val.ind].ty.span,
162                                             format!(
163                                                 "Box<{}>",
164                                                 snippet_with_applicability(
165                                                     cx,
166                                                     fields[val.ind].ty.span,
167                                                     "..",
168                                                     &mut applicability
169                                                 )
170                                                 .into_owned()
171                                             ),
172                                         ))
173                                     } else {
174                                         None
175                                     }
176                                 })
177                                 .collect();
178
179                             if !sugg.is_empty() {
180                                 diag.multipart_suggestion(help_text, sugg, Applicability::MaybeIncorrect);
181                                 return;
182                             }
183                         }
184                         diag.span_help(def.variants[variants_size[0].ind].span, help_text);
185                     },
186                 );
187             }
188         }
189     }
190 }
191
192 fn maybe_copy<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> bool {
193     if let Adt(_def, substs) = ty.kind()
194         && substs.types().next().is_some()
195         && let Some(copy_trait) = cx.tcx.lang_items().copy_trait()
196     {
197         return cx.tcx.non_blanket_impls_for_ty(copy_trait, ty).next().is_some();
198     }
199     false
200 }