2021-03-25 18:29:11 +00:00
|
|
|
use clippy_utils::diagnostics::span_lint_and_then;
|
2021-06-07 00:01:14 +00:00
|
|
|
use clippy_utils::visitors::is_local_used;
|
2021-08-19 18:31:25 +00:00
|
|
|
use clippy_utils::{higher, is_lang_ctor, is_unit_expr, path_to_local, peel_ref_operators, SpanlessEq};
|
2020-12-06 14:01:03 +00:00
|
|
|
use if_chain::if_chain;
|
2021-04-22 09:31:13 +00:00
|
|
|
use rustc_hir::LangItem::OptionNone;
|
2021-08-19 18:31:25 +00:00
|
|
|
use rustc_hir::{Arm, Expr, ExprKind, Guard, HirId, MatchSource, Pat, PatKind, StmtKind};
|
2020-12-06 14:01:03 +00:00
|
|
|
use rustc_lint::{LateContext, LateLintPass};
|
|
|
|
use rustc_session::{declare_lint_pass, declare_tool_lint};
|
|
|
|
use rustc_span::{MultiSpan, Span};
|
|
|
|
|
|
|
|
declare_clippy_lint! {
|
2021-07-29 10:16:06 +00:00
|
|
|
/// ### What it does
|
|
|
|
/// Finds nested `match` or `if let` expressions where the patterns may be "collapsed" together
|
2020-12-06 14:01:03 +00:00
|
|
|
/// without adding any branches.
|
|
|
|
///
|
|
|
|
/// Note that this lint is not intended to find _all_ cases where nested match patterns can be merged, but only
|
|
|
|
/// cases where merging would most likely make the code more readable.
|
|
|
|
///
|
2021-07-29 10:16:06 +00:00
|
|
|
/// ### Why is this bad?
|
|
|
|
/// It is unnecessarily verbose and complex.
|
2020-12-06 14:01:03 +00:00
|
|
|
///
|
2021-07-29 10:16:06 +00:00
|
|
|
/// ### Example
|
2020-12-06 14:01:03 +00:00
|
|
|
/// ```rust
|
|
|
|
/// fn func(opt: Option<Result<u64, String>>) {
|
|
|
|
/// let n = match opt {
|
|
|
|
/// Some(n) => match n {
|
|
|
|
/// Ok(n) => n,
|
|
|
|
/// _ => return,
|
|
|
|
/// }
|
|
|
|
/// None => return,
|
|
|
|
/// };
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
/// Use instead:
|
|
|
|
/// ```rust
|
|
|
|
/// fn func(opt: Option<Result<u64, String>>) {
|
|
|
|
/// let n = match opt {
|
|
|
|
/// Some(Ok(n)) => n,
|
|
|
|
/// _ => return,
|
|
|
|
/// };
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
pub COLLAPSIBLE_MATCH,
|
|
|
|
style,
|
|
|
|
"Nested `match` or `if let` expressions where the patterns may be \"collapsed\" together."
|
|
|
|
}
|
|
|
|
|
|
|
|
declare_lint_pass!(CollapsibleMatch => [COLLAPSIBLE_MATCH]);
|
|
|
|
|
|
|
|
impl<'tcx> LateLintPass<'tcx> for CollapsibleMatch {
|
|
|
|
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'tcx>) {
|
2021-08-19 18:31:25 +00:00
|
|
|
match IfLetOrMatch::parse(cx, expr) {
|
|
|
|
Some(IfLetOrMatch::Match(_, arms, _)) => {
|
|
|
|
if let Some(els_arm) = arms.iter().rfind(|arm| arm_is_wild_like(cx, arm)) {
|
|
|
|
for arm in arms {
|
|
|
|
check_arm(cx, true, arm.pat, arm.body, arm.guard.as_ref(), Some(els_arm.body));
|
|
|
|
}
|
2020-12-06 14:01:03 +00:00
|
|
|
}
|
2021-09-02 11:38:17 +00:00
|
|
|
},
|
2021-08-19 18:31:25 +00:00
|
|
|
Some(IfLetOrMatch::IfLet(_, pat, body, els)) => {
|
|
|
|
check_arm(cx, false, pat, body, None, els);
|
2021-09-02 11:38:17 +00:00
|
|
|
},
|
|
|
|
None => {},
|
2020-12-06 14:01:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-08 14:49:13 +00:00
|
|
|
fn check_arm<'tcx>(
|
|
|
|
cx: &LateContext<'tcx>,
|
2021-08-19 18:31:25 +00:00
|
|
|
outer_is_match: bool,
|
2021-08-08 14:49:13 +00:00
|
|
|
outer_pat: &'tcx Pat<'tcx>,
|
2021-08-19 18:31:25 +00:00
|
|
|
outer_then_body: &'tcx Expr<'tcx>,
|
|
|
|
outer_guard: Option<&'tcx Guard<'tcx>>,
|
2021-09-02 11:38:17 +00:00
|
|
|
outer_else_body: Option<&'tcx Expr<'tcx>>,
|
2021-08-08 14:49:13 +00:00
|
|
|
) {
|
2021-08-19 18:31:25 +00:00
|
|
|
let inner_expr = strip_singleton_blocks(outer_then_body);
|
2020-12-06 14:01:03 +00:00
|
|
|
if_chain! {
|
2021-08-19 18:31:25 +00:00
|
|
|
if let Some(inner) = IfLetOrMatch::parse(cx, inner_expr);
|
|
|
|
if let Some((inner_scrutinee, inner_then_pat, inner_else_body)) = match inner {
|
|
|
|
IfLetOrMatch::IfLet(scrutinee, pat, _, els) => Some((scrutinee, pat, els)),
|
|
|
|
IfLetOrMatch::Match(scrutinee, arms, ..) => if_chain! {
|
|
|
|
// if there are more than two arms, collapsing would be non-trivial
|
|
|
|
if arms.len() == 2 && arms.iter().all(|a| a.guard.is_none());
|
|
|
|
// one of the arms must be "wild-like"
|
|
|
|
if let Some(wild_idx) = arms.iter().rposition(|a| arm_is_wild_like(cx, a));
|
|
|
|
then {
|
|
|
|
let (then, els) = (&arms[1 - wild_idx], &arms[wild_idx]);
|
|
|
|
Some((scrutinee, then.pat, Some(els.body)))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
if outer_pat.span.ctxt() == inner_scrutinee.span.ctxt();
|
2020-12-06 14:01:03 +00:00
|
|
|
// match expression must be a local binding
|
|
|
|
// match <local> { .. }
|
2021-08-19 18:31:25 +00:00
|
|
|
if let Some(binding_id) = path_to_local(peel_ref_operators(cx, inner_scrutinee));
|
|
|
|
if !pat_contains_or(inner_then_pat);
|
2020-12-06 14:01:03 +00:00
|
|
|
// the binding must come from the pattern of the containing match arm
|
|
|
|
// ..<local>.. => match <local> { .. }
|
2021-08-08 14:49:13 +00:00
|
|
|
if let Some(binding_span) = find_pat_binding(outer_pat, binding_id);
|
2021-08-19 18:31:25 +00:00
|
|
|
// the "else" branches must be equal
|
|
|
|
if match (outer_else_body, inner_else_body) {
|
|
|
|
(None, None) => true,
|
|
|
|
(None, Some(e)) | (Some(e), None) => is_unit_expr(e),
|
|
|
|
(Some(a), Some(b)) => SpanlessEq::new(cx).eq_expr(a, b),
|
|
|
|
};
|
2020-12-06 14:01:03 +00:00
|
|
|
// the binding must not be used in the if guard
|
2021-09-02 11:38:17 +00:00
|
|
|
if outer_guard.map_or(true, |(Guard::If(e) | Guard::IfLet(_, e))| !is_local_used(cx, *e, binding_id)); // ...or anywhere in the inner expression
|
2021-08-19 18:31:25 +00:00
|
|
|
if match inner {
|
|
|
|
IfLetOrMatch::IfLet(_, _, body, els) => {
|
2021-09-02 11:38:17 +00:00
|
|
|
!is_local_used(cx, body, binding_id) && els.map_or(true, |e| !is_local_used(cx, e, binding_id))
|
2021-08-19 18:31:25 +00:00
|
|
|
},
|
2021-09-02 11:38:17 +00:00
|
|
|
IfLetOrMatch::Match(_, arms, ..) => !arms.iter().any(|arm| is_local_used(cx, arm, binding_id)),
|
2021-01-30 17:06:34 +00:00
|
|
|
};
|
2020-12-06 14:01:03 +00:00
|
|
|
then {
|
2021-08-19 18:31:25 +00:00
|
|
|
let msg = format!(
|
|
|
|
"this `{}` can be collapsed into the outer `{}`",
|
|
|
|
if matches!(inner, IfLetOrMatch::Match(..)) { "match" } else { "if let" },
|
|
|
|
if outer_is_match { "match" } else { "if let" },
|
2020-12-06 14:01:03 +00:00
|
|
|
);
|
2021-08-08 14:49:13 +00:00
|
|
|
span_lint_and_then(
|
|
|
|
cx,
|
|
|
|
COLLAPSIBLE_MATCH,
|
2021-08-19 18:31:25 +00:00
|
|
|
inner_expr.span,
|
|
|
|
&msg,
|
2021-08-08 14:49:13 +00:00
|
|
|
|diag| {
|
2021-08-19 18:31:25 +00:00
|
|
|
let mut help_span = MultiSpan::from_spans(vec![binding_span, inner_then_pat.span]);
|
2021-08-08 14:49:13 +00:00
|
|
|
help_span.push_span_label(binding_span, "replace this binding".into());
|
2021-08-19 18:31:25 +00:00
|
|
|
help_span.push_span_label(inner_then_pat.span, "with this pattern".into());
|
2021-08-08 14:49:13 +00:00
|
|
|
diag.span_help(help_span, "the outer pattern can be modified to include the inner pattern");
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-06 14:01:03 +00:00
|
|
|
fn strip_singleton_blocks<'hir>(mut expr: &'hir Expr<'hir>) -> &'hir Expr<'hir> {
|
|
|
|
while let ExprKind::Block(block, _) = expr.kind {
|
|
|
|
match (block.stmts, block.expr) {
|
|
|
|
([stmt], None) => match stmt.kind {
|
|
|
|
StmtKind::Expr(e) | StmtKind::Semi(e) => expr = e,
|
|
|
|
_ => break,
|
|
|
|
},
|
|
|
|
([], Some(e)) => expr = e,
|
|
|
|
_ => break,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
expr
|
|
|
|
}
|
|
|
|
|
2021-08-19 18:31:25 +00:00
|
|
|
enum IfLetOrMatch<'hir> {
|
|
|
|
Match(&'hir Expr<'hir>, &'hir [Arm<'hir>], MatchSource),
|
|
|
|
/// scrutinee, pattern, then block, else block
|
2021-09-02 11:38:17 +00:00
|
|
|
IfLet(
|
|
|
|
&'hir Expr<'hir>,
|
|
|
|
&'hir Pat<'hir>,
|
|
|
|
&'hir Expr<'hir>,
|
|
|
|
Option<&'hir Expr<'hir>>,
|
|
|
|
),
|
2021-08-19 18:31:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'hir> IfLetOrMatch<'hir> {
|
|
|
|
fn parse(cx: &LateContext<'_>, expr: &Expr<'hir>) -> Option<Self> {
|
|
|
|
match expr.kind {
|
|
|
|
ExprKind::Match(expr, arms, source) => Some(Self::Match(expr, arms, source)),
|
2021-09-02 11:38:17 +00:00
|
|
|
_ => higher::IfLet::hir(cx, expr).map(
|
|
|
|
|higher::IfLet {
|
|
|
|
let_expr,
|
|
|
|
let_pat,
|
|
|
|
if_then,
|
|
|
|
if_else,
|
|
|
|
}| { Self::IfLet(let_expr, let_pat, if_then, if_else) },
|
|
|
|
),
|
2021-08-19 18:31:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A "wild-like" arm has a wild (`_`) or `None` pattern and no guard. Such arms can be "collapsed"
|
|
|
|
/// into a single wild arm without any significant loss in semantics or readability.
|
|
|
|
fn arm_is_wild_like(cx: &LateContext<'_>, arm: &Arm<'_>) -> bool {
|
|
|
|
if arm.guard.is_some() {
|
2020-12-06 14:01:03 +00:00
|
|
|
return false;
|
|
|
|
}
|
2021-08-19 18:31:25 +00:00
|
|
|
match arm.pat.kind {
|
2020-12-06 14:01:03 +00:00
|
|
|
PatKind::Binding(..) | PatKind::Wild => true,
|
2021-04-22 09:31:13 +00:00
|
|
|
PatKind::Path(ref qpath) => is_lang_ctor(cx, qpath, OptionNone),
|
2020-12-06 14:01:03 +00:00
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn find_pat_binding(pat: &Pat<'_>, hir_id: HirId) -> Option<Span> {
|
|
|
|
let mut span = None;
|
|
|
|
pat.walk_short(|p| match &p.kind {
|
|
|
|
// ignore OR patterns
|
|
|
|
PatKind::Or(_) => false,
|
|
|
|
PatKind::Binding(_bm, _, _ident, _) => {
|
|
|
|
let found = p.hir_id == hir_id;
|
|
|
|
if found {
|
|
|
|
span = Some(p.span);
|
|
|
|
}
|
|
|
|
!found
|
|
|
|
},
|
|
|
|
_ => true,
|
|
|
|
});
|
|
|
|
span
|
|
|
|
}
|
|
|
|
|
|
|
|
fn pat_contains_or(pat: &Pat<'_>) -> bool {
|
|
|
|
let mut result = false;
|
|
|
|
pat.walk(|p| {
|
|
|
|
let is_or = matches!(p.kind, PatKind::Or(_));
|
|
|
|
result |= is_or;
|
|
|
|
!is_or
|
|
|
|
});
|
|
|
|
result
|
|
|
|
}
|