| 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) |
| } |