Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 75923df

Browse files
committed
Split out wildcard_enum_match_arm and match_wildcard_for_single_variants
1 parent dc75695 commit 75923df

File tree

2 files changed

+203
-195
lines changed

2 files changed

+203
-195
lines changed
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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+
#[allow(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+
#[allow(clippy::match_same_arms)]
60+
let id = match cx.qpath_res(path, pat.hir_id) {
61+
Res::Def(
62+
DefKind::Const | DefKind::ConstParam | DefKind::AnonConst | DefKind::InlineConst,
63+
_,
64+
) => return,
65+
Res::Def(_, id) => id,
66+
_ => return,
67+
};
68+
if arm.guard.is_none() {
69+
missing_variants.retain(|e| e.ctor_def_id != Some(id));
70+
}
71+
path
72+
},
73+
PatKind::TupleStruct(path, patterns, ..) => {
74+
if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() {
75+
if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p)) {
76+
missing_variants.retain(|e| e.ctor_def_id != Some(id));
77+
}
78+
}
79+
path
80+
},
81+
PatKind::Struct(path, patterns, ..) => {
82+
if let Some(id) = cx.qpath_res(path, pat.hir_id).opt_def_id() {
83+
if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p.pat)) {
84+
missing_variants.retain(|e| e.def_id != id);
85+
}
86+
}
87+
path
88+
},
89+
_ => return,
90+
};
91+
match path {
92+
QPath::Resolved(_, path) => path_prefix.with_path(path.segments),
93+
QPath::TypeRelative(
94+
Ty {
95+
kind: TyKind::Path(QPath::Resolved(_, path)),
96+
..
97+
},
98+
_,
99+
) => path_prefix.with_prefix(path.segments),
100+
_ => (),
101+
}
102+
});
103+
}
104+
105+
let format_suggestion = |variant: &VariantDef| {
106+
format!(
107+
"{}{}{}{}",
108+
if let Some(ident) = wildcard_ident {
109+
format!("{} @ ", ident.name)
110+
} else {
111+
String::new()
112+
},
113+
if let CommonPrefixSearcher::Path(path_prefix) = path_prefix {
114+
let mut s = String::new();
115+
for seg in path_prefix {
116+
s.push_str(seg.ident.as_str());
117+
s.push_str("::");
118+
}
119+
s
120+
} else {
121+
let mut s = cx.tcx.def_path_str(adt_def.did);
122+
s.push_str("::");
123+
s
124+
},
125+
variant.name,
126+
match variant.ctor_kind {
127+
CtorKind::Fn if variant.fields.len() == 1 => "(_)",
128+
CtorKind::Fn => "(..)",
129+
CtorKind::Const => "",
130+
CtorKind::Fictive => "{ .. }",
131+
}
132+
)
133+
};
134+
135+
match missing_variants.as_slice() {
136+
[] => (),
137+
[x] if !adt_def.is_variant_list_non_exhaustive() && !has_hidden => span_lint_and_sugg(
138+
cx,
139+
MATCH_WILDCARD_FOR_SINGLE_VARIANTS,
140+
wildcard_span,
141+
"wildcard matches only a single variant and will also match any future added variants",
142+
"try this",
143+
format_suggestion(x),
144+
Applicability::MaybeIncorrect,
145+
),
146+
variants => {
147+
let mut suggestions: Vec<_> = variants.iter().copied().map(format_suggestion).collect();
148+
let message = if adt_def.is_variant_list_non_exhaustive() || has_hidden {
149+
suggestions.push("_".into());
150+
"wildcard matches known variants and will also match future added variants"
151+
} else {
152+
"wildcard match will also match any future added variants"
153+
};
154+
155+
span_lint_and_sugg(
156+
cx,
157+
WILDCARD_ENUM_MATCH_ARM,
158+
wildcard_span,
159+
message,
160+
"try this",
161+
suggestions.join(" | "),
162+
Applicability::MaybeIncorrect,
163+
);
164+
},
165+
};
166+
}
167+
168+
enum CommonPrefixSearcher<'a> {
169+
None,
170+
Path(&'a [PathSegment<'a>]),
171+
Mixed,
172+
}
173+
impl<'a> CommonPrefixSearcher<'a> {
174+
fn with_path(&mut self, path: &'a [PathSegment<'a>]) {
175+
match path {
176+
[path @ .., _] => self.with_prefix(path),
177+
[] => (),
178+
}
179+
}
180+
181+
fn with_prefix(&mut self, path: &'a [PathSegment<'a>]) {
182+
match self {
183+
Self::None => *self = Self::Path(path),
184+
Self::Path(self_path)
185+
if path
186+
.iter()
187+
.map(|p| p.ident.name)
188+
.eq(self_path.iter().map(|p| p.ident.name)) => {},
189+
Self::Path(_) => *self = Self::Mixed,
190+
Self::Mixed => (),
191+
}
192+
}
193+
}
194+
195+
fn is_hidden(cx: &LateContext<'_>, variant_def: &VariantDef) -> bool {
196+
let attrs = cx.tcx.get_attrs(variant_def.def_id);
197+
clippy_utils::attrs::is_doc_hidden(attrs) || clippy_utils::attrs::is_unstable(attrs)
198+
}

0 commit comments

Comments
 (0)