183 lines
7.2 KiB
Rust
183 lines
7.2 KiB
Rust
use rustc_errors::Applicability;
|
|
use rustc_hir::def::Res;
|
|
use rustc_hir::{Arm, Expr, ExprKind, HirId, LangItem, MatchSource, Pat, PatKind, QPath};
|
|
use rustc_lint::{LateContext, LateLintPass, LintContext};
|
|
use rustc_middle::ty::GenericArgKind;
|
|
use rustc_session::declare_lint_pass;
|
|
use rustc_span::sym;
|
|
|
|
use clippy_utils::diagnostics::span_lint_and_sugg;
|
|
use clippy_utils::higher::IfLetOrMatch;
|
|
use clippy_utils::sugg::Sugg;
|
|
use clippy_utils::ty::implements_trait;
|
|
use clippy_utils::{is_default_equivalent, is_in_const_context, peel_blocks, span_contains_comment};
|
|
|
|
declare_clippy_lint! {
|
|
/// ### What it does
|
|
/// Checks if a `match` or `if let` expression can be simplified using
|
|
/// `.unwrap_or_default()`.
|
|
///
|
|
/// ### Why is this bad?
|
|
/// It can be done in one call with `.unwrap_or_default()`.
|
|
///
|
|
/// ### Example
|
|
/// ```no_run
|
|
/// let x: Option<String> = Some(String::new());
|
|
/// let y: String = match x {
|
|
/// Some(v) => v,
|
|
/// None => String::new(),
|
|
/// };
|
|
///
|
|
/// let x: Option<Vec<String>> = Some(Vec::new());
|
|
/// let y: Vec<String> = if let Some(v) = x {
|
|
/// v
|
|
/// } else {
|
|
/// Vec::new()
|
|
/// };
|
|
/// ```
|
|
/// Use instead:
|
|
/// ```no_run
|
|
/// let x: Option<String> = Some(String::new());
|
|
/// let y: String = x.unwrap_or_default();
|
|
///
|
|
/// let x: Option<Vec<String>> = Some(Vec::new());
|
|
/// let y: Vec<String> = x.unwrap_or_default();
|
|
/// ```
|
|
#[clippy::version = "1.79.0"]
|
|
pub MANUAL_UNWRAP_OR_DEFAULT,
|
|
suspicious,
|
|
"check if a `match` or `if let` can be simplified with `unwrap_or_default`"
|
|
}
|
|
|
|
declare_lint_pass!(ManualUnwrapOrDefault => [MANUAL_UNWRAP_OR_DEFAULT]);
|
|
|
|
fn get_some<'tcx>(cx: &LateContext<'tcx>, pat: &Pat<'tcx>) -> Option<HirId> {
|
|
if let PatKind::TupleStruct(QPath::Resolved(_, path), &[pat], _) = pat.kind
|
|
&& let PatKind::Binding(_, pat_id, _, _) = pat.kind
|
|
&& let Some(def_id) = path.res.opt_def_id()
|
|
// Since it comes from a pattern binding, we need to get the parent to actually match
|
|
// against it.
|
|
&& let Some(def_id) = cx.tcx.opt_parent(def_id)
|
|
&& (cx.tcx.lang_items().get(LangItem::OptionSome) == Some(def_id)
|
|
|| cx.tcx.lang_items().get(LangItem::ResultOk) == Some(def_id))
|
|
{
|
|
Some(pat_id)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn get_none<'tcx>(cx: &LateContext<'tcx>, arm: &Arm<'tcx>) -> Option<&'tcx Expr<'tcx>> {
|
|
if let PatKind::Path(QPath::Resolved(_, path)) = arm.pat.kind
|
|
&& let Some(def_id) = path.res.opt_def_id()
|
|
// Since it comes from a pattern binding, we need to get the parent to actually match
|
|
// against it.
|
|
&& let Some(def_id) = cx.tcx.opt_parent(def_id)
|
|
&& cx.tcx.lang_items().get(LangItem::OptionNone) == Some(def_id)
|
|
{
|
|
Some(arm.body)
|
|
} else if let PatKind::TupleStruct(QPath::Resolved(_, path), _, _)= arm.pat.kind
|
|
&& let Some(def_id) = path.res.opt_def_id()
|
|
// Since it comes from a pattern binding, we need to get the parent to actually match
|
|
// against it.
|
|
&& let Some(def_id) = cx.tcx.opt_parent(def_id)
|
|
&& cx.tcx.lang_items().get(LangItem::ResultErr) == Some(def_id)
|
|
{
|
|
Some(arm.body)
|
|
} else if let PatKind::Wild = arm.pat.kind {
|
|
// We consider that the `Some` check will filter it out if it's not right.
|
|
Some(arm.body)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn get_some_and_none_bodies<'tcx>(
|
|
cx: &LateContext<'tcx>,
|
|
arm1: &'tcx Arm<'tcx>,
|
|
arm2: &'tcx Arm<'tcx>,
|
|
) -> Option<((&'tcx Expr<'tcx>, HirId), &'tcx Expr<'tcx>)> {
|
|
if let Some(binding_id) = get_some(cx, arm1.pat)
|
|
&& let Some(body_none) = get_none(cx, arm2)
|
|
{
|
|
Some(((arm1.body, binding_id), body_none))
|
|
} else if let Some(binding_id) = get_some(cx, arm2.pat)
|
|
&& let Some(body_none) = get_none(cx, arm1)
|
|
{
|
|
Some(((arm2.body, binding_id), body_none))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::needless_pass_by_value)]
|
|
fn handle<'tcx>(cx: &LateContext<'tcx>, if_let_or_match: IfLetOrMatch<'tcx>, expr: &'tcx Expr<'tcx>) {
|
|
// Get expr_name ("if let" or "match" depending on kind of expression), the condition, the body for
|
|
// the some arm, the body for the none arm and the binding id of the some arm
|
|
let (expr_name, condition, body_some, body_none, binding_id) = match if_let_or_match {
|
|
IfLetOrMatch::Match(condition, [arm1, arm2], MatchSource::Normal | MatchSource::ForLoopDesugar)
|
|
// Make sure there are no guards to keep things simple
|
|
if arm1.guard.is_none()
|
|
&& arm2.guard.is_none()
|
|
// Get the some and none bodies and the binding id of the some arm
|
|
&& let Some(((body_some, binding_id), body_none)) = get_some_and_none_bodies(cx, arm1, arm2) =>
|
|
{
|
|
("match", condition, body_some, body_none, binding_id)
|
|
},
|
|
IfLetOrMatch::IfLet(condition, pat, if_expr, Some(else_expr), _)
|
|
if let Some(binding_id) = get_some(cx, pat) =>
|
|
{
|
|
("if let", condition, if_expr, else_expr, binding_id)
|
|
},
|
|
_ => {
|
|
// All other cases (match with number of arms != 2, if let without else, etc.)
|
|
return;
|
|
},
|
|
};
|
|
|
|
// We check if the return type of the expression implements Default.
|
|
let expr_type = cx.typeck_results().expr_ty(expr);
|
|
if let Some(default_trait_id) = cx.tcx.get_diagnostic_item(sym::Default)
|
|
&& implements_trait(cx, expr_type, default_trait_id, &[])
|
|
// We check if the initial condition implements Default.
|
|
&& let Some(condition_ty) = cx.typeck_results().expr_ty(condition).walk().nth(1)
|
|
&& let GenericArgKind::Type(condition_ty) = condition_ty.unpack()
|
|
&& implements_trait(cx, condition_ty, default_trait_id, &[])
|
|
// We check that the `Some(x) => x` doesn't do anything apart "returning" the value in `Some`.
|
|
&& let ExprKind::Path(QPath::Resolved(_, path)) = peel_blocks(body_some).kind
|
|
&& let Res::Local(local_id) = path.res
|
|
&& local_id == binding_id
|
|
// We now check the `None` arm is calling a method equivalent to `Default::default`.
|
|
&& let body_none = peel_blocks(body_none)
|
|
&& is_default_equivalent(cx, body_none)
|
|
&& let Some(receiver) = Sugg::hir_opt(cx, condition).map(Sugg::maybe_par)
|
|
{
|
|
// Machine applicable only if there are no comments present
|
|
let applicability = if span_contains_comment(cx.sess().source_map(), expr.span) {
|
|
Applicability::MaybeIncorrect
|
|
} else {
|
|
Applicability::MachineApplicable
|
|
};
|
|
span_lint_and_sugg(
|
|
cx,
|
|
MANUAL_UNWRAP_OR_DEFAULT,
|
|
expr.span,
|
|
format!("{expr_name} can be simplified with `.unwrap_or_default()`"),
|
|
"replace it with",
|
|
format!("{receiver}.unwrap_or_default()"),
|
|
applicability,
|
|
);
|
|
}
|
|
}
|
|
|
|
impl<'tcx> LateLintPass<'tcx> for ManualUnwrapOrDefault {
|
|
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
|
|
if let Some(if_let_or_match) = IfLetOrMatch::parse(cx, expr)
|
|
&& !expr.span.from_expansion()
|
|
&& !is_in_const_context(cx)
|
|
{
|
|
handle(cx, if_let_or_match, expr);
|
|
}
|
|
}
|
|
}
|