Skip to content

Commit 00e5e1b

Browse files
authored
refactor(match_like_matches_macro): disentangle the if-let and match cases (#15854)
As discussed in [#clippy > `match_like_matches_macro` does the work of`match_same_arms`](https://rust-lang.zulipchat.com/#narrow/channel/257328-clippy/topic/.60match_like_matches_macro.60.20does.20the.20work.20of.60match_same_arms.60/with/544003831) Sorry for the large number of commits -- I could've gotten away just three ("clean-up", and one for each inlining of `find_matches_sugg` and subsequent simplifications), but I found the diff of that quite difficult to understand. changelog: none
2 parents 8697533 + 0535908 commit 00e5e1b

File tree

4 files changed

+140
-94
lines changed

4 files changed

+140
-94
lines changed

clippy_lints/src/matches/match_like_matches.rs

Lines changed: 122 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1+
//! Lint a `match` or `if let .. { .. } else { .. }` expr that could be replaced by `matches!`
2+
13
use super::REDUNDANT_PATTERN_MATCHING;
24
use clippy_utils::diagnostics::span_lint_and_sugg;
35
use clippy_utils::source::snippet_with_applicability;
46
use clippy_utils::{is_lint_allowed, is_wild, span_contains_comment};
57
use rustc_ast::LitKind;
68
use rustc_errors::Applicability;
7-
use rustc_hir::{Arm, Attribute, BorrowKind, Expr, ExprKind, Pat, PatKind, QPath};
9+
use rustc_hir::{Arm, BorrowKind, Expr, ExprKind, Pat, PatKind, QPath};
810
use rustc_lint::{LateContext, LintContext};
911
use rustc_middle::ty;
1012
use rustc_span::source_map::Spanned;
1113

1214
use super::MATCH_LIKE_MATCHES_MACRO;
1315

14-
/// Lint a `match` or `if let .. { .. } else { .. }` expr that could be replaced by `matches!`
1516
pub(crate) fn check_if_let<'tcx>(
1617
cx: &LateContext<'tcx>,
1718
expr: &'tcx Expr<'_>,
@@ -20,16 +21,42 @@ pub(crate) fn check_if_let<'tcx>(
2021
then_expr: &'tcx Expr<'_>,
2122
else_expr: &'tcx Expr<'_>,
2223
) {
23-
find_matches_sugg(
24-
cx,
25-
let_expr,
26-
IntoIterator::into_iter([
27-
(&[][..], Some(let_pat), then_expr, None),
28-
(&[][..], None, else_expr, None),
29-
]),
30-
expr,
31-
true,
32-
);
24+
if !span_contains_comment(cx.sess().source_map(), expr.span)
25+
&& cx.typeck_results().expr_ty(expr).is_bool()
26+
&& let Some(b0) = find_bool_lit(then_expr)
27+
&& let Some(b1) = find_bool_lit(else_expr)
28+
&& b0 != b1
29+
{
30+
if !is_lint_allowed(cx, REDUNDANT_PATTERN_MATCHING, let_pat.hir_id) && is_some_wild(let_pat.kind) {
31+
return;
32+
}
33+
34+
// The suggestion may be incorrect, because some arms can have `cfg` attributes
35+
// evaluated into `false` and so such arms will be stripped before.
36+
let mut applicability = Applicability::MaybeIncorrect;
37+
let pat = snippet_with_applicability(cx, let_pat.span, "..", &mut applicability);
38+
39+
// strip potential borrows (#6503), but only if the type is a reference
40+
let mut ex_new = let_expr;
41+
if let ExprKind::AddrOf(BorrowKind::Ref, .., ex_inner) = let_expr.kind
42+
&& let ty::Ref(..) = cx.typeck_results().expr_ty(ex_inner).kind()
43+
{
44+
ex_new = ex_inner;
45+
}
46+
span_lint_and_sugg(
47+
cx,
48+
MATCH_LIKE_MATCHES_MACRO,
49+
expr.span,
50+
"if let .. else expression looks like `matches!` macro",
51+
"try",
52+
format!(
53+
"{}matches!({}, {pat})",
54+
if b0 { "" } else { "!" },
55+
snippet_with_applicability(cx, ex_new.span, "..", &mut applicability),
56+
),
57+
applicability,
58+
);
59+
}
3360
}
3461

3562
pub(super) fn check_match<'tcx>(
@@ -38,55 +65,80 @@ pub(super) fn check_match<'tcx>(
3865
scrutinee: &'tcx Expr<'_>,
3966
arms: &'tcx [Arm<'tcx>],
4067
) -> bool {
41-
find_matches_sugg(
42-
cx,
43-
scrutinee,
44-
arms.iter()
45-
.map(|arm| (cx.tcx.hir_attrs(arm.hir_id), Some(arm.pat), arm.body, arm.guard)),
46-
e,
47-
false,
48-
)
49-
}
50-
51-
/// Lint a `match` or `if let` for replacement by `matches!`
52-
fn find_matches_sugg<'a, 'b, I>(
53-
cx: &LateContext<'_>,
54-
ex: &Expr<'_>,
55-
mut iter: I,
56-
expr: &Expr<'_>,
57-
is_if_let: bool,
58-
) -> bool
59-
where
60-
'b: 'a,
61-
I: Clone
62-
+ DoubleEndedIterator
63-
+ ExactSizeIterator
64-
+ Iterator<Item = (&'a [Attribute], Option<&'a Pat<'b>>, &'a Expr<'b>, Option<&'a Expr<'b>>)>,
65-
{
66-
if !span_contains_comment(cx.sess().source_map(), expr.span)
67-
&& iter.len() >= 2
68-
&& cx.typeck_results().expr_ty(expr).is_bool()
69-
&& let Some((_, last_pat_opt, last_expr, _)) = iter.next_back()
70-
&& let iter_without_last = iter.clone()
71-
&& let Some((first_attrs, _, first_expr, first_guard)) = iter.next()
72-
&& let Some(b0) = find_bool_lit(&first_expr.kind)
73-
&& let Some(b1) = find_bool_lit(&last_expr.kind)
68+
if let Some((last_arm, arms_without_last)) = arms.split_last()
69+
&& let Some((first_arm, middle_arms)) = arms_without_last.split_first()
70+
&& !span_contains_comment(cx.sess().source_map(), e.span)
71+
&& cx.typeck_results().expr_ty(e).is_bool()
72+
&& let Some(b0) = find_bool_lit(first_arm.body)
73+
&& let Some(b1) = find_bool_lit(last_arm.body)
7474
&& b0 != b1
75-
&& (first_guard.is_none() || iter.len() == 0)
76-
&& first_attrs.is_empty()
77-
&& iter.all(|arm| find_bool_lit(&arm.2.kind).is_some_and(|b| b == b0) && arm.3.is_none() && arm.0.is_empty())
75+
// We handle two cases:
76+
&& (
77+
// - There are no middle arms, i.e., 2 arms in total
78+
//
79+
// In that case, the first arm may or may not have a guard, because this:
80+
// ```rs
81+
// match e {
82+
// Either::Left $(if $guard)|+ => true, // or `false`, but then we'll need `!matches!(..)`
83+
// _ => false,
84+
// }
85+
// ```
86+
// can always become this:
87+
// ```rs
88+
// matches!(e, Either::Left $(if $guard)|+)
89+
// ```
90+
middle_arms.is_empty()
91+
92+
// - (added in #6216) There are middle arms
93+
//
94+
// In that case, neither they nor the first arm may have guards
95+
// -- otherwise, they couldn't be combined into an or-pattern in `matches!`
96+
//
97+
// This:
98+
// ```rs
99+
// match e {
100+
// Either3::First => true,
101+
// Either3::Second => true,
102+
// _ /* matches `Either3::Third` */ => false,
103+
// }
104+
// ```
105+
// can become this:
106+
// ```rs
107+
// matches!(e, Either3::First | Either3::Second)
108+
// ```
109+
//
110+
// But this:
111+
// ```rs
112+
// match e {
113+
// Either3::First if X => true,
114+
// Either3::Second => true,
115+
// _ => false,
116+
// }
117+
// ```
118+
// cannot be transformed.
119+
//
120+
// We set an additional constraint of all of them needing to return the same bool,
121+
// so we don't lint things like:
122+
// ```rs
123+
// match e {
124+
// Either3::First => true,
125+
// Either3::Second => false,
126+
// _ => false,
127+
// }
128+
// ```
129+
// This is not *strictly* necessary, but it simplifies the logic a bit
130+
|| arms_without_last.iter().all(|arm| {
131+
cx.tcx.hir_attrs(arm.hir_id).is_empty() && arm.guard.is_none() && find_bool_lit(arm.body) == Some(b0)
132+
})
133+
)
78134
{
79-
if let Some(last_pat) = last_pat_opt
80-
&& !is_wild(last_pat)
81-
{
135+
if !is_wild(last_arm.pat) {
82136
return false;
83137
}
84138

85-
for arm in iter_without_last.clone() {
86-
if let Some(pat) = arm.1
87-
&& !is_lint_allowed(cx, REDUNDANT_PATTERN_MATCHING, pat.hir_id)
88-
&& is_some(pat.kind)
89-
{
139+
for arm in arms_without_last {
140+
let pat = arm.pat;
141+
if !is_lint_allowed(cx, REDUNDANT_PATTERN_MATCHING, pat.hir_id) && is_some_wild(pat.kind) {
90142
return false;
91143
}
92144
}
@@ -96,14 +148,12 @@ where
96148
let mut applicability = Applicability::MaybeIncorrect;
97149
let pat = {
98150
use itertools::Itertools as _;
99-
iter_without_last
100-
.filter_map(|arm| {
101-
let pat_span = arm.1?.span;
102-
Some(snippet_with_applicability(cx, pat_span, "..", &mut applicability))
103-
})
151+
arms_without_last
152+
.iter()
153+
.map(|arm| snippet_with_applicability(cx, arm.pat.span, "..", &mut applicability))
104154
.join(" | ")
105155
};
106-
let pat_and_guard = if let Some(g) = first_guard {
156+
let pat_and_guard = if let Some(g) = first_arm.guard {
107157
format!(
108158
"{pat} if {}",
109159
snippet_with_applicability(cx, g.span, "..", &mut applicability)
@@ -113,20 +163,17 @@ where
113163
};
114164

115165
// strip potential borrows (#6503), but only if the type is a reference
116-
let mut ex_new = ex;
117-
if let ExprKind::AddrOf(BorrowKind::Ref, .., ex_inner) = ex.kind
166+
let mut ex_new = scrutinee;
167+
if let ExprKind::AddrOf(BorrowKind::Ref, .., ex_inner) = scrutinee.kind
118168
&& let ty::Ref(..) = cx.typeck_results().expr_ty(ex_inner).kind()
119169
{
120170
ex_new = ex_inner;
121171
}
122172
span_lint_and_sugg(
123173
cx,
124174
MATCH_LIKE_MATCHES_MACRO,
125-
expr.span,
126-
format!(
127-
"{} expression looks like `matches!` macro",
128-
if is_if_let { "if let .. else" } else { "match" }
129-
),
175+
e.span,
176+
"match expression looks like `matches!` macro",
130177
"try",
131178
format!(
132179
"{}matches!({}, {pat_and_guard})",
@@ -142,11 +189,11 @@ where
142189
}
143190

144191
/// Extract a `bool` or `{ bool }`
145-
fn find_bool_lit(ex: &ExprKind<'_>) -> Option<bool> {
146-
match ex {
192+
fn find_bool_lit(ex: &Expr<'_>) -> Option<bool> {
193+
match ex.kind {
147194
ExprKind::Lit(Spanned {
148195
node: LitKind::Bool(b), ..
149-
}) => Some(*b),
196+
}) => Some(b),
150197
ExprKind::Block(
151198
rustc_hir::Block {
152199
stmts: [],
@@ -168,8 +215,9 @@ fn find_bool_lit(ex: &ExprKind<'_>) -> Option<bool> {
168215
}
169216
}
170217

171-
fn is_some(path_kind: PatKind<'_>) -> bool {
172-
match path_kind {
218+
/// Checks whether a pattern is `Some(_)`
219+
fn is_some_wild(pat_kind: PatKind<'_>) -> bool {
220+
match pat_kind {
173221
PatKind::TupleStruct(QPath::Resolved(_, path), [first, ..], _) if is_wild(first) => {
174222
let name = path.segments[0].ident;
175223
name.name == rustc_span::sym::Some

tests/ui/match_expr_like_matches_macro.fixed renamed to tests/ui/match_like_matches_macro.fixed

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#![warn(clippy::match_like_matches_macro)]
22
#![allow(
33
unreachable_patterns,
4-
dead_code,
54
clippy::equatable_if_let,
65
clippy::needless_borrowed_reference,
76
clippy::redundant_guards
@@ -14,11 +13,11 @@ fn main() {
1413
let _y = matches!(x, Some(0));
1514
//~^^^^ match_like_matches_macro
1615

17-
// Lint
16+
// No lint: covered by `redundant_pattern_matching`
1817
let _w = x.is_some();
1918
//~^^^^ redundant_pattern_matching
2019

21-
// Turn into is_none
20+
// No lint: covered by `redundant_pattern_matching`
2221
let _z = x.is_none();
2322
//~^^^^ redundant_pattern_matching
2423

tests/ui/match_expr_like_matches_macro.rs renamed to tests/ui/match_like_matches_macro.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#![warn(clippy::match_like_matches_macro)]
22
#![allow(
33
unreachable_patterns,
4-
dead_code,
54
clippy::equatable_if_let,
65
clippy::needless_borrowed_reference,
76
clippy::redundant_guards
@@ -17,14 +16,14 @@ fn main() {
1716
};
1817
//~^^^^ match_like_matches_macro
1918

20-
// Lint
19+
// No lint: covered by `redundant_pattern_matching`
2120
let _w = match x {
2221
Some(_) => true,
2322
_ => false,
2423
};
2524
//~^^^^ redundant_pattern_matching
2625

27-
// Turn into is_none
26+
// No lint: covered by `redundant_pattern_matching`
2827
let _z = match x {
2928
Some(_) => false,
3029
None => true,

0 commit comments

Comments
 (0)