Skip to content

Commit

Permalink
Improvements to match_wildcard_for_single_variants and `wildcard_en…
Browse files Browse the repository at this point in the history
…um_match_arm` lints

Don't lint on `Result` and `Option` types.
Considers `or` patterns.
Considers variants prefixed with `Self`
Suggestions will try to find a common prefix rather than just using the full path
  • Loading branch information
Jarcho committed Mar 8, 2021
1 parent 54def1e commit 45c41db
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 103 deletions.
247 changes: 156 additions & 91 deletions clippy_lints/src/matches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ use crate::utils::visitors::LocalUsedVisitor;
use crate::utils::{
expr_block, get_parent_expr, implements_trait, in_macro, indent_of, is_allowed, is_expn_of, is_refutable,
is_type_diagnostic_item, is_wild, match_qpath, match_type, meets_msrv, multispan_sugg, path_to_local,
path_to_local_id, peel_hir_pat_refs, peel_mid_ty_refs, peel_n_hir_expr_refs, remove_blocks, snippet, snippet_block,
snippet_opt, snippet_with_applicability, span_lint_and_help, span_lint_and_note, span_lint_and_sugg,
span_lint_and_then, strip_pat_refs,
path_to_local_id, peel_hir_pat_refs, peel_mid_ty_refs, peel_n_hir_expr_refs, recurse_or_patterns, remove_blocks,
snippet, snippet_block, snippet_opt, snippet_with_applicability, span_lint_and_help, span_lint_and_note,
span_lint_and_sugg, span_lint_and_then, strip_pat_refs,
};
use crate::utils::{paths, search_same, SpanlessEq, SpanlessHash};
use if_chain::if_chain;
use rustc_ast::ast::LitKind;
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_errors::Applicability;
use rustc_hir::def::CtorKind;
use rustc_hir::def::{CtorKind, DefKind, Res};
use rustc_hir::{
Arm, BindingAnnotation, Block, BorrowKind, Expr, ExprKind, Guard, HirId, Local, MatchSource, Mutability, Node, Pat,
PatKind, QPath, RangeEnd,
self as hir, Arm, BindingAnnotation, Block, BorrowKind, Expr, ExprKind, Guard, HirId, Local, MatchSource,
Mutability, Node, Pat, PatKind, PathSegment, QPath, RangeEnd, TyKind,
};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::lint::in_external_macro;
use rustc_middle::ty::{self, Ty, TyS};
use rustc_middle::ty::{self, Ty, TyS, VariantDef};
use rustc_semver::RustcVersion;
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::source_map::{Span, Spanned};
Expand Down Expand Up @@ -954,114 +954,179 @@ fn check_wild_err_arm<'tcx>(cx: &LateContext<'tcx>, ex: &Expr<'tcx>, arms: &[Arm
}
}

fn check_wild_enum_match(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) {
let ty = cx.typeck_results().expr_ty(ex);
if !ty.is_enum() {
// If there isn't a nice closed set of possible values that can be conveniently enumerated,
// don't complain about not enumerating the mall.
return;
enum CommonPrefixSearcher<'a> {
None,
Path(&'a [PathSegment<'a>]),
Mixed,
}
impl CommonPrefixSearcher<'a> {
fn with_path(&mut self, path: &'a [PathSegment<'a>]) {
match path {
[path @ .., _] => self.with_prefix(path),
[] => (),
}
}

fn with_prefix(&mut self, path: &'a [PathSegment<'a>]) {
match self {
Self::None => *self = Self::Path(path),
Self::Path(self_path)
if path
.iter()
.map(|p| p.ident.name)
.eq(self_path.iter().map(|p| p.ident.name)) => {},
Self::Path(_) => *self = Self::Mixed,
Self::Mixed => (),
}
}
}

#[allow(clippy::too_many_lines)]
fn check_wild_enum_match(cx: &LateContext<'_>, ex: &Expr<'_>, arms: &[Arm<'_>]) {
let ty = cx.typeck_results().expr_ty(ex).peel_refs();
let adt_def = match ty.kind() {
ty::Adt(adt_def, _)
if adt_def.is_enum()
&& !(is_type_diagnostic_item(cx, ty, sym::option_type)
|| is_type_diagnostic_item(cx, ty, sym::result_type)) =>
{
adt_def
},
_ => return,
};

// First pass - check for violation, but don't do much book-keeping because this is hopefully
// the uncommon case, and the book-keeping is slightly expensive.
let mut wildcard_span = None;
let mut wildcard_ident = None;
let mut has_non_wild = false;
for arm in arms {
if let PatKind::Wild = arm.pat.kind {
wildcard_span = Some(arm.pat.span);
} else if let PatKind::Binding(_, _, ident, None) = arm.pat.kind {
wildcard_span = Some(arm.pat.span);
wildcard_ident = Some(ident);
match peel_hir_pat_refs(arm.pat).0.kind {
PatKind::Wild => wildcard_span = Some(arm.pat.span),
PatKind::Binding(_, _, ident, None) => {
wildcard_span = Some(arm.pat.span);
wildcard_ident = Some(ident);
},
_ => has_non_wild = true,
}
}
let wildcard_span = match wildcard_span {
Some(x) if has_non_wild => x,
_ => return,
};

if let Some(wildcard_span) = wildcard_span {
// Accumulate the variants which should be put in place of the wildcard because they're not
// already covered.
// Accumulate the variants which should be put in place of the wildcard because they're not
// already covered.
let mut missing_variants: Vec<_> = adt_def.variants.iter().collect();

let mut missing_variants = vec![];
if let ty::Adt(def, _) = ty.kind() {
for variant in &def.variants {
missing_variants.push(variant);
let mut path_prefix = CommonPrefixSearcher::None;
for arm in arms {
// Guards mean that this case probably isn't exhaustively covered. Technically
// this is incorrect, as we should really check whether each variant is exhaustively
// covered by the set of guards that cover it, but that's really hard to do.
recurse_or_patterns(arm.pat, |pat| {
let path = match &peel_hir_pat_refs(pat).0.kind {
PatKind::Path(path) => {
let id = match cx.qpath_res(path, pat.hir_id) {
Res::Def(DefKind::Const | DefKind::ConstParam | DefKind::AnonConst, _) => return,
Res::Def(_, id) => id,
_ => return,
};
if arm.guard.is_none() {
missing_variants.retain(|e| e.ctor_def_id != Some(id));
}
path
},
PatKind::TupleStruct(path, patterns, ..) => {
if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p)) {
let id = cx.qpath_res(path, pat.hir_id).def_id();
missing_variants.retain(|e| e.ctor_def_id != Some(id));
}
path
},
PatKind::Struct(path, patterns, ..) => {
if arm.guard.is_none() && patterns.iter().all(|p| !is_refutable(cx, p.pat)) {
let id = cx.qpath_res(path, pat.hir_id).def_id();
missing_variants.retain(|e| e.def_id != id);
}
path
},
_ => return,
};
match path {
QPath::Resolved(_, path) => path_prefix.with_path(path.segments),
QPath::TypeRelative(
hir::Ty {
kind: TyKind::Path(QPath::Resolved(_, path)),
..
},
_,
) => path_prefix.with_prefix(path.segments),
_ => (),
}
}
});
}

for arm in arms {
if arm.guard.is_some() {
// Guards mean that this case probably isn't exhaustively covered. Technically
// this is incorrect, as we should really check whether each variant is exhaustively
// covered by the set of guards that cover it, but that's really hard to do.
continue;
}
if let PatKind::Path(ref path) = arm.pat.kind {
if let QPath::Resolved(_, p) = path {
missing_variants.retain(|e| e.ctor_def_id != Some(p.res.def_id()));
}
} else if let PatKind::TupleStruct(QPath::Resolved(_, p), ref patterns, ..) = arm.pat.kind {
// Some simple checks for exhaustive patterns.
// There is a room for improvements to detect more cases,
// but it can be more expensive to do so.
let is_pattern_exhaustive =
|pat: &&Pat<'_>| matches!(pat.kind, PatKind::Wild | PatKind::Binding(.., None));
if patterns.iter().all(is_pattern_exhaustive) {
missing_variants.retain(|e| e.ctor_def_id != Some(p.res.def_id()));
let format_suggestion = |variant: &VariantDef| {
format!(
"{}{}{}{}",
if let Some(ident) = wildcard_ident {
format!("{} @ ", ident.name)
} else {
String::new()
},
if let CommonPrefixSearcher::Path(path_prefix) = path_prefix {
let mut s = String::new();
for seg in path_prefix {
s.push_str(&seg.ident.as_str());
s.push_str("::");
}
s
} else {
let mut s = cx.tcx.def_path_str(adt_def.did);
s.push_str("::");
s
},
variant.ident.name,
match variant.ctor_kind {
CtorKind::Fn => "(..)",
CtorKind::Const => "",
CtorKind::Fictive => "{ .. }",
}
}

let mut suggestion: Vec<String> = missing_variants
.iter()
.map(|v| {
let suffix = match v.ctor_kind {
CtorKind::Fn => "(..)",
CtorKind::Const | CtorKind::Fictive => "",
};
let ident_str = if let Some(ident) = wildcard_ident {
format!("{} @ ", ident.name)
} else {
String::new()
};
// This path assumes that the enum type is imported into scope.
format!("{}{}{}", ident_str, cx.tcx.def_path_str(v.def_id), suffix)
})
.collect();

if suggestion.is_empty() {
return;
}

let mut message = "wildcard match will miss any future added variants";
)
};

if let ty::Adt(def, _) = ty.kind() {
if def.is_variant_list_non_exhaustive() {
message = "match on non-exhaustive enum doesn't explicitly match all known variants";
suggestion.push(String::from("_"));
}
}
match missing_variants.as_slice() {
[] => (),
[x] if !adt_def.is_variant_list_non_exhaustive() => span_lint_and_sugg(
cx,
MATCH_WILDCARD_FOR_SINGLE_VARIANTS,
wildcard_span,
"match on non-exhaustive enum doesn't explicitly match all known variants",
"try this",
format_suggestion(x),
Applicability::MaybeIncorrect,
),
variants => {
let mut suggestions: Vec<_> = variants.iter().cloned().map(format_suggestion).collect();
let message = if adt_def.is_variant_list_non_exhaustive() {
suggestions.push("_".into());
"match on non-exhaustive enum doesn't explicitly match all known variants"
} else {
"wildcard match will miss any future added variants"
};

if suggestion.len() == 1 {
// No need to check for non-exhaustive enum as in that case len would be greater than 1
span_lint_and_sugg(
cx,
MATCH_WILDCARD_FOR_SINGLE_VARIANTS,
WILDCARD_ENUM_MATCH_ARM,
wildcard_span,
message,
"try this",
suggestion[0].clone(),
suggestions.join(" | "),
Applicability::MaybeIncorrect,
)
};

span_lint_and_sugg(
cx,
WILDCARD_ENUM_MATCH_ARM,
wildcard_span,
message,
"try this",
suggestion.join(" | "),
Applicability::MaybeIncorrect,
)
}
},
};
}

// If the block contains only a `panic!` macro (as expression or statement)
Expand Down
10 changes: 10 additions & 0 deletions clippy_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,16 @@ pub fn is_refutable(cx: &LateContext<'_>, pat: &Pat<'_>) -> bool {
}
}

/// If the pattern is an `or` pattern, call the function once for each sub pattern. Otherwise, call
/// the function once on the given pattern.
pub fn recurse_or_patterns<'tcx, F: FnMut(&'tcx Pat<'tcx>)>(pat: &'tcx Pat<'tcx>, mut f: F) {
if let PatKind::Or(pats) = pat.kind {
pats.iter().cloned().for_each(f)
} else {
f(pat)
}
}

/// Checks for the `#[automatically_derived]` attribute all `#[derive]`d
/// implementations have.
pub fn is_automatically_derived(attrs: &[ast::Attribute]) -> bool {
Expand Down
40 changes: 40 additions & 0 deletions tests/ui/match_wildcard_for_single_variants.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ enum Color {
Blue,
Rgb(u8, u8, u8),
}
impl Color {
fn f(self) {
match self {
Self::Red => (),
Self::Green => (),
Self::Blue => (),
Self::Rgb(..) => (),
};
}
}

fn main() {
let f = Foo::A;
Expand Down Expand Up @@ -56,4 +66,34 @@ fn main() {
Color::Rgb(255, _, _) => {},
_ => {},
}

// References shouldn't change anything
match &color {
&Color::Red => (),
Color::Green => (),
&Color::Rgb(..) => (),
Color::Blue => (),
}

use self::Color as C;

match color {
C::Red => (),
C::Green => (),
C::Rgb(..) => (),
C::Blue => (),
}

match color {
C::Red => (),
Color::Green => (),
Color::Rgb(..) => (),
Color::Blue => (),
}

match Some(0) {
Some(0) => 0,
Some(_) => 1,
_ => 2,
};
}
Loading

0 comments on commit 45c41db

Please sign in to comment.