Skip to content

Commit c2e9608

Browse files
committed
Handle or patterns in single_match and single_match_else
1 parent 11a3aa2 commit c2e9608

File tree

11 files changed

+407
-146
lines changed

11 files changed

+407
-146
lines changed

clippy_lints/src/matches/match_wild_enum.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,8 @@ enum CommonPrefixSearcher<'a> {
176176
}
177177
impl<'a> CommonPrefixSearcher<'a> {
178178
fn with_path(&mut self, path: &'a [PathSegment<'a>]) {
179-
match path {
180-
[path @ .., _] => self.with_prefix(path),
181-
[] => (),
179+
if let [path @ .., _] = path {
180+
self.with_prefix(path);
182181
}
183182
}
184183

Lines changed: 240 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
use clippy_utils::diagnostics::span_lint_and_sugg;
22
use clippy_utils::source::{expr_block, snippet, SpanRangeExt};
3-
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item, peel_mid_ty_refs};
4-
use clippy_utils::{is_lint_allowed, is_unit_expr, is_wild, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs};
5-
use core::cmp::max;
3+
use clippy_utils::ty::{implements_trait, peel_mid_ty_refs};
4+
use clippy_utils::{is_lint_allowed, is_unit_expr, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs};
5+
use core::ops::ControlFlow;
6+
use rustc_arena::DroplessArena;
67
use rustc_errors::Applicability;
7-
use rustc_hir::{Arm, BindingMode, Expr, ExprKind, Pat, PatKind};
8+
use rustc_hir::def::{DefKind, Res};
9+
use rustc_hir::intravisit::{walk_pat, Visitor};
10+
use rustc_hir::{Arm, Expr, ExprKind, HirId, Pat, PatKind, QPath};
811
use rustc_lint::LateContext;
9-
use rustc_middle::ty::{self, Ty};
12+
use rustc_middle::ty::{self, AdtDef, ParamEnv, TyCtxt, TypeckResults, VariantDef};
1013
use rustc_span::{sym, Span};
1114

1215
use super::{MATCH_BOOL, SINGLE_MATCH, SINGLE_MATCH_ELSE};
@@ -27,7 +30,7 @@ fn empty_arm_has_comment(cx: &LateContext<'_>, span: Span) -> bool {
2730
}
2831

2932
#[rustfmt::skip]
30-
pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) {
33+
pub(crate) fn check<'tcx>(cx: &LateContext<'tcx>, ex: &'tcx Expr<'_>, arms: &'tcx [Arm<'_>], expr: &'tcx Expr<'_>) {
3134
if let [arm1, arm2] = arms
3235
&& arm1.guard.is_none()
3336
&& arm2.guard.is_none()
@@ -50,10 +53,29 @@ pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr:
5053
return;
5154
};
5255

53-
let ty = cx.typeck_results().expr_ty(ex);
54-
if (*ty.kind() != ty::Bool || is_lint_allowed(cx, MATCH_BOOL, ex.hir_id))
55-
&& (is_wild(arm2.pat) || form_exhaustive_matches(cx, ty, arm1.pat, arm2.pat))
56-
{
56+
let typeck = cx.typeck_results();
57+
if *typeck.expr_ty(ex).peel_refs().kind() != ty::Bool || is_lint_allowed(cx, MATCH_BOOL, ex.hir_id) {
58+
let mut v = PatVisitor {
59+
typeck,
60+
has_enum: false,
61+
};
62+
if v.visit_pat(arm2.pat).is_break() {
63+
return;
64+
}
65+
if v.has_enum {
66+
let cx = PatCtxt {
67+
tcx: cx.tcx,
68+
param_env: cx.param_env,
69+
typeck,
70+
arena: DroplessArena::default(),
71+
};
72+
let mut state = PatState::Other;
73+
if !(state.add_pat(&cx, arm2.pat) || state.add_pat(&cx, arm1.pat)) {
74+
// Don't lint if the pattern contains an enum which doesn't have a wild match.
75+
return;
76+
}
77+
}
78+
5779
report_single_pattern(cx, ex, arm1, expr, els);
5880
}
5981
}
@@ -119,100 +141,225 @@ fn report_single_pattern(cx: &LateContext<'_>, ex: &Expr<'_>, arm: &Arm<'_>, exp
119141
span_lint_and_sugg(cx, lint, expr.span, msg, "try", sugg, app);
120142
}
121143

122-
/// Returns `true` if all of the types in the pattern are enums which we know
123-
/// won't be expanded in the future
124-
fn pat_in_candidate_enum<'a>(cx: &LateContext<'a>, ty: Ty<'a>, pat: &Pat<'_>) -> bool {
125-
let mut paths_and_types = Vec::new();
126-
collect_pat_paths(&mut paths_and_types, cx, pat, ty);
127-
paths_and_types.iter().all(|ty| in_candidate_enum(cx, *ty))
144+
struct PatVisitor<'tcx> {
145+
typeck: &'tcx TypeckResults<'tcx>,
146+
has_enum: bool,
147+
}
148+
impl<'tcx> Visitor<'tcx> for PatVisitor<'tcx> {
149+
type Result = ControlFlow<()>;
150+
fn visit_pat(&mut self, pat: &'tcx Pat<'_>) -> Self::Result {
151+
if matches!(pat.kind, PatKind::Binding(..)) {
152+
ControlFlow::Break(())
153+
} else {
154+
self.has_enum |= self.typeck.pat_ty(pat).ty_adt_def().map_or(false, AdtDef::is_enum);
155+
walk_pat(self, pat)
156+
}
157+
}
128158
}
129159

130-
/// Returns `true` if the given type is an enum we know won't be expanded in the future
131-
fn in_candidate_enum(cx: &LateContext<'_>, ty: Ty<'_>) -> bool {
132-
// list of candidate `Enum`s we know will never get any more members
133-
let candidates = [sym::Cow, sym::Option, sym::Result];
160+
/// The context needed to manipulate a `PatState`.
161+
struct PatCtxt<'tcx> {
162+
tcx: TyCtxt<'tcx>,
163+
param_env: ParamEnv<'tcx>,
164+
typeck: &'tcx TypeckResults<'tcx>,
165+
arena: DroplessArena,
166+
}
134167

135-
for candidate_ty in candidates {
136-
if is_type_diagnostic_item(cx, ty, candidate_ty) {
137-
return true;
168+
/// State for tracking whether a match can become non-exhaustive by adding a variant to a contained
169+
/// enum.
170+
///
171+
/// This treats certain std enums as if they will never be extended.
172+
enum PatState<'a> {
173+
/// Either a wild match or an uninteresting type. Uninteresting types include:
174+
/// * builtin types (e.g. `i32` or `!`)
175+
/// * A struct/tuple/array containing only uninteresting types.
176+
/// * A std enum containing only uninteresting types.
177+
Wild,
178+
/// A std enum we know won't be extended. Tracks the states of each variant separately.
179+
///
180+
/// This is not used for `Option` since it uses the current pattern to track it's state.
181+
StdEnum(&'a mut [PatState<'a>]),
182+
/// Either the initial state for a pattern or a non-std enum. There is currently no need to
183+
/// distinguish these cases.
184+
///
185+
/// For non-std enums there's no need to track the state of sub-patterns as the state of just
186+
/// this pattern on it's own is enough for linting. Consider two cases:
187+
/// * This enum has no wild match. This case alone is enough to determine we can lint.
188+
/// * This enum has a wild match and therefore all sub-patterns also have a wild match.
189+
/// In both cases the sub patterns are not needed to determine whether to lint.
190+
Other,
191+
}
192+
impl<'a> PatState<'a> {
193+
/// Adds a set of patterns as a product type to the current state. Returns whether or not the
194+
/// current state is a wild match after the merge.
195+
fn add_product_pat<'tcx>(
196+
&mut self,
197+
cx: &'a PatCtxt<'tcx>,
198+
pats: impl IntoIterator<Item = &'tcx Pat<'tcx>>,
199+
) -> bool {
200+
// Ideally this would actually keep track of the state separately for each pattern. Doing so would
201+
// require implementing something similar to exhaustiveness checking which is a significant increase
202+
// in complexity.
203+
//
204+
// For now treat this as a wild match only if all the sub-patterns are wild
205+
let is_wild = pats.into_iter().all(|p| {
206+
let mut state = Self::Other;
207+
state.add_pat(cx, p)
208+
});
209+
if is_wild {
210+
*self = Self::Wild;
138211
}
212+
is_wild
139213
}
140-
false
141-
}
142214

143-
/// Collects types from the given pattern
144-
fn collect_pat_paths<'a>(acc: &mut Vec<Ty<'a>>, cx: &LateContext<'a>, pat: &Pat<'_>, ty: Ty<'a>) {
145-
match pat.kind {
146-
PatKind::Tuple(inner, _) => inner.iter().for_each(|p| {
147-
let p_ty = cx.typeck_results().pat_ty(p);
148-
collect_pat_paths(acc, cx, p, p_ty);
149-
}),
150-
PatKind::TupleStruct(..) | PatKind::Binding(BindingMode::NONE, .., None) | PatKind::Path(_) => {
151-
acc.push(ty);
152-
},
153-
_ => {},
215+
/// Attempts to get the state for the enum variant, initializing the current state if necessary.
216+
fn get_enum_variant<'tcx>(
217+
&mut self,
218+
cx: &'a PatCtxt<'tcx>,
219+
adt: AdtDef<'tcx>,
220+
path: &'tcx QPath<'_>,
221+
hir_id: HirId,
222+
) -> Option<(&mut Self, &'tcx VariantDef)> {
223+
let states = match self {
224+
Self::Wild => return None,
225+
Self::Other => {
226+
*self = Self::StdEnum(cx.arena.alloc_from_iter((0..adt.variants().len()).map(|_| Self::Other)));
227+
let Self::StdEnum(x) = self else {
228+
unreachable!();
229+
};
230+
x
231+
},
232+
Self::StdEnum(x) => x,
233+
};
234+
let i = match cx.typeck.qpath_res(path, hir_id) {
235+
Res::Def(DefKind::Ctor(..), id) => adt.variant_index_with_ctor_id(id),
236+
Res::Def(DefKind::Variant, id) => adt.variant_index_with_id(id),
237+
_ => return None,
238+
};
239+
Some((&mut states[i.as_usize()], adt.variant(i)))
154240
}
155-
}
156241

157-
/// Returns true if the given arm of pattern matching contains wildcard patterns.
158-
fn contains_only_wilds(pat: &Pat<'_>) -> bool {
159-
match pat.kind {
160-
PatKind::Wild => true,
161-
PatKind::Tuple(inner, _) | PatKind::TupleStruct(_, inner, ..) => inner.iter().all(contains_only_wilds),
162-
_ => false,
242+
fn check_all_wild_enum(&mut self) -> bool {
243+
if let Self::StdEnum(states) = self
244+
&& states.iter().all(|s| matches!(s, Self::Wild))
245+
{
246+
*self = Self::Wild;
247+
true
248+
} else {
249+
false
250+
}
163251
}
164-
}
165252

166-
/// Returns true if the given patterns forms only exhaustive matches that don't contain enum
167-
/// patterns without a wildcard.
168-
fn form_exhaustive_matches<'a>(cx: &LateContext<'a>, ty: Ty<'a>, left: &Pat<'_>, right: &Pat<'_>) -> bool {
169-
match (&left.kind, &right.kind) {
170-
(PatKind::Wild, _) | (_, PatKind::Wild) => true,
171-
(PatKind::Tuple(left_in, left_pos), PatKind::Tuple(right_in, right_pos)) => {
172-
// We don't actually know the position and the presence of the `..` (dotdot) operator
173-
// in the arms, so we need to evaluate the correct offsets here in order to iterate in
174-
// both arms at the same time.
175-
let left_pos = left_pos.as_opt_usize();
176-
let right_pos = right_pos.as_opt_usize();
177-
let len = max(
178-
left_in.len() + usize::from(left_pos.is_some()),
179-
right_in.len() + usize::from(right_pos.is_some()),
180-
);
181-
let mut left_pos = left_pos.unwrap_or(usize::MAX);
182-
let mut right_pos = right_pos.unwrap_or(usize::MAX);
183-
let mut left_dot_space = 0;
184-
let mut right_dot_space = 0;
185-
for i in 0..len {
186-
let mut found_dotdot = false;
187-
if i == left_pos {
188-
left_dot_space += 1;
189-
if left_dot_space < len - left_in.len() {
190-
left_pos += 1;
191-
}
192-
found_dotdot = true;
193-
}
194-
if i == right_pos {
195-
right_dot_space += 1;
196-
if right_dot_space < len - right_in.len() {
197-
right_pos += 1;
198-
}
199-
found_dotdot = true;
200-
}
201-
if found_dotdot {
202-
continue;
253+
fn add_struct_pats<'tcx>(
254+
&mut self,
255+
cx: &'a PatCtxt<'tcx>,
256+
pat: &'tcx Pat<'tcx>,
257+
path: &'tcx QPath<'tcx>,
258+
single_pat: Option<&'tcx Pat<'tcx>>,
259+
pats: impl IntoIterator<Item = &'tcx Pat<'tcx>>,
260+
) -> bool {
261+
let ty::Adt(adt, _) = *cx.typeck.pat_ty(pat).kind() else {
262+
// Should never happen
263+
*self = Self::Wild;
264+
return true;
265+
};
266+
if adt.is_struct() {
267+
return if let Some(pat) = single_pat
268+
&& adt.non_enum_variant().fields.len() == 1
269+
{
270+
self.add_pat(cx, pat)
271+
} else {
272+
self.add_product_pat(cx, pats)
273+
};
274+
}
275+
match cx.tcx.get_diagnostic_name(adt.did()) {
276+
Some(sym::Option) => {
277+
if let Some(pat) = single_pat {
278+
self.add_pat(cx, pat)
279+
} else {
280+
*self = Self::Wild;
281+
true
203282
}
204-
if !contains_only_wilds(&left_in[i - left_dot_space])
205-
&& !contains_only_wilds(&right_in[i - right_dot_space])
283+
},
284+
Some(sym::Result | sym::Cow) => {
285+
let Some((state, variant)) = self.get_enum_variant(cx, adt, path, pat.hir_id) else {
286+
return matches!(self, Self::Wild);
287+
};
288+
let is_wild = if let Some(pat) = single_pat
289+
&& variant.fields.len() == 1
206290
{
207-
return false;
208-
}
209-
}
210-
true
211-
},
212-
(PatKind::TupleStruct(..), PatKind::Path(_)) => pat_in_candidate_enum(cx, ty, right),
213-
(PatKind::TupleStruct(..), PatKind::TupleStruct(_, inner, _)) => {
214-
pat_in_candidate_enum(cx, ty, right) && inner.iter().all(contains_only_wilds)
215-
},
216-
_ => false,
291+
state.add_pat(cx, pat)
292+
} else {
293+
state.add_product_pat(cx, pats)
294+
};
295+
is_wild && self.check_all_wild_enum()
296+
},
297+
_ => matches!(self, Self::Wild),
298+
}
299+
}
300+
301+
/// Adds the pattern into the current state. Returns whether or not the current state is a wild
302+
/// match after the merge.
303+
#[expect(clippy::similar_names, clippy::too_many_lines)]
304+
fn add_pat<'tcx>(&mut self, cx: &'a PatCtxt<'tcx>, pat: &'tcx Pat<'_>) -> bool {
305+
match pat.kind {
306+
PatKind::Path(_)
307+
if match *cx.typeck.pat_ty(pat).peel_refs().kind() {
308+
ty::Adt(adt, _) => adt.is_enum() || (adt.is_struct() && !adt.non_enum_variant().fields.is_empty()),
309+
ty::Tuple(tys) => !tys.is_empty(),
310+
ty::Array(_, len) => len.try_eval_target_usize(cx.tcx, cx.param_env) != Some(1),
311+
ty::Slice(..) => true,
312+
_ => false,
313+
} =>
314+
{
315+
matches!(self, Self::Wild)
316+
},
317+
318+
// Patterns for things which can only contain a single sub-pattern.
319+
PatKind::Binding(_, _, _, Some(pat)) | PatKind::Ref(pat, _) | PatKind::Box(pat) | PatKind::Deref(pat) => {
320+
self.add_pat(cx, pat)
321+
},
322+
PatKind::Tuple([sub_pat], pos)
323+
if pos.as_opt_usize().is_none() || cx.typeck.pat_ty(pat).tuple_fields().len() == 1 =>
324+
{
325+
self.add_pat(cx, sub_pat)
326+
},
327+
PatKind::Slice([sub_pat], _, []) | PatKind::Slice([], _, [sub_pat])
328+
if let ty::Array(_, len) = *cx.typeck.pat_ty(pat).kind()
329+
&& len.try_eval_target_usize(cx.tcx, cx.param_env) == Some(1) =>
330+
{
331+
self.add_pat(cx, sub_pat)
332+
},
333+
334+
PatKind::Or(pats) => pats.iter().any(|p| self.add_pat(cx, p)),
335+
PatKind::Tuple(pats, _) => self.add_product_pat(cx, pats),
336+
PatKind::Slice(head, _, tail) => self.add_product_pat(cx, head.iter().chain(tail)),
337+
338+
PatKind::TupleStruct(ref path, pats, _) => self.add_struct_pats(
339+
cx,
340+
pat,
341+
path,
342+
if let [pat] = pats { Some(pat) } else { None },
343+
pats.iter(),
344+
),
345+
PatKind::Struct(ref path, pats, _) => self.add_struct_pats(
346+
cx,
347+
pat,
348+
path,
349+
if let [pat] = pats { Some(pat.pat) } else { None },
350+
pats.iter().map(|p| p.pat),
351+
),
352+
353+
PatKind::Wild
354+
| PatKind::Binding(_, _, _, None)
355+
| PatKind::Lit(_)
356+
| PatKind::Range(..)
357+
| PatKind::Path(_)
358+
| PatKind::Never
359+
| PatKind::Err(_) => {
360+
*self = PatState::Wild;
361+
true
362+
},
363+
}
217364
}
218365
}

0 commit comments

Comments
 (0)