summaryrefslogtreecommitdiffstats
path: root/src/tools/clippy/clippy_lints/src/matches/single_match.rs
blob: 92091a0c3395f7b4578bc9e26126b1c5daa72e7a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
use clippy_utils::diagnostics::span_lint_and_sugg;
use clippy_utils::source::{expr_block, snippet};
use clippy_utils::ty::{implements_trait, match_type, peel_mid_ty_refs};
use clippy_utils::{
    is_lint_allowed, is_unit_expr, is_wild, paths, peel_blocks, peel_hir_pat_refs, peel_n_hir_expr_refs,
};
use core::cmp::max;
use rustc_errors::Applicability;
use rustc_hir::{Arm, BindingAnnotation, Block, Expr, ExprKind, Pat, PatKind};
use rustc_lint::LateContext;
use rustc_middle::ty::{self, Ty};

use super::{MATCH_BOOL, SINGLE_MATCH, SINGLE_MATCH_ELSE};

#[rustfmt::skip]
pub(crate) fn check(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>], expr: &Expr<'_>) {
    if arms.len() == 2 && arms[0].guard.is_none() && arms[1].guard.is_none() {
        if expr.span.from_expansion() {
            // Don't lint match expressions present in
            // macro_rules! block
            return;
        }
        if let PatKind::Or(..) = arms[0].pat.kind {
            // don't lint for or patterns for now, this makes
            // the lint noisy in unnecessary situations
            return;
        }
        let els = arms[1].body;
        let els = if is_unit_expr(peel_blocks(els)) {
            None
        } else if let ExprKind::Block(Block { stmts, expr: block_expr, .. }, _) = els.kind {
            if stmts.len() == 1 && block_expr.is_none() || stmts.is_empty() && block_expr.is_some() {
                // single statement/expr "else" block, don't lint
                return;
            }
            // block with 2+ statements or 1 expr and 1+ statement
            Some(els)
        } else {
            // not a block, don't lint
            return;
        };

        let ty = cx.typeck_results().expr_ty(ex);
        if *ty.kind() != ty::Bool || is_lint_allowed(cx, MATCH_BOOL, ex.hir_id) {
            check_single_pattern(cx, ex, arms, expr, els);
            check_opt_like(cx, ex, arms, expr, ty, els);
        }
    }
}

fn check_single_pattern(
    cx: &LateContext<'_>,
    ex: &Expr<'_>,
    arms: &[Arm<'_>],
    expr: &Expr<'_>,
    els: Option<&Expr<'_>>,
) {
    if is_wild(arms[1].pat) {
        report_single_pattern(cx, ex, arms, expr, els);
    }
}

fn report_single_pattern(
    cx: &LateContext<'_>,
    ex: &Expr<'_>,
    arms: &[Arm<'_>],
    expr: &Expr<'_>,
    els: Option<&Expr<'_>>,
) {
    let lint = if els.is_some() { SINGLE_MATCH_ELSE } else { SINGLE_MATCH };
    let els_str = els.map_or(String::new(), |els| {
        format!(" else {}", expr_block(cx, els, None, "..", Some(expr.span)))
    });

    let (pat, pat_ref_count) = peel_hir_pat_refs(arms[0].pat);
    let (msg, sugg) = if_chain! {
        if let PatKind::Path(_) | PatKind::Lit(_) = pat.kind;
        let (ty, ty_ref_count) = peel_mid_ty_refs(cx.typeck_results().expr_ty(ex));
        if let Some(spe_trait_id) = cx.tcx.lang_items().structural_peq_trait();
        if let Some(pe_trait_id) = cx.tcx.lang_items().eq_trait();
        if ty.is_integral() || ty.is_char() || ty.is_str()
            || (implements_trait(cx, ty, spe_trait_id, &[])
                && implements_trait(cx, ty, pe_trait_id, &[ty.into()]));
        then {
            // scrutinee derives PartialEq and the pattern is a constant.
            let pat_ref_count = match pat.kind {
                // string literals are already a reference.
                PatKind::Lit(Expr { kind: ExprKind::Lit(lit), .. }) if lit.node.is_str() => pat_ref_count + 1,
                _ => pat_ref_count,
            };
            // References are only implicitly added to the pattern, so no overflow here.
            // e.g. will work: match &Some(_) { Some(_) => () }
            // will not: match Some(_) { &Some(_) => () }
            let ref_count_diff = ty_ref_count - pat_ref_count;

            // Try to remove address of expressions first.
            let (ex, removed) = peel_n_hir_expr_refs(ex, ref_count_diff);
            let ref_count_diff = ref_count_diff - removed;

            let msg = "you seem to be trying to use `match` for an equality check. Consider using `if`";
            let sugg = format!(
                "if {} == {}{} {}{}",
                snippet(cx, ex.span, ".."),
                // PartialEq for different reference counts may not exist.
                "&".repeat(ref_count_diff),
                snippet(cx, arms[0].pat.span, ".."),
                expr_block(cx, arms[0].body, None, "..", Some(expr.span)),
                els_str,
            );
            (msg, sugg)
        } else {
            let msg = "you seem to be trying to use `match` for destructuring a single pattern. Consider using `if let`";
            let sugg = format!(
                "if let {} = {} {}{}",
                snippet(cx, arms[0].pat.span, ".."),
                snippet(cx, ex.span, ".."),
                expr_block(cx, arms[0].body, None, "..", Some(expr.span)),
                els_str,
            );
            (msg, sugg)
        }
    };

    span_lint_and_sugg(
        cx,
        lint,
        expr.span,
        msg,
        "try this",
        sugg,
        Applicability::HasPlaceholders,
    );
}

fn check_opt_like<'a>(
    cx: &LateContext<'a>,
    ex: &Expr<'_>,
    arms: &[Arm<'_>],
    expr: &Expr<'_>,
    ty: Ty<'a>,
    els: Option<&Expr<'_>>,
) {
    // We don't want to lint if the second arm contains an enum which could
    // have more variants in the future.
    if form_exhaustive_matches(cx, ty, arms[0].pat, arms[1].pat) {
        report_single_pattern(cx, ex, arms, expr, els);
    }
}

/// Returns `true` if all of the types in the pattern are enums which we know
/// won't be expanded in the future
fn pat_in_candidate_enum<'a>(cx: &LateContext<'a>, ty: Ty<'a>, pat: &Pat<'_>) -> bool {
    let mut paths_and_types = Vec::new();
    collect_pat_paths(&mut paths_and_types, cx, pat, ty);
    paths_and_types.iter().all(|ty| in_candidate_enum(cx, *ty))
}

/// Returns `true` if the given type is an enum we know won't be expanded in the future
fn in_candidate_enum<'a>(cx: &LateContext<'a>, ty: Ty<'_>) -> bool {
    // list of candidate `Enum`s we know will never get any more members
    let candidates = [&paths::COW, &paths::OPTION, &paths::RESULT];

    for candidate_ty in candidates {
        if match_type(cx, ty, candidate_ty) {
            return true;
        }
    }
    false
}

/// Collects types from the given pattern
fn collect_pat_paths<'a>(acc: &mut Vec<Ty<'a>>, cx: &LateContext<'a>, pat: &Pat<'_>, ty: Ty<'a>) {
    match pat.kind {
        PatKind::Tuple(inner, _) => inner.iter().for_each(|p| {
            let p_ty = cx.typeck_results().pat_ty(p);
            collect_pat_paths(acc, cx, p, p_ty);
        }),
        PatKind::TupleStruct(..) | PatKind::Binding(BindingAnnotation::Unannotated, .., None) | PatKind::Path(_) => {
            acc.push(ty);
        },
        _ => {},
    }
}

/// Returns true if the given arm of pattern matching contains wildcard patterns.
fn contains_only_wilds(pat: &Pat<'_>) -> bool {
    match pat.kind {
        PatKind::Wild => true,
        PatKind::Tuple(inner, _) | PatKind::TupleStruct(_, inner, ..) => inner.iter().all(contains_only_wilds),
        _ => false,
    }
}

/// Returns true if the given patterns forms only exhaustive matches that don't contain enum
/// patterns without a wildcard.
fn form_exhaustive_matches<'a>(cx: &LateContext<'a>, ty: Ty<'a>, left: &Pat<'_>, right: &Pat<'_>) -> bool {
    match (&left.kind, &right.kind) {
        (PatKind::Wild, _) | (_, PatKind::Wild) => true,
        (PatKind::Tuple(left_in, left_pos), PatKind::Tuple(right_in, right_pos)) => {
            // We don't actually know the position and the presence of the `..` (dotdot) operator
            // in the arms, so we need to evaluate the correct offsets here in order to iterate in
            // both arms at the same time.
            let len = max(
                left_in.len() + {
                    if left_pos.is_some() { 1 } else { 0 }
                },
                right_in.len() + {
                    if right_pos.is_some() { 1 } else { 0 }
                },
            );
            let mut left_pos = left_pos.unwrap_or(usize::MAX);
            let mut right_pos = right_pos.unwrap_or(usize::MAX);
            let mut left_dot_space = 0;
            let mut right_dot_space = 0;
            for i in 0..len {
                let mut found_dotdot = false;
                if i == left_pos {
                    left_dot_space += 1;
                    if left_dot_space < len - left_in.len() {
                        left_pos += 1;
                    }
                    found_dotdot = true;
                }
                if i == right_pos {
                    right_dot_space += 1;
                    if right_dot_space < len - right_in.len() {
                        right_pos += 1;
                    }
                    found_dotdot = true;
                }
                if found_dotdot {
                    continue;
                }
                if !contains_only_wilds(&left_in[i - left_dot_space])
                    && !contains_only_wilds(&right_in[i - right_dot_space])
                {
                    return false;
                }
            }
            true
        },
        (PatKind::TupleStruct(..), PatKind::Path(_)) => pat_in_candidate_enum(cx, ty, right),
        (PatKind::TupleStruct(..), PatKind::TupleStruct(_, inner, _)) => {
            pat_in_candidate_enum(cx, ty, right) && inner.iter().all(contains_only_wilds)
        },
        _ => false,
    }
}