| use std::ops::ControlFlow; |
| |
| use super::WHILE_LET_ON_ITERATOR; |
| use clippy_utils::diagnostics::span_lint_and_sugg; |
| use clippy_utils::res::{MaybeDef, MaybeTypeckRes}; |
| use clippy_utils::source::snippet_with_applicability; |
| use clippy_utils::visitors::is_res_used; |
| use clippy_utils::{as_some_pattern, get_enclosing_loop_or_multi_call_closure, higher, is_refutable}; |
| use rustc_errors::Applicability; |
| use rustc_hir::def::Res; |
| use rustc_hir::intravisit::{Visitor, walk_expr}; |
| use rustc_hir::{Closure, Expr, ExprKind, HirId, LetStmt, Mutability, UnOp}; |
| use rustc_lint::LateContext; |
| use rustc_middle::hir::nested_filter::OnlyBodies; |
| use rustc_middle::ty::adjustment::Adjust; |
| use rustc_span::Symbol; |
| use rustc_span::symbol::sym; |
| |
| pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) { |
| if let Some(higher::WhileLet { if_then, let_pat, let_expr, label, .. }) = higher::WhileLet::hir(expr) |
| // check for `Some(..)` pattern |
| && let Some(some_pat) = as_some_pattern(cx, let_pat) |
| // check for call to `Iterator::next` |
| && let ExprKind::MethodCall(method_name, iter_expr, [], _) = let_expr.kind |
| && method_name.ident.name == sym::next |
| && cx.ty_based_def(let_expr).opt_parent(cx).is_diag_item(cx, sym::Iterator) |
| && let Some(iter_expr_struct) = try_parse_iter_expr(cx, iter_expr) |
| // get the loop containing the match expression |
| && !uses_iter(cx, &iter_expr_struct, if_then) |
| { |
| let mut applicability = Applicability::MachineApplicable; |
| |
| let loop_label = label.map_or(String::new(), |l| format!("{}: ", l.ident.name)); |
| |
| let loop_var = if let Some(some_pat) = some_pat.first() { |
| if is_refutable(cx, some_pat) { |
| // Refutable patterns don't work with for loops. |
| return; |
| } |
| snippet_with_applicability(cx, some_pat.span, "..", &mut applicability) |
| } else { |
| "_".into() |
| }; |
| |
| // If the iterator is a field or the iterator is accessed after the loop is complete it needs to be |
| // passed by reference. TODO: If the struct can be partially moved from and the struct isn't used |
| // afterwards a mutable borrow of a field isn't necessary. |
| let iterator = snippet_with_applicability(cx, iter_expr.span, "_", &mut applicability); |
| let iterator_by_ref = if cx.typeck_results().expr_ty(iter_expr).ref_mutability() == Some(Mutability::Mut) |
| || !iter_expr_struct.can_move |
| || !iter_expr_struct.fields.is_empty() |
| || needs_mutable_borrow(cx, &iter_expr_struct, expr) |
| { |
| make_iterator_snippet(cx, iter_expr, &iterator) |
| } else { |
| iterator.into_owned() |
| }; |
| |
| span_lint_and_sugg( |
| cx, |
| WHILE_LET_ON_ITERATOR, |
| expr.span.with_hi(let_expr.span.hi()), |
| "this loop could be written as a `for` loop", |
| "try", |
| format!("{loop_label}for {loop_var} in {iterator_by_ref}"), |
| applicability, |
| ); |
| } |
| } |
| |
| #[derive(Debug)] |
| struct IterExpr { |
| /// The fields used, in order of child to parent. |
| fields: Vec<Symbol>, |
| /// The path being used. |
| path: Res, |
| /// Whether or not the iterator can be moved. |
| can_move: bool, |
| } |
| |
| /// Parses any expression to find out which field of which variable is used. Will return `None` if |
| /// the expression might have side effects. |
| fn try_parse_iter_expr(cx: &LateContext<'_>, mut e: &Expr<'_>) -> Option<IterExpr> { |
| let mut fields = Vec::new(); |
| let mut can_move = true; |
| loop { |
| if cx |
| .typeck_results() |
| .expr_adjustments(e) |
| .iter() |
| .any(|a| matches!(a.kind, Adjust::Deref(Some(..)))) |
| { |
| // Custom deref impls need to borrow the whole value as it's captured by reference |
| can_move = false; |
| fields.clear(); |
| } |
| match e.kind { |
| ExprKind::Path(ref path) => { |
| break Some(IterExpr { |
| fields, |
| path: cx.qpath_res(path, e.hir_id), |
| can_move, |
| }); |
| }, |
| ExprKind::Field(base, name) => { |
| fields.push(name.name); |
| e = base; |
| }, |
| // Dereferencing a pointer has no side effects and doesn't affect which field is being used. |
| ExprKind::Unary(UnOp::Deref, base) if cx.typeck_results().expr_ty(base).is_ref() => e = base, |
| |
| // Shouldn't have side effects, but there's no way to trace which field is used. So forget which fields have |
| // already been seen. |
| ExprKind::Index(base, idx, _) if !idx.can_have_side_effects() => { |
| can_move = false; |
| fields.clear(); |
| e = base; |
| }, |
| ExprKind::Unary(UnOp::Deref, base) => { |
| can_move = false; |
| fields.clear(); |
| e = base; |
| }, |
| |
| // No effect and doesn't affect which field is being used. |
| ExprKind::DropTemps(base) | ExprKind::AddrOf(_, _, base) | ExprKind::Type(base, _) => e = base, |
| _ => break None, |
| } |
| } |
| } |
| |
| fn is_expr_same_field(cx: &LateContext<'_>, mut e: &Expr<'_>, mut fields: &[Symbol], path_res: Res) -> bool { |
| loop { |
| match (&e.kind, fields) { |
| (&ExprKind::Field(base, name), [head_field, tail_fields @ ..]) if name.name == *head_field => { |
| e = base; |
| fields = tail_fields; |
| }, |
| (ExprKind::Path(path), []) => { |
| break cx.qpath_res(path, e.hir_id) == path_res; |
| }, |
| (&(ExprKind::DropTemps(base) | ExprKind::AddrOf(_, _, base) | ExprKind::Type(base, _)), _) => e = base, |
| _ => break false, |
| } |
| } |
| } |
| |
| /// Checks if the given expression is the same field as, is a child of, or is the parent of the |
| /// given field. Used to check if the expression can be used while the given field is borrowed |
| /// mutably. e.g. if checking for `x.y`, then `x.y`, `x.y.z`, and `x` will all return true, but |
| /// `x.z`, and `y` will return false. |
| fn is_expr_same_child_or_parent_field(cx: &LateContext<'_>, expr: &Expr<'_>, fields: &[Symbol], path_res: Res) -> bool { |
| match expr.kind { |
| ExprKind::Field(base, name) => { |
| if let Some((head_field, tail_fields)) = fields.split_first() { |
| if name.name == *head_field && is_expr_same_field(cx, base, tail_fields, path_res) { |
| return true; |
| } |
| // Check if the expression is a parent field |
| let mut fields_iter = tail_fields.iter(); |
| while let Some(field) = fields_iter.next() { |
| if *field == name.name && is_expr_same_field(cx, base, fields_iter.as_slice(), path_res) { |
| return true; |
| } |
| } |
| } |
| |
| // Check if the expression is a child field. |
| let mut e = base; |
| loop { |
| match e.kind { |
| ExprKind::Field(..) if is_expr_same_field(cx, e, fields, path_res) => break true, |
| ExprKind::Field(base, _) | ExprKind::DropTemps(base) | ExprKind::Type(base, _) => e = base, |
| ExprKind::Path(ref path) if fields.is_empty() => { |
| break cx.qpath_res(path, e.hir_id) == path_res; |
| }, |
| _ => break false, |
| } |
| } |
| }, |
| // If the path matches, this is either an exact match, or the expression is a parent of the field. |
| ExprKind::Path(ref path) => cx.qpath_res(path, expr.hir_id) == path_res, |
| ExprKind::DropTemps(base) | ExprKind::Type(base, _) | ExprKind::AddrOf(_, _, base) => { |
| is_expr_same_child_or_parent_field(cx, base, fields, path_res) |
| }, |
| _ => false, |
| } |
| } |
| |
| /// Strips off all field and path expressions. This will return true if a field or path has been |
| /// skipped. Used to skip them after failing to check for equality. |
| fn skip_fields_and_path<'tcx>(expr: &'tcx Expr<'_>) -> (Option<&'tcx Expr<'tcx>>, bool) { |
| let mut e = expr; |
| let e = loop { |
| match e.kind { |
| ExprKind::Field(base, _) | ExprKind::DropTemps(base) | ExprKind::Type(base, _) => e = base, |
| ExprKind::Path(_) => return (None, true), |
| _ => break e, |
| } |
| }; |
| (Some(e), e.hir_id != expr.hir_id) |
| } |
| |
| /// Checks if the given expression uses the iterator. |
| fn uses_iter<'tcx>(cx: &LateContext<'tcx>, iter_expr: &IterExpr, container: &'tcx Expr<'_>) -> bool { |
| struct V<'a, 'b, 'tcx> { |
| cx: &'a LateContext<'tcx>, |
| iter_expr: &'b IterExpr, |
| } |
| impl<'tcx> Visitor<'tcx> for V<'_, '_, 'tcx> { |
| type Result = ControlFlow<()>; |
| fn visit_expr(&mut self, e: &'tcx Expr<'_>) -> Self::Result { |
| if is_expr_same_child_or_parent_field(self.cx, e, &self.iter_expr.fields, self.iter_expr.path) { |
| ControlFlow::Break(()) |
| } else if let (e, true) = skip_fields_and_path(e) { |
| if let Some(e) = e { |
| self.visit_expr(e) |
| } else { |
| ControlFlow::Continue(()) |
| } |
| } else if let ExprKind::Closure(&Closure { body: id, .. }) = e.kind { |
| if is_res_used(self.cx, self.iter_expr.path, id) { |
| ControlFlow::Break(()) |
| } else { |
| ControlFlow::Continue(()) |
| } |
| } else { |
| walk_expr(self, e) |
| } |
| } |
| } |
| |
| let mut v = V { cx, iter_expr }; |
| v.visit_expr(container).is_break() |
| } |
| |
| #[expect(clippy::too_many_lines)] |
| fn needs_mutable_borrow(cx: &LateContext<'_>, iter_expr: &IterExpr, loop_expr: &Expr<'_>) -> bool { |
| struct AfterLoopVisitor<'a, 'b, 'tcx> { |
| cx: &'a LateContext<'tcx>, |
| iter_expr: &'b IterExpr, |
| loop_id: HirId, |
| after_loop: bool, |
| } |
| impl<'tcx> Visitor<'tcx> for AfterLoopVisitor<'_, '_, 'tcx> { |
| type NestedFilter = OnlyBodies; |
| type Result = ControlFlow<()>; |
| fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { |
| self.cx.tcx |
| } |
| |
| fn visit_expr(&mut self, e: &'tcx Expr<'_>) -> Self::Result { |
| if self.after_loop { |
| if is_expr_same_child_or_parent_field(self.cx, e, &self.iter_expr.fields, self.iter_expr.path) { |
| ControlFlow::Break(()) |
| } else if let (e, true) = skip_fields_and_path(e) { |
| if let Some(e) = e { |
| self.visit_expr(e) |
| } else { |
| ControlFlow::Continue(()) |
| } |
| } else if let ExprKind::Closure(&Closure { body: id, .. }) = e.kind { |
| if is_res_used(self.cx, self.iter_expr.path, id) { |
| ControlFlow::Break(()) |
| } else { |
| ControlFlow::Continue(()) |
| } |
| } else { |
| walk_expr(self, e) |
| } |
| } else if self.loop_id == e.hir_id { |
| self.after_loop = true; |
| ControlFlow::Continue(()) |
| } else { |
| walk_expr(self, e) |
| } |
| } |
| } |
| |
| struct NestedLoopVisitor<'a, 'b, 'tcx> { |
| cx: &'a LateContext<'tcx>, |
| iter_expr: &'b IterExpr, |
| local_id: HirId, |
| loop_id: HirId, |
| after_loop: bool, |
| found_local: bool, |
| used_after: bool, |
| } |
| impl<'tcx> Visitor<'tcx> for NestedLoopVisitor<'_, '_, 'tcx> { |
| type NestedFilter = OnlyBodies; |
| fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt { |
| self.cx.tcx |
| } |
| |
| fn visit_local(&mut self, l: &'tcx LetStmt<'_>) { |
| if !self.after_loop { |
| l.pat.each_binding_or_first(&mut |_, id, _, _| { |
| if id == self.local_id { |
| self.found_local = true; |
| } |
| }); |
| } |
| if let Some(e) = l.init { |
| self.visit_expr(e); |
| } |
| } |
| |
| fn visit_expr(&mut self, e: &'tcx Expr<'_>) { |
| if self.used_after { |
| return; |
| } |
| if self.after_loop { |
| if is_expr_same_child_or_parent_field(self.cx, e, &self.iter_expr.fields, self.iter_expr.path) { |
| self.used_after = true; |
| } else if let (e, true) = skip_fields_and_path(e) { |
| if let Some(e) = e { |
| self.visit_expr(e); |
| } |
| } else if let ExprKind::Closure(&Closure { body: id, .. }) = e.kind { |
| self.used_after = is_res_used(self.cx, self.iter_expr.path, id); |
| } else { |
| walk_expr(self, e); |
| } |
| } else if e.hir_id == self.loop_id { |
| self.after_loop = true; |
| } else { |
| walk_expr(self, e); |
| } |
| } |
| } |
| |
| if let Some(e) = get_enclosing_loop_or_multi_call_closure(cx, loop_expr) { |
| let Res::Local(local_id) = iter_expr.path else { |
| return true; |
| }; |
| let mut v = NestedLoopVisitor { |
| cx, |
| iter_expr, |
| local_id, |
| loop_id: loop_expr.hir_id, |
| after_loop: false, |
| found_local: false, |
| used_after: false, |
| }; |
| v.visit_expr(e); |
| v.used_after || !v.found_local |
| } else { |
| let mut v = AfterLoopVisitor { |
| cx, |
| iter_expr, |
| loop_id: loop_expr.hir_id, |
| after_loop: false, |
| }; |
| v.visit_expr(cx.tcx.hir_body(cx.enclosing_body.unwrap()).value) |
| .is_break() |
| } |
| } |
| |
| /// Constructs the transformed iterator expression for the suggestion. |
| /// Returns `iterator.by_ref()` unless the last deref adjustment targets an unsized type, |
| /// in which case it applies all derefs (e.g., `&mut **iterator` or `&mut ***iterator`). |
| fn make_iterator_snippet<'tcx>(cx: &LateContext<'tcx>, iter_expr: &Expr<'tcx>, iterator: &str) -> String { |
| if let Some((n, adjust)) = cx |
| .typeck_results() |
| .expr_adjustments(iter_expr) |
| .iter() |
| .take_while(|x| matches!(x.kind, Adjust::Deref(_))) |
| .enumerate() |
| .last() |
| && !adjust.target.is_sized(cx.tcx, cx.typing_env()) |
| { |
| format!("&mut {:*<n$}{iterator}", '*', n = n + 1) |
| } else { |
| format!("{iterator}.by_ref()") |
| } |
| } |