blob: 3342bf83478592fe27daf0f2625b46d69dc54268 [file] [log] [blame] [edit]
use clippy_config::Conf;
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::msrvs::Msrv;
use clippy_utils::res::{MaybeDef, MaybeQPath, MaybeResPath};
use clippy_utils::source::snippet_with_context;
use clippy_utils::{is_none_pattern, msrvs, peel_hir_expr_refs, sym};
use rustc_errors::Applicability;
use rustc_hir::def::{DefKind, Res};
use rustc_hir::{Arm, Expr, ExprKind, LangItem, Pat, PatKind, QPath, is_range_literal};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::impl_lint_pass;
use rustc_span::{Span, Symbol};
declare_clippy_lint! {
/// ### What it does
/// This detects various manual reimplementations of `Option::as_slice`.
///
/// ### Why is this bad?
/// Those implementations are both more complex than calling `as_slice`
/// and unlike that incur a branch, pessimizing performance and leading
/// to more generated code.
///
/// ### Example
/// ```no_run
///# let opt = Some(1);
/// _ = opt.as_ref().map_or(&[][..], std::slice::from_ref);
/// _ = match opt.as_ref() {
/// Some(f) => std::slice::from_ref(f),
/// None => &[],
/// };
/// ```
/// Use instead:
/// ```no_run
///# let opt = Some(1);
/// _ = opt.as_slice();
/// _ = opt.as_slice();
/// ```
#[clippy::version = "1.86.0"]
pub MANUAL_OPTION_AS_SLICE,
complexity,
"manual `Option::as_slice`"
}
pub struct ManualOptionAsSlice {
msrv: Msrv,
}
impl ManualOptionAsSlice {
pub fn new(conf: &Conf) -> Self {
Self { msrv: conf.msrv }
}
}
impl_lint_pass!(ManualOptionAsSlice => [MANUAL_OPTION_AS_SLICE]);
impl LateLintPass<'_> for ManualOptionAsSlice {
fn check_expr(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
let span = expr.span;
if span.from_expansion() {
return;
}
match expr.kind {
ExprKind::Match(scrutinee, [arm1, arm2], _) => {
if is_none_pattern(cx, arm2.pat) && check_arms(cx, arm2, arm1)
|| is_none_pattern(cx, arm1.pat) && check_arms(cx, arm1, arm2)
{
check_as_ref(cx, scrutinee, span, self.msrv);
}
},
ExprKind::If(cond, then, Some(other)) => {
if let ExprKind::Let(let_expr) = cond.kind
&& let Some(binding) = extract_ident_from_some_pat(cx, let_expr.pat)
&& check_some_body(cx, binding, then)
&& is_empty_slice(cx, other.peel_blocks())
{
check_as_ref(cx, let_expr.init, span, self.msrv);
}
},
ExprKind::MethodCall(seg, callee, [], _) => {
if seg.ident.name == sym::unwrap_or_default {
check_map(cx, callee, span, self.msrv);
}
},
ExprKind::MethodCall(seg, callee, [or], _) => match seg.ident.name {
sym::unwrap_or => {
if is_empty_slice(cx, or) {
check_map(cx, callee, span, self.msrv);
}
},
sym::unwrap_or_else => {
if returns_empty_slice(cx, or) {
check_map(cx, callee, span, self.msrv);
}
},
_ => {},
},
ExprKind::MethodCall(seg, callee, [or_else, map], _) => match seg.ident.name {
sym::map_or => {
if is_empty_slice(cx, or_else) && is_slice_from_ref(cx, map) {
check_as_ref(cx, callee, span, self.msrv);
}
},
sym::map_or_else => {
if returns_empty_slice(cx, or_else) && is_slice_from_ref(cx, map) {
check_as_ref(cx, callee, span, self.msrv);
}
},
_ => {},
},
_ => {},
}
}
}
fn check_map(cx: &LateContext<'_>, map: &Expr<'_>, span: Span, msrv: Msrv) {
if let ExprKind::MethodCall(seg, callee, [mapping], _) = map.kind
&& seg.ident.name == sym::map
&& is_slice_from_ref(cx, mapping)
{
check_as_ref(cx, callee, span, msrv);
}
}
fn check_as_ref(cx: &LateContext<'_>, expr: &Expr<'_>, span: Span, msrv: Msrv) {
if let ExprKind::MethodCall(seg, callee, [], _) = expr.kind
&& seg.ident.name == sym::as_ref
&& cx.typeck_results().expr_ty(callee).is_diag_item(cx, sym::Option)
&& msrv.meets(
cx,
if clippy_utils::is_in_const_context(cx) {
msrvs::CONST_OPTION_AS_SLICE
} else {
msrvs::OPTION_AS_SLICE
},
)
{
span_lint_and_then(
cx,
MANUAL_OPTION_AS_SLICE,
span,
"manual implementation of `Option::as_slice`",
|diag| {
let mut app = Applicability::MachineApplicable;
let callee = snippet_with_context(cx, callee.span, expr.span.ctxt(), "_", &mut app).0;
diag.span_suggestion_verbose(
span,
"use `Option::as_slice` directly",
format!("{callee}.as_slice()"),
app,
);
},
);
}
}
fn extract_ident_from_some_pat(cx: &LateContext<'_>, pat: &Pat<'_>) -> Option<Symbol> {
if let PatKind::TupleStruct(QPath::Resolved(None, path), [binding], _) = pat.kind
&& let Res::Def(DefKind::Ctor(..), def_id) = path.res
&& let PatKind::Binding(_mode, _hir_id, ident, _inner_pat) = binding.kind
&& clippy_utils::is_lang_item_or_ctor(cx, def_id, LangItem::OptionSome)
{
Some(ident.name)
} else {
None
}
}
/// Returns true if `expr` is `std::slice::from_ref(<name>)`. Used in `if let`s.
fn check_some_body(cx: &LateContext<'_>, name: Symbol, expr: &Expr<'_>) -> bool {
if let ExprKind::Call(slice_from_ref, [arg]) = expr.peel_blocks().kind
&& is_slice_from_ref(cx, slice_from_ref)
&& let ExprKind::Path(QPath::Resolved(None, path)) = arg.kind
&& let [seg] = path.segments
{
seg.ident.name == name
} else {
false
}
}
fn check_arms(cx: &LateContext<'_>, none_arm: &Arm<'_>, some_arm: &Arm<'_>) -> bool {
if none_arm.guard.is_none()
&& some_arm.guard.is_none()
&& is_empty_slice(cx, none_arm.body)
&& let Some(name) = extract_ident_from_some_pat(cx, some_arm.pat)
{
check_some_body(cx, name, some_arm.body)
} else {
false
}
}
fn returns_empty_slice(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
match expr.kind {
ExprKind::Path(_) => expr.res(cx).is_diag_item(cx, sym::default_fn),
ExprKind::Closure(cl) => is_empty_slice(cx, cx.tcx.hir_body(cl.body).value),
_ => false,
}
}
/// Returns if expr returns an empty slice. If:
/// - An indexing operation to an empty array with a built-in range. `[][..]`
/// - An indexing operation with a zero-ended range. `expr[..0]`
/// - A reference to an empty array. `&[]`
/// - Or a call to `Default::default`.
fn is_empty_slice(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
let expr = peel_hir_expr_refs(expr.peel_blocks()).0;
match expr.kind {
ExprKind::Index(arr, range, _) => match arr.kind {
ExprKind::Array([]) => is_range_literal(range),
ExprKind::Array(_) => {
let Some(range) = clippy_utils::higher::Range::hir(range) else {
return false;
};
range.end.is_some_and(|e| clippy_utils::is_integer_const(cx, e, 0))
},
_ => false,
},
ExprKind::Array([]) => true,
ExprKind::Call(def, []) => def.res(cx).is_diag_item(cx, sym::default_fn),
_ => false,
}
}
fn is_slice_from_ref(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
expr.basic_res().is_diag_item(cx, sym::slice_from_ref)
}