Skip to content

Commit 6d847c4

Browse files
committed
Auto merge of #6402 - camsteffen:collapsible-match, r=llogiq
Add Collapsible match lint changelog: Add collapsible_match lint Closes #1252 Closes #2521 This lint finds nested `match` or `if let` patterns that can be squashed together. It is designed to be very conservative to only find cases where merging the patterns would most likely reduce cognitive complexity. Example: ```rust match result { Ok(opt) => match opt { Some(x) => x, _ => return, } _ => return, } ``` to ```rust match result { Ok(Some(x)) => x, _ => return, } ``` These criteria must be met for the lint to fire: * The inner match has exactly 2 branches. * Both the outer and inner match have a "wild" branch like `_ => ..`. There is a special case for `None => ..` to also be considered "wild-like". * The contents of the wild branches are identical. * The binding which "links" the matches is never used elsewhere. Thanks to the hir, `if let`'s are easily included with this lint since they are desugared into equivalent `match`'es. I think this would fit into the style category, but I would also understand changing it to pedantic.
2 parents 68cf94f + 0e20788 commit 6d847c4

27 files changed

+829
-132
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1770,6 +1770,7 @@ Released 2018-09-13
17701770
[`cmp_owned`]: https://rust-lang.github.io/rust-clippy/master/index.html#cmp_owned
17711771
[`cognitive_complexity`]: https://rust-lang.github.io/rust-clippy/master/index.html#cognitive_complexity
17721772
[`collapsible_if`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if
1773+
[`collapsible_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_match
17731774
[`comparison_chain`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_chain
17741775
[`comparison_to_empty`]: https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
17751776
[`copy_iterator`]: https://rust-lang.github.io/rust-clippy/master/index.html#copy_iterator

clippy_lints/src/collapsible_match.rs

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use crate::utils::visitors::LocalUsedVisitor;
2+
use crate::utils::{span_lint_and_then, SpanlessEq};
3+
use if_chain::if_chain;
4+
use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res};
5+
use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, Pat, PatKind, QPath, StmtKind};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_middle::ty::{DefIdTree, TyCtxt};
8+
use rustc_session::{declare_lint_pass, declare_tool_lint};
9+
use rustc_span::{MultiSpan, Span};
10+
11+
declare_clippy_lint! {
12+
/// **What it does:** Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
13+
/// without adding any branches.
14+
///
15+
/// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only
16+
/// cases where merging would most likely make the code more readable.
17+
///
18+
/// **Why is this bad?** It is unnecessarily verbose and complex.
19+
///
20+
/// **Known problems:** None.
21+
///
22+
/// **Example:**
23+
///
24+
/// ```rust
25+
/// fn func(opt: Option<Result<u64, String>>) {
26+
/// let n = match opt {
27+
/// Some(n) => match n {
28+
/// Ok(n) => n,
29+
/// _ => return,
30+
/// }
31+
/// None => return,
32+
/// };
33+
/// }
34+
/// ```
35+
/// Use instead:
36+
/// ```rust
37+
/// fn func(opt: Option<Result<u64, String>>) {
38+
/// let n = match opt {
39+
/// Some(Ok(n)) => n,
40+
/// _ => return,
41+
/// };
42+
/// }
43+
/// ```
44+
pub COLLAPSIBLE_MATCH,
45+
style,
46+
"Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
47+
}
48+
49+
declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
50+
51+
impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch {
52+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
53+
if let ExprKind::Match(_expr, arms, _source) = expr.kind {
54+
if let Some(wild_arm) = arms.iter().rfind(|arm| arm_is_wild_like(arm, cx.tcx)) {
55+
for arm in arms {
56+
check_arm(arm, wild_arm, cx);
57+
}
58+
}
59+
}
60+
}
61+
}
62+
63+
fn check_arm(arm: &Arm<'_>, wild_outer_arm: &Arm<'_>, cx: &LateContext<'_>) {
64+
if_chain! {
65+
let expr = strip_singleton_blocks(arm.body);
66+
if let ExprKind::Match(expr_in, arms_inner, _) = expr.kind;
67+
// the outer arm pattern and the inner match
68+
if expr_in.span.ctxt() == arm.pat.span.ctxt();
69+
// there must be no more than two arms in the inner match for this lint
70+
if arms_inner.len() == 2;
71+
// no if guards on the inner match
72+
if arms_inner.iter().all(|arm| arm.guard.is_none());
73+
// match expression must be a local binding
74+
// match <local> { .. }
75+
if let ExprKind::Path(QPath::Resolved(None, path)) = expr_in.kind;
76+
if let Res::Local(binding_id) = path.res;
77+
// one of the branches must be "wild-like"
78+
if let Some(wild_inner_arm_idx) = arms_inner.iter().rposition(|arm_inner| arm_is_wild_like(arm_inner, cx.tcx));
79+
let (wild_inner_arm, non_wild_inner_arm) =
80+
(&arms_inner[wild_inner_arm_idx], &arms_inner[1 - wild_inner_arm_idx]);
81+
if !pat_contains_or(non_wild_inner_arm.pat);
82+
// the binding must come from the pattern of the containing match arm
83+
// ..<local>.. => match <local> { .. }
84+
if let Some(binding_span) = find_pat_binding(arm.pat, binding_id);
85+
// the "wild-like" branches must be equal
86+
if SpanlessEq::new(cx).eq_expr(wild_inner_arm.body, wild_outer_arm.body);
87+
// the binding must not be used in the if guard
88+
if !matches!(arm.guard, Some(Guard::If(guard)) if LocalUsedVisitor::new(binding_id).check_expr(guard));
89+
// ...or anywhere in the inner match
90+
if !arms_inner.iter().any(|arm| LocalUsedVisitor::new(binding_id).check_arm(arm));
91+
then {
92+
span_lint_and_then(
93+
cx,
94+
COLLAPSIBLE_MATCH,
95+
expr.span,
96+
"Unnecessary nested match",
97+
|diag| {
98+
let mut help_span = MultiSpan::from_spans(vec![binding_span, non_wild_inner_arm.pat.span]);
99+
help_span.push_span_label(binding_span, "Replace this binding".into());
100+
help_span.push_span_label(non_wild_inner_arm.pat.span, "with this pattern".into());
101+
diag.span_help(help_span, "The outer pattern can be modified to include the inner pattern.");
102+
},
103+
);
104+
}
105+
}
106+
}
107+
108+
fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> {
109+
while let ExprKind::Block(block, _) = expr.kind {
110+
match (block.stmts, block.expr) {
111+
([stmt], None) => match stmt.kind {
112+
StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e,
113+
_ => break,
114+
},
115+
([], Some(e)) => expr = e,
116+
_ => break,
117+
}
118+
}
119+
expr
120+
}
121+
122+
/// A "wild-like" pattern is wild ("_") or `None`.
123+
/// For this lint to apply, both the outer and inner match expressions
124+
/// must have "wild-like" branches that can be combined.
125+
fn arm_is_wild_like(arm: &Arm<'_>, tcx: TyCtxt<'_>) -> bool {
126+
if arm.guard.is_some() {
127+
return false;
128+
}
129+
match arm.pat.kind {
130+
PatKind::Binding(..) | PatKind::Wild => true,
131+
PatKind::Path(QPath::Resolved(None, path)) if is_none_ctor(path.res, tcx) => true,
132+
_ => false,
133+
}
134+
}
135+
136+
fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
137+
let mut span = None;
138+
pat.walk_short(|p| match &p.kind {
139+
// ignore OR patterns
140+
PatKind::Or(_) => false,
141+
PatKind::Binding(_bm, _, _ident, _) => {
142+
let found = p.hir_id == hir_id;
143+
if found {
144+
span = Some(p.span);
145+
}
146+
!found
147+
},
148+
_ => true,
149+
});
150+
span
151+
}
152+
153+
fn pat_contains_or(pat: &Pat<'_>) -> bool {
154+
let mut result = false;
155+
pat.walk(|p| {
156+
let is_or = matches!(p.kind, PatKind::Or(_));
157+
result |= is_or;
158+
!is_or
159+
});
160+
result
161+
}
162+
163+
fn is_none_ctor(res: Res, tcx: TyCtxt<'_>) -> bool {
164+
if let Some(none_id) = tcx.lang_items().option_none_variant() {
165+
if let Res::Def(DefKind::Ctor(CtorOf::Variant, CtorKind::Const), id) = res {
166+
if let Some(variant_id) = tcx.parent(id) {
167+
return variant_id == none_id;
168+
}
169+
}
170+
}
171+
false
172+
}

clippy_lints/src/default.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,7 @@ fn field_reassigned_by_stmt<'tcx>(this: &Stmt<'tcx>, binding_name: Symbol) -> Op
280280
// only take assignments to fields where the left-hand side field is a field of
281281
// the same binding as the previous statement
282282
if let ExprKind::Field(ref binding, field_ident) = assign_lhs.kind;
283-
if let ExprKind::Path(ref qpath) = binding.kind;
284-
if let QPath::Resolved(_, path) = qpath;
283+
if let ExprKind::Path(QPath::Resolved(_, path)) = binding.kind;
285284
if let Some(second_binding_name) = path.segments.last();
286285
if second_binding_name.ident.name == binding_name;
287286
then {

clippy_lints/src/if_let_some_result.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ declare_lint_pass!(OkIfLet => [IF_LET_SOME_RESULT]);
4141
impl<'tcx> LateLintPass<'tcx> for OkIfLet {
4242
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
4343
if_chain! { //begin checking variables
44-
if let ExprKind::Match(ref op, ref body, source) = expr.kind; //test if expr is a match
45-
if let MatchSource::IfLetDesugar { .. } = source; //test if it is an If Let
44+
if let ExprKind::Match(ref op, ref body, MatchSource::IfLetDesugar { .. }) = expr.kind; //test if expr is if let
4645
if let ExprKind::MethodCall(_, ok_span, ref result_types, _) = op.kind; //check is expr.ok() has type Result<T,E>.ok(, _)
4746
if let PatKind::TupleStruct(QPath::Resolved(_, ref x), ref y, _) = body[0].pat.kind; //get operation
4847
if method_chain_args(op, &["ok"]).is_some(); //test to see if using ok() methoduse std::marker::Sized;

clippy_lints/src/implicit_return.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ fn expr_match(cx: &LateContext<'_>, expr: &Expr<'_>) {
6868
if_chain! {
6969
if let StmtKind::Semi(expr, ..) = &stmt.kind;
7070
// make sure it's a break, otherwise we want to skip
71-
if let ExprKind::Break(.., break_expr) = &expr.kind;
72-
if let Some(break_expr) = break_expr;
71+
if let ExprKind::Break(.., Some(break_expr)) = &expr.kind;
7372
then {
7473
lint(cx, expr.span, break_expr.span, LINT_BREAK);
7574
}

clippy_lints/src/implicit_saturating_sub.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingSub {
5959
if let Some(target) = subtracts_one(cx, e);
6060

6161
// Extracting out the variable name
62-
if let ExprKind::Path(ref assign_path) = target.kind;
63-
if let QPath::Resolved(_, ref ares_path) = assign_path;
62+
if let ExprKind::Path(QPath::Resolved(_, ref ares_path)) = target.kind;
6463

6564
then {
6665
// Handle symmetric conditions in the if statement

clippy_lints/src/large_const_arrays.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ impl<'tcx> LateLintPass<'tcx> for LargeConstArrays {
5252
if let ItemKind::Const(hir_ty, _) = &item.kind;
5353
let ty = hir_ty_to_ty(cx.tcx, hir_ty);
5454
if let ty::Array(element_type, cst) = ty.kind();
55-
if let ConstKind::Value(val) = cst.val;
56-
if let ConstValue::Scalar(element_count) = val;
55+
if let ConstKind::Value(ConstValue::Scalar(element_count)) = cst.val;
5756
if let Ok(element_count) = element_count.to_machine_usize(&cx.tcx);
5857
if let Ok(element_size) = cx.layout_of(element_type).map(|l| l.size.bytes());
5958
if self.maximum_allowed_size < element_count * element_size;

clippy_lints/src/large_stack_arrays.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ impl<'tcx> LateLintPass<'tcx> for LargeStackArrays {
4343
if_chain! {
4444
if let ExprKind::Repeat(_, _) = expr.kind;
4545
if let ty::Array(element_type, cst) = cx.typeck_results().expr_ty(expr).kind();
46-
if let ConstKind::Value(val) = cst.val;
47-
if let ConstValue::Scalar(element_count) = val;
46+
if let ConstKind::Value(ConstValue::Scalar(element_count)) = cst.val;
4847
if let Ok(element_count) = element_count.to_machine_usize(&cx.tcx);
4948
if let Ok(element_size) = cx.layout_of(element_type).map(|l| l.size.bytes());
5049
if self.maximum_allowed_size < element_count * element_size;

clippy_lints/src/let_if_seq.rs

+6-47
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
use crate::utils::visitors::LocalUsedVisitor;
12
use crate::utils::{higher, qpath_res, snippet, span_lint_and_then};
23
use if_chain::if_chain;
34
use rustc_errors::Applicability;
45
use rustc_hir as hir;
56
use rustc_hir::def::Res;
6-
use rustc_hir::intravisit;
77
use rustc_hir::BindingAnnotation;
88
use rustc_lint::{LateContext, LateLintPass};
9-
use rustc_middle::hir::map::Map;
109
use rustc_session::{declare_lint_pass, declare_tool_lint};
1110

1211
declare_clippy_lint! {
@@ -66,10 +65,10 @@ impl<'tcx> LateLintPass<'tcx> for LetIfSeq {
6665
if let hir::PatKind::Binding(mode, canonical_id, ident, None) = local.pat.kind;
6766
if let hir::StmtKind::Expr(ref if_) = expr.kind;
6867
if let Some((ref cond, ref then, ref else_)) = higher::if_block(&if_);
69-
if !used_in_expr(cx, canonical_id, cond);
68+
if !LocalUsedVisitor::new(canonical_id).check_expr(cond);
7069
if let hir::ExprKind::Block(ref then, _) = then.kind;
7170
if let Some(value) = check_assign(cx, canonical_id, &*then);
72-
if !used_in_expr(cx, canonical_id, value);
71+
if !LocalUsedVisitor::new(canonical_id).check_expr(value);
7372
then {
7473
let span = stmt.span.to(if_.span);
7574

@@ -136,32 +135,6 @@ impl<'tcx> LateLintPass<'tcx> for LetIfSeq {
136135
}
137136
}
138137

139-
struct UsedVisitor<'a, 'tcx> {
140-
cx: &'a LateContext<'tcx>,
141-
id: hir::HirId,
142-
used: bool,
143-
}
144-
145-
impl<'a, 'tcx> intravisit::Visitor<'tcx> for UsedVisitor<'a, 'tcx> {
146-
type Map = Map<'tcx>;
147-
148-
fn visit_expr(&mut self, expr: &'tcx hir::Expr<'_>) {
149-
if_chain! {
150-
if let hir::ExprKind::Path(ref qpath) = expr.kind;
151-
if let Res::Local(local_id) = qpath_res(self.cx, qpath, expr.hir_id);
152-
if self.id == local_id;
153-
then {
154-
self.used = true;
155-
return;
156-
}
157-
}
158-
intravisit::walk_expr(self, expr);
159-
}
160-
fn nested_visit_map(&mut self) -> intravisit::NestedVisitorMap<Self::Map> {
161-
intravisit::NestedVisitorMap::None
162-
}
163-
}
164-
165138
fn check_assign<'tcx>(
166139
cx: &LateContext<'tcx>,
167140
decl: hir::HirId,
@@ -176,18 +149,10 @@ fn check_assign<'tcx>(
176149
if let Res::Local(local_id) = qpath_res(cx, qpath, var.hir_id);
177150
if decl == local_id;
178151
then {
179-
let mut v = UsedVisitor {
180-
cx,
181-
id: decl,
182-
used: false,
183-
};
184-
185-
for s in block.stmts.iter().take(block.stmts.len()-1) {
186-
intravisit::walk_stmt(&mut v, s);
152+
let mut v = LocalUsedVisitor::new(decl);
187153

188-
if v.used {
189-
return None;
190-
}
154+
if block.stmts.iter().take(block.stmts.len()-1).any(|stmt| v.check_stmt(stmt)) {
155+
return None;
191156
}
192157

193158
return Some(value);
@@ -196,9 +161,3 @@ fn check_assign<'tcx>(
196161

197162
None
198163
}
199-
200-
fn used_in_expr<'tcx>(cx: &LateContext<'tcx>, id: hir::HirId, expr: &'tcx hir::Expr<'_>) -> bool {
201-
let mut v = UsedVisitor { cx, id, used: false };
202-
intravisit::walk_expr(&mut v, expr);
203-
v.used
204-
}

clippy_lints/src/lib.rs

+5
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ mod cargo_common_metadata;
172172
mod checked_conversions;
173173
mod cognitive_complexity;
174174
mod collapsible_if;
175+
mod collapsible_match;
175176
mod comparison_chain;
176177
mod copies;
177178
mod copy_iterator;
@@ -531,6 +532,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
531532
&checked_conversions::CHECKED_CONVERSIONS,
532533
&cognitive_complexity::COGNITIVE_COMPLEXITY,
533534
&collapsible_if::COLLAPSIBLE_IF,
535+
&collapsible_match::COLLAPSIBLE_MATCH,
534536
&comparison_chain::COMPARISON_CHAIN,
535537
&copies::IFS_SAME_COND,
536538
&copies::IF_SAME_THEN_ELSE,
@@ -960,6 +962,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
960962
store.register_late_pass(|| box len_zero::LenZero);
961963
store.register_late_pass(|| box attrs::Attributes);
962964
store.register_late_pass(|| box blocks_in_if_conditions::BlocksInIfConditions);
965+
store.register_late_pass(|| box collapsible_match::CollapsibleMatch);
963966
store.register_late_pass(|| box unicode::Unicode);
964967
store.register_late_pass(|| box unit_return_expecting_ord::UnitReturnExpectingOrd);
965968
store.register_late_pass(|| box strings::StringAdd);
@@ -1351,6 +1354,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13511354
LintId::of(&booleans::NONMINIMAL_BOOL),
13521355
LintId::of(&bytecount::NAIVE_BYTECOUNT),
13531356
LintId::of(&collapsible_if::COLLAPSIBLE_IF),
1357+
LintId::of(&collapsible_match::COLLAPSIBLE_MATCH),
13541358
LintId::of(&comparison_chain::COMPARISON_CHAIN),
13551359
LintId::of(&copies::IFS_SAME_COND),
13561360
LintId::of(&copies::IF_SAME_THEN_ELSE),
@@ -1617,6 +1621,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16171621
LintId::of(&blacklisted_name::BLACKLISTED_NAME),
16181622
LintId::of(&blocks_in_if_conditions::BLOCKS_IN_IF_CONDITIONS),
16191623
LintId::of(&collapsible_if::COLLAPSIBLE_IF),
1624+
LintId::of(&collapsible_match::COLLAPSIBLE_MATCH),
16201625
LintId::of(&comparison_chain::COMPARISON_CHAIN),
16211626
LintId::of(&default::FIELD_REASSIGN_WITH_DEFAULT),
16221627
LintId::of(&doc::MISSING_SAFETY_DOC),

0 commit comments

Comments
 (0)