use std::ops::ControlFlow; use clippy_utils::diagnostics::span_lint_and_sugg; use clippy_utils::eager_or_lazy::switch_to_lazy_eval; use clippy_utils::source::snippet_with_context; use clippy_utils::ty::{expr_type_is_certain, implements_trait, is_type_diagnostic_item}; use clippy_utils::visitors::for_each_expr; use clippy_utils::{ contains_return, is_default_equivalent, is_default_equivalent_call, last_path_segment, peel_blocks, }; use rustc_errors::Applicability; use rustc_lint::LateContext; use rustc_middle::ty; use rustc_span::Span; use rustc_span::symbol::{self, Symbol, sym}; use {rustc_ast as ast, rustc_hir as hir}; use super::{OR_FUN_CALL, UNWRAP_OR_DEFAULT}; /// Checks for the `OR_FUN_CALL` lint. #[expect(clippy::too_many_lines)] pub(super) fn check<'tcx>( cx: &LateContext<'tcx>, expr: &hir::Expr<'_>, method_span: Span, name: &str, receiver: &'tcx hir::Expr<'_>, args: &'tcx [hir::Expr<'_>], ) { /// Checks for `unwrap_or(T::new())`, `unwrap_or(T::default())`, /// `or_insert(T::new())` or `or_insert(T::default())`. /// Similarly checks for `unwrap_or_else(T::new)`, `unwrap_or_else(T::default)`, /// `or_insert_with(T::new)` or `or_insert_with(T::default)`. fn check_unwrap_or_default( cx: &LateContext<'_>, name: &str, receiver: &hir::Expr<'_>, fun: &hir::Expr<'_>, call_expr: Option<&hir::Expr<'_>>, span: Span, method_span: Span, ) -> bool { if !expr_type_is_certain(cx, receiver) { return false; } let is_new = |fun: &hir::Expr<'_>| { if let hir::ExprKind::Path(ref qpath) = fun.kind { let path = last_path_segment(qpath).ident.name; matches!(path, sym::new) } else { false } }; let output_type_implements_default = |fun| { let fun_ty = cx.typeck_results().expr_ty(fun); if let ty::FnDef(def_id, args) = fun_ty.kind() { let output_ty = cx.tcx.fn_sig(def_id).instantiate(cx.tcx, args).skip_binder().output(); cx.tcx .get_diagnostic_item(sym::Default) .is_some_and(|default_trait_id| implements_trait(cx, output_ty, default_trait_id, &[])) } else { false } }; let sugg = match (name, call_expr.is_some()) { ("unwrap_or", true) | ("unwrap_or_else", false) => sym!(unwrap_or_default), ("or_insert", true) | ("or_insert_with", false) => sym!(or_default), _ => return false, }; let receiver_ty = cx.typeck_results().expr_ty_adjusted(receiver).peel_refs(); let Some(suggested_method_def_id) = receiver_ty.ty_adt_def().and_then(|adt_def| { cx.tcx .inherent_impls(adt_def.did()) .iter() .flat_map(|impl_id| cx.tcx.associated_items(impl_id).filter_by_name_unhygienic(sugg)) .find_map(|assoc| { if assoc.fn_has_self_parameter && cx.tcx.fn_sig(assoc.def_id).skip_binder().inputs().skip_binder().len() == 1 { Some(assoc.def_id) } else { None } }) }) else { return false; }; let in_sugg_method_implementation = { matches!( suggested_method_def_id.as_local(), Some(local_def_id) if local_def_id == cx.tcx.hir().get_parent_item(receiver.hir_id).def_id ) }; if in_sugg_method_implementation { return false; } // needs to target Default::default in particular or be *::new and have a Default impl // available if (is_new(fun) && output_type_implements_default(fun)) || match call_expr { Some(call_expr) => is_default_equivalent(cx, call_expr), None => is_default_equivalent_call(cx, fun, None) || closure_body_returns_empty_to_string(cx, fun), } { span_lint_and_sugg( cx, UNWRAP_OR_DEFAULT, method_span.with_hi(span.hi()), format!("use of `{name}` to construct default value"), "try", format!("{sugg}()"), Applicability::MachineApplicable, ); true } else { false } } /// Checks for `*or(foo())`. #[expect(clippy::too_many_arguments)] fn check_or_fn_call<'tcx>( cx: &LateContext<'tcx>, name: &str, method_span: Span, self_expr: &hir::Expr<'_>, arg: &'tcx hir::Expr<'_>, // `Some` if fn has second argument second_arg: Option<&hir::Expr<'_>>, span: Span, // None if lambda is required fun_span: Option, ) -> bool { // (path, fn_has_argument, methods, suffix) const KNOW_TYPES: [(Symbol, bool, &[&str], &str); 4] = [ (sym::BTreeEntry, false, &["or_insert"], "with"), (sym::HashMapEntry, false, &["or_insert"], "with"), (sym::Option, false, &["map_or", "ok_or", "or", "unwrap_or"], "else"), (sym::Result, true, &["or", "unwrap_or"], "else"), ]; if KNOW_TYPES.iter().any(|k| k.2.contains(&name)) && switch_to_lazy_eval(cx, arg) && !contains_return(arg) && let self_ty = cx.typeck_results().expr_ty(self_expr) && let Some(&(_, fn_has_arguments, poss, suffix)) = KNOW_TYPES.iter().find(|&&i| is_type_diagnostic_item(cx, self_ty, i.0)) && poss.contains(&name) { let ctxt = span.ctxt(); let mut app = Applicability::HasPlaceholders; let sugg = { let (snippet_span, use_lambda) = match (fn_has_arguments, fun_span) { (false, Some(fun_span)) => (fun_span, false), _ => (arg.span, true), }; let snip = snippet_with_context(cx, snippet_span, ctxt, "..", &mut app).0; let snip = if use_lambda { let l_arg = if fn_has_arguments { "_" } else { "" }; format!("|{l_arg}| {snip}") } else { snip.into_owned() }; if let Some(f) = second_arg { let f = snippet_with_context(cx, f.span, ctxt, "..", &mut app).0; format!("{snip}, {f}") } else { snip } }; let span_replace_word = method_span.with_hi(span.hi()); span_lint_and_sugg( cx, OR_FUN_CALL, span_replace_word, format!("function call inside of `{name}`"), "try", format!("{name}_{suffix}({sugg})"), app, ); true } else { false } } if let [arg] = args { let inner_arg = peel_blocks(arg); for_each_expr(cx, inner_arg, |ex| { // `or_fun_call` lint needs to take nested expr into account, // but `unwrap_or_default` lint doesn't, we don't want something like: // `opt.unwrap_or(Foo { inner: String::default(), other: 1 })` to get replaced by // `opt.unwrap_or_default()`. let is_nested_expr = ex.hir_id != inner_arg.hir_id; let is_triggered = match ex.kind { hir::ExprKind::Call(fun, fun_args) => { let inner_fun_has_args = !fun_args.is_empty(); let fun_span = if inner_fun_has_args || is_nested_expr { None } else { Some(fun.span) }; (!inner_fun_has_args && !is_nested_expr && check_unwrap_or_default(cx, name, receiver, fun, Some(ex), expr.span, method_span)) || check_or_fn_call(cx, name, method_span, receiver, arg, None, expr.span, fun_span) }, hir::ExprKind::Path(..) | hir::ExprKind::Closure(..) if !is_nested_expr => { check_unwrap_or_default(cx, name, receiver, ex, None, expr.span, method_span) }, hir::ExprKind::Index(..) | hir::ExprKind::MethodCall(..) => { check_or_fn_call(cx, name, method_span, receiver, arg, None, expr.span, None) }, _ => false, }; if is_triggered { ControlFlow::Break(()) } else { ControlFlow::Continue(()) } }); } // `map_or` takes two arguments if let [arg, lambda] = args { let inner_arg = peel_blocks(arg); for_each_expr(cx, inner_arg, |ex| { let is_top_most_expr = ex.hir_id == inner_arg.hir_id; if let hir::ExprKind::Call(fun, fun_args) = ex.kind { let fun_span = if fun_args.is_empty() && is_top_most_expr { Some(fun.span) } else { None }; if check_or_fn_call(cx, name, method_span, receiver, arg, Some(lambda), expr.span, fun_span) { return ControlFlow::Break(()); } } ControlFlow::Continue(()) }); } } fn closure_body_returns_empty_to_string(cx: &LateContext<'_>, e: &hir::Expr<'_>) -> bool { if let hir::ExprKind::Closure(&hir::Closure { body, .. }) = e.kind { let body = cx.tcx.hir_body(body); if body.params.is_empty() && let hir::Expr { kind, .. } = &body.value && let hir::ExprKind::MethodCall(hir::PathSegment { ident, .. }, self_arg, [], _) = kind && ident.name == sym::to_string && let hir::Expr { kind, .. } = self_arg && let hir::ExprKind::Lit(lit) = kind && let ast::LitKind::Str(symbol::kw::Empty, _) = lit.node { return true; } } false }