| use clippy_utils::diagnostics::span_lint_and_then; |
| use clippy_utils::msrvs::{self, Msrv}; |
| use clippy_utils::res::{MaybeDef, MaybeResPath}; |
| use clippy_utils::source::{IntoSpan, SpanRangeExt}; |
| use clippy_utils::ty::get_field_by_name; |
| use clippy_utils::visitors::{for_each_expr, for_each_expr_without_closures}; |
| use clippy_utils::{ExprUseNode, expr_use_ctxt, sym}; |
| use core::ops::ControlFlow; |
| use rustc_errors::Applicability; |
| use rustc_hir::{BindingMode, BorrowKind, ByRef, ClosureKind, Expr, ExprKind, Mutability, Node, PatKind}; |
| use rustc_lint::LateContext; |
| use rustc_middle::ty::adjustment::{Adjust, Adjustment, AutoBorrow, AutoBorrowMutability}; |
| use rustc_span::{DUMMY_SP, Span, Symbol}; |
| |
| use super::MANUAL_INSPECT; |
| |
| #[expect(clippy::too_many_lines)] |
| pub(crate) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, arg: &Expr<'_>, name: Symbol, name_span: Span, msrv: Msrv) { |
| if let ExprKind::Closure(c) = arg.kind |
| && matches!(c.kind, ClosureKind::Closure) |
| && let typeck = cx.typeck_results() |
| && let Some(fn_def) = typeck.type_dependent_def(expr.hir_id) |
| && (fn_def.opt_parent(cx).is_diag_item(cx, sym::Iterator) |
| || ((fn_def.opt_parent(cx).opt_impl_ty(cx).is_diag_item(cx, sym::Option) |
| || fn_def.opt_parent(cx).opt_impl_ty(cx).is_diag_item(cx, sym::Result)) |
| && msrv.meets(cx, msrvs::OPTION_RESULT_INSPECT))) |
| && let body = cx.tcx.hir_body(c.body) |
| && let [param] = body.params |
| && let PatKind::Binding(BindingMode(ByRef::No, Mutability::Not), arg_id, _, None) = param.pat.kind |
| && let arg_ty = typeck.node_type(arg_id) |
| && let ExprKind::Block(block, _) = body.value.kind |
| && let Some(final_expr) = block.expr |
| && !block.stmts.is_empty() |
| && final_expr.res_local_id() == Some(arg_id) |
| && typeck.expr_adjustments(final_expr).is_empty() |
| { |
| let mut requires_copy = false; |
| let mut requires_deref = false; |
| |
| // The number of unprocessed return expressions. |
| let mut ret_count = 0u32; |
| |
| // The uses for which processing is delayed until after the visitor. |
| let mut delayed = vec![]; |
| |
| let ctxt = arg.span.ctxt(); |
| let can_lint = for_each_expr_without_closures(block.stmts, |e| { |
| if let ExprKind::Closure(c) = e.kind { |
| // Nested closures don't need to treat returns specially. |
| let _: Option<!> = for_each_expr(cx, cx.tcx.hir_body(c.body).value, |e| { |
| if e.res_local_id() == Some(arg_id) { |
| let (kind, same_ctxt) = check_use(cx, e); |
| match (kind, same_ctxt && e.span.ctxt() == ctxt) { |
| (_, false) | (UseKind::Deref | UseKind::Return(..), true) => { |
| requires_copy = true; |
| requires_deref = true; |
| }, |
| (UseKind::AutoBorrowed, true) => {}, |
| (UseKind::WillAutoDeref, true) => { |
| requires_copy = true; |
| }, |
| (kind, true) => delayed.push(kind), |
| } |
| } |
| ControlFlow::Continue(()) |
| }); |
| } else if matches!(e.kind, ExprKind::Ret(_)) { |
| ret_count += 1; |
| } else if e.res_local_id() == Some(arg_id) { |
| let (kind, same_ctxt) = check_use(cx, e); |
| match (kind, same_ctxt && e.span.ctxt() == ctxt) { |
| (UseKind::Return(..), false) => { |
| return ControlFlow::Break(()); |
| }, |
| (_, false) | (UseKind::Deref, true) => { |
| requires_copy = true; |
| requires_deref = true; |
| }, |
| (UseKind::AutoBorrowed, true) => {}, |
| (UseKind::WillAutoDeref, true) => { |
| requires_copy = true; |
| }, |
| (kind @ UseKind::Return(_), true) => { |
| ret_count -= 1; |
| delayed.push(kind); |
| }, |
| (kind, true) => delayed.push(kind), |
| } |
| } |
| ControlFlow::Continue(()) |
| }) |
| .is_none(); |
| |
| if ret_count != 0 { |
| // A return expression that didn't return the original value was found. |
| return; |
| } |
| |
| let mut edits = Vec::with_capacity(delayed.len() + 3); |
| let mut addr_of_edits = Vec::with_capacity(delayed.len()); |
| for x in delayed { |
| match x { |
| UseKind::Return(s) => edits.push((s.with_leading_whitespace(cx).with_ctxt(s.ctxt()), String::new())), |
| UseKind::Borrowed(s) => { |
| let range = s.map_range(cx, |_, src, range| { |
| let src = src.get(range.clone())?; |
| let trimmed = src.trim_start_matches([' ', '\t', '\n', '\r', '(']); |
| trimmed.starts_with('&').then(|| { |
| let pos = range.start + src.len() - trimmed.len(); |
| pos..pos + 1 |
| }) |
| }); |
| if let Some(range) = range { |
| addr_of_edits.push((range.with_ctxt(s.ctxt()), String::new())); |
| } else { |
| requires_copy = true; |
| requires_deref = true; |
| } |
| }, |
| UseKind::FieldAccess(name, e) => { |
| let Some(mut ty) = get_field_by_name(cx.tcx, arg_ty.peel_refs(), name) else { |
| requires_copy = true; |
| continue; |
| }; |
| let mut prev_expr = e; |
| |
| for (_, parent) in cx.tcx.hir_parent_iter(e.hir_id) { |
| if let Node::Expr(e) = parent { |
| match e.kind { |
| ExprKind::Field(_, name) |
| if let Some(fty) = get_field_by_name(cx.tcx, ty.peel_refs(), name.name) => |
| { |
| ty = fty; |
| prev_expr = e; |
| continue; |
| }, |
| ExprKind::AddrOf(BorrowKind::Ref, ..) => break, |
| _ if matches!( |
| typeck.expr_adjustments(prev_expr).first(), |
| Some(Adjustment { |
| kind: Adjust::Borrow(AutoBorrow::Ref(AutoBorrowMutability::Not)) |
| | Adjust::Deref(_), |
| .. |
| }) |
| ) => |
| { |
| break; |
| }, |
| _ => {}, |
| } |
| } |
| requires_copy |= !cx.type_is_copy_modulo_regions(ty); |
| break; |
| } |
| }, |
| // Already processed uses. |
| UseKind::AutoBorrowed | UseKind::WillAutoDeref | UseKind::Deref => {}, |
| } |
| } |
| |
| if can_lint |
| && (!requires_copy || cx.type_is_copy_modulo_regions(arg_ty)) |
| // This case could be handled, but a fair bit of care would need to be taken. |
| && (!requires_deref || arg_ty.is_freeze(cx.tcx, cx.typing_env())) |
| { |
| if requires_deref { |
| edits.push((param.span.shrink_to_lo(), "&".into())); |
| } else { |
| edits.extend(addr_of_edits); |
| } |
| let edit = match name { |
| sym::map => "inspect", |
| sym::map_err => "inspect_err", |
| _ => return, |
| }; |
| edits.push((name_span, edit.to_string())); |
| edits.push(( |
| final_expr |
| .span |
| .with_leading_whitespace(cx) |
| .with_ctxt(final_expr.span.ctxt()), |
| String::new(), |
| )); |
| let app = if edits.iter().any(|(s, _)| s.from_expansion()) { |
| Applicability::MaybeIncorrect |
| } else { |
| Applicability::MachineApplicable |
| }; |
| span_lint_and_then( |
| cx, |
| MANUAL_INSPECT, |
| name_span, |
| format!("using `{name}` over `{edit}`"), |
| |diag| { |
| diag.multipart_suggestion("try", edits, app); |
| }, |
| ); |
| } |
| } |
| } |
| |
| enum UseKind<'tcx> { |
| AutoBorrowed, |
| WillAutoDeref, |
| Deref, |
| Return(Span), |
| Borrowed(Span), |
| FieldAccess(Symbol, &'tcx Expr<'tcx>), |
| } |
| |
| /// Checks how the value is used, and whether it was used in the same `SyntaxContext`. |
| fn check_use<'tcx>(cx: &LateContext<'tcx>, e: &'tcx Expr<'_>) -> (UseKind<'tcx>, bool) { |
| let use_cx = expr_use_ctxt(cx, e); |
| if use_cx |
| .adjustments |
| .first() |
| .is_some_and(|a| matches!(a.kind, Adjust::Deref(_))) |
| { |
| return (UseKind::AutoBorrowed, use_cx.same_ctxt); |
| } |
| let res = match use_cx.use_node(cx) { |
| ExprUseNode::Return(_) => { |
| if let ExprKind::Ret(Some(e)) = use_cx.node.expect_expr().kind { |
| UseKind::Return(e.span) |
| } else { |
| return (UseKind::Return(DUMMY_SP), false); |
| } |
| }, |
| ExprUseNode::FieldAccess(name) => UseKind::FieldAccess(name.name, use_cx.node.expect_expr()), |
| ExprUseNode::Callee | ExprUseNode::MethodArg(_, _, 0) |
| if use_cx |
| .adjustments |
| .first() |
| .is_some_and(|a| matches!(a.kind, Adjust::Borrow(AutoBorrow::Ref(AutoBorrowMutability::Not)))) => |
| { |
| UseKind::AutoBorrowed |
| }, |
| ExprUseNode::Callee | ExprUseNode::MethodArg(_, _, 0) => UseKind::WillAutoDeref, |
| ExprUseNode::AddrOf(BorrowKind::Ref, _) => UseKind::Borrowed(use_cx.node.expect_expr().span), |
| _ => UseKind::Deref, |
| }; |
| (res, use_cx.same_ctxt) |
| } |