]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/matches/match_wild_enum.rs
Rollup merge of #100767 - kadiwa4:escape_ascii, r=jackh726
[rust.git] / src / tools / clippy / clippy_lints / src / matches / match_wild_enum.rs
1 use clippy_utils::diagnostics::span_lint_and_sugg;
2 use clippy_utils::ty::is_type_diagnostic_item;
3 use clippy_utils::{is_refutable, peel_hir_pat_refs, recurse_or_patterns};
4 use rustc_errors::Applicability;
5 use rustc_hir::def::{CtorKind, DefKind, Res};
6 use rustc_hir::{Arm, Expr, PatKind, PathSegment, QPath, Ty, TyKind};
7 use rustc_lint::LateContext;
8 use rustc_middle::ty::{self, VariantDef};
9 use rustc_span::sym;
10
11 use super::{MATCH_WILDCARD_FOR_SINGLE_VARIANTS, WILDCARD_ENUM_MATCH_ARM};
12
13 #[expect(clippy::too_many_lines)]
14 pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) {
15     let ty = cx.typeck_results().expr_ty(ex).peel_refs();
16     let adt_def = match ty.kind() {
17         ty::Adt(adt_def, _)
18             if adt_def.is_enum()
19                 && !(is_type_diagnostic_item(cx, ty, sym::Option) || is_type_diagnostic_item(cx, ty, sym::Result)) =>
20         {
21             adt_def
22         },
23         _ => return,
24     };
25
26     // First pass - check for violation, but don't do much book-keeping because this is hopefully
27     // the uncommon case, and the book-keeping is slightly expensive.
28     let mut wildcard_span = None;
29     let mut wildcard_ident = None;
30     let mut has_non_wild = false;
31     for arm in arms {
32         match peel_hir_pat_refs(arm.pat).0.kind {
33             PatKind::Wild => wildcard_span = Some(arm.pat.span),
34             PatKind::Binding(_, _, ident, None) => {
35                 wildcard_span = Some(arm.pat.span);
36                 wildcard_ident = Some(ident);
37             },
38             _ => has_non_wild = true,
39         }
40     }
41     let wildcard_span = match wildcard_span {
42         Some(x) if has_non_wild => x,
43         _ => return,
44     };
45
46     // Accumulate the variants which should be put in place of the wildcard because they're not
47     // already covered.
48     let has_hidden = adt_def.variants().iter().any(|x| is_hidden(cx, x));
49     let mut missing_variants: Vec<_> = adt_def.variants().iter().filter(|x| !is_hidden(cx, x)).collect();
50
51     let mut path_prefix = CommonPrefixSearcher::None;
52     for arm in arms {
53         // Guards mean that this case probably isn't exhaustively covered. Technically
54         // this is incorrect, as we should really check whether each variant is exhaustively
55         // covered by the set of guards that cover it, but that's really hard to do.
56         recurse_or_patterns(arm.pat, |pat| {
57             let path = match &peel_hir_pat_refs(pat).0.kind {
58                 PatKind::Path(path) => {
59                     let id = match cx.qpath_res(path, pat.hir_id) {
60                         Res::Def(
61                             DefKind::Const | DefKind::ConstParam | DefKind::AnonConst | DefKind::InlineConst,
62                             _,
63                         ) => return,
64                         Res::Def(_, id) => id,
65                         _ => return,
66                     };
67                     if arm.guard.is_none() {
68                         missing_variants.retain(|e| e.ctor_def_id != Some(id));
69                     }
70                     path
71                 },
72                 PatKind::TupleStruct(path, patterns, ..) => {
73                     if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() {
74                         if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p)) {
75                             missing_variants.retain(|e| e.ctor_def_id != Some(id));
76                         }
77                     }
78                     path
79                 },
80                 PatKind::Struct(path, patterns, ..) => {
81                     if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() {
82                         if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p.pat)) {
83                             missing_variants.retain(|e| e.def_id != id);
84                         }
85                     }
86                     path
87                 },
88                 _ => return,
89             };
90             match path {
91                 QPath::Resolved(_, path) => path_prefix.with_path(path.segments),
92                 QPath::TypeRelative(
93                     Ty {
94                         kind: TyKind::Path(QPath::Resolved(_, path)),
95                         ..
96                     },
97                     _,
98                 ) => path_prefix.with_prefix(path.segments),
99                 _ => (),
100             }
101         });
102     }
103
104     let format_suggestion = |variant: &VariantDef| {
105         format!(
106             "{}{}{}{}",
107             if let Some(ident) = wildcard_ident {
108                 format!("{} @ ", ident.name)
109             } else {
110                 String::new()
111             },
112             if let CommonPrefixSearcher::Path(path_prefix) = path_prefix {
113                 let mut s = String::new();
114                 for seg in path_prefix {
115                     s.push_str(seg.ident.as_str());
116                     s.push_str("::");
117                 }
118                 s
119             } else {
120                 let mut s = cx.tcx.def_path_str(adt_def.did());
121                 s.push_str("::");
122                 s
123             },
124             variant.name,
125             match variant.ctor_kind {
126                 CtorKind::Fn if variant.fields.len() == 1 => "(_)",
127                 CtorKind::Fn => "(..)",
128                 CtorKind::Const => "",
129                 CtorKind::Fictive => "{ .. }",
130             }
131         )
132     };
133
134     match missing_variants.as_slice() {
135         [] => (),
136         [x] if !adt_def.is_variant_list_non_exhaustive() && !has_hidden => span_lint_and_sugg(
137             cx,
138             MATCH_WILDCARD_FOR_SINGLE_VARIANTS,
139             wildcard_span,
140             "wildcard matches only a single variant and will also match any future added variants",
141             "try this",
142             format_suggestion(x),
143             Applicability::MaybeIncorrect,
144         ),
145         variants => {
146             let mut suggestions: Vec<_> = variants.iter().copied().map(format_suggestion).collect();
147             let message = if adt_def.is_variant_list_non_exhaustive() || has_hidden {
148                 suggestions.push("_".into());
149                 "wildcard matches known variants and will also match future added variants"
150             } else {
151                 "wildcard match will also match any future added variants"
152             };
153
154             span_lint_and_sugg(
155                 cx,
156                 WILDCARD_ENUM_MATCH_ARM,
157                 wildcard_span,
158                 message,
159                 "try this",
160                 suggestions.join(" | "),
161                 Applicability::MaybeIncorrect,
162             );
163         },
164     };
165 }
166
167 enum CommonPrefixSearcher<'a> {
168     None,
169     Path(&'a [PathSegment<'a>]),
170     Mixed,
171 }
172 impl<'a> CommonPrefixSearcher<'a> {
173     fn with_path(&mut self, path: &'a [PathSegment<'a>]) {
174         match path {
175             [path @ .., _] => self.with_prefix(path),
176             [] => (),
177         }
178     }
179
180     fn with_prefix(&mut self, path: &'a [PathSegment<'a>]) {
181         match self {
182             Self::None => *self = Self::Path(path),
183             Self::Path(self_path)
184                 if path
185                     .iter()
186                     .map(|p| p.ident.name)
187                     .eq(self_path.iter().map(|p| p.ident.name)) => {},
188             Self::Path(_) => *self = Self::Mixed,
189             Self::Mixed => (),
190         }
191     }
192 }
193
194 fn is_hidden(cx: &LateContext<'_>, variant_def: &VariantDef) -> bool {
195     cx.tcx.is_doc_hidden(variant_def.def_id) || cx.tcx.has_attr(variant_def.def_id, sym::unstable)
196 }