1 //! lint when there is a large size difference between variants on an enum
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;
14 declare_clippy_lint! {
16 /// Checks for large size differences between variants on
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.
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.
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).
36 /// The lint will ignore generic types if the layout depends on the
37 /// generics, even if the size difference will be large anyway.
49 /// // Possibly better
52 /// B(Box<[i32; 8000]>),
55 #[clippy::version = "pre 1.29.0"]
56 pub LARGE_ENUM_VARIANT,
58 "large size difference between variants on an enum"
61 #[derive(Copy, Clone)]
62 pub struct LargeEnumVariant {
63 maximum_size_difference_allowed: u64,
66 impl LargeEnumVariant {
68 pub fn new(maximum_size_difference_allowed: u64) -> Self {
70 maximum_size_difference_allowed,
83 fields_size: Vec<FieldInfo>,
86 impl_lint_pass!(LargeEnumVariant => [LARGE_ENUM_VARIANT]);
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) {
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 {
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) {
107 let fsize = l.size.bytes();
108 fields_size.push(FieldInfo { ind: i, size: fsize });
115 let size: u64 = fields_size.iter().map(|info| info.size).sum();
117 variants_size.push(VariantInfo {
124 variants_size.sort_by(|a, b| (b.size.cmp(&a.size)));
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";
132 def.variants[variants_size[0].ind].span,
133 "large size difference between variants",
136 def.variants[variants_size[0].ind].span,
137 &format!("this variant is {} bytes", variants_size[0].size),
140 def.variants[variants_size[1].ind].span,
141 &format!("and the second-largest variant is {} bytes:", variants_size[1].size),
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) {
150 "boxing a variant would require the type no longer be `Copy`",
153 let sugg: Vec<(Span, String)> = variants_size[0]
158 if difference > self.maximum_size_difference_allowed {
159 difference = difference.saturating_sub(val.size);
161 fields[val.ind].ty.span,
164 snippet_with_applicability(
166 fields[val.ind].ty.span,
179 if !sugg.is_empty() {
180 diag.multipart_suggestion(help_text, sugg, Applicability::MaybeIncorrect);
184 diag.span_help(def.variants[variants_size[0].ind].span, help_text);
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()
197 return cx.tcx.non_blanket_impls_for_ty(copy_trait, ty).next().is_some();