|
| 1 | +use clippy_utils::diagnostics::span_lint_and_then; |
| 2 | +use clippy_utils::sugg::DiagnosticExt; |
| 3 | +use clippy_utils::ty::is_copy; |
| 4 | +use rustc_data_structures::fx::FxIndexMap; |
| 5 | +use rustc_errors::Applicability; |
| 6 | +use rustc_hir::*; |
| 7 | +use rustc_hir_typeck::expr_use_visitor as euv; |
| 8 | +use rustc_infer::infer::TyCtxtInferExt; |
| 9 | +use rustc_lint::{LateContext, LateLintPass}; |
| 10 | +use rustc_middle::mir::FakeReadCause; |
| 11 | +use rustc_middle::ty::{self, UpvarId}; |
| 12 | +use rustc_session::{declare_lint_pass, declare_tool_lint}; |
| 13 | + |
| 14 | +declare_clippy_lint! { |
| 15 | + /// ### What it does |
| 16 | + /// Checks for closures and `async` blocks where the `move` is not necessary. |
| 17 | + /// E.g. all the values are captured by value into the closure / `async` block. |
| 18 | + /// |
| 19 | + /// ### Why is this bad? |
| 20 | + /// Pedantry |
| 21 | + /// ### Example |
| 22 | + /// ```no_run |
| 23 | + /// let a = String::new(); |
| 24 | + /// let closure = move || { |
| 25 | + /// drop(a); |
| 26 | + /// }; |
| 27 | + /// ``` |
| 28 | + /// Use instead: |
| 29 | + /// ```no_run |
| 30 | + /// let a = String::new(); |
| 31 | + /// let closure = || { |
| 32 | + /// drop(a); |
| 33 | + /// }; |
| 34 | + /// ``` |
| 35 | + #[clippy::version = "1.75.0"] |
| 36 | + pub NEEDLESS_MOVE, |
| 37 | + pedantic, |
| 38 | + "checks for needless `move`s on closures / `async` blocks" |
| 39 | +} |
| 40 | + |
| 41 | +declare_lint_pass!(NeedlessMove => [NEEDLESS_MOVE]); |
| 42 | + |
| 43 | +impl NeedlessMove { |
| 44 | + fn check_closure<'tcx>(&self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, closure: &'tcx Closure<'tcx>) { |
| 45 | + let CaptureBy::Value { move_kw } = closure.capture_clause else { |
| 46 | + return; |
| 47 | + }; |
| 48 | + |
| 49 | + if move_kw.is_dummy() { |
| 50 | + // async fn ...() {} convert the body to an `async move {}` block, |
| 51 | + // with a DUMMY_SP for the move_kw |
| 52 | + return; |
| 53 | + } |
| 54 | + |
| 55 | + // Collect moved & borrowed variables from the closure, which the closure *actually* needs. |
| 56 | + let MovedVariablesCtxt { |
| 57 | + moved_vars, |
| 58 | + mut captured_vars, |
| 59 | + } = { |
| 60 | + let mut ctx = MovedVariablesCtxt { |
| 61 | + captured_vars: Default::default(), |
| 62 | + moved_vars: Default::default(), |
| 63 | + }; |
| 64 | + let body = cx.tcx.hir().body(closure.body); |
| 65 | + let infcx = cx.tcx.infer_ctxt().build(); |
| 66 | + euv::ExprUseVisitor::new(&mut ctx, &infcx, closure.def_id, cx.param_env, cx.typeck_results()) |
| 67 | + .consume_body(body); |
| 68 | + ctx |
| 69 | + }; |
| 70 | + |
| 71 | + // Remove the captured vars which were also `move`d. |
| 72 | + // See special case 1. below. |
| 73 | + for (hir_id, _upvars) in moved_vars.iter() { |
| 74 | + let Some(vars) = captured_vars.get_mut(hir_id) else { |
| 75 | + continue; |
| 76 | + }; |
| 77 | + |
| 78 | + if vars |
| 79 | + .iter() |
| 80 | + .any(|(_, hir_borrow_id, _)| is_copy(cx, cx.typeck_results().node_type(*hir_borrow_id))) |
| 81 | + { |
| 82 | + continue; |
| 83 | + } |
| 84 | + |
| 85 | + captured_vars.remove(hir_id); |
| 86 | + } |
| 87 | + |
| 88 | + let lint = |note_msg: &'static str| { |
| 89 | + span_lint_and_then( |
| 90 | + cx, |
| 91 | + NEEDLESS_MOVE, |
| 92 | + expr.span, |
| 93 | + "you seem to use `move`, but the `move` is unnecessary", |
| 94 | + |diag| { |
| 95 | + diag.suggest_remove_item(cx, move_kw, "remove the `move`", Applicability::MachineApplicable); |
| 96 | + diag.note(note_msg); |
| 97 | + }, |
| 98 | + ); |
| 99 | + }; |
| 100 | + |
| 101 | + match (moved_vars.is_empty(), captured_vars.is_empty()) { |
| 102 | + (true, true) => lint("there were no captured variables, so the `move` is unnecessary"), |
| 103 | + (false, true) => { |
| 104 | + lint("there were consumed variables, but no borrowed variables, so the `move` is unnecessary") |
| 105 | + }, |
| 106 | + (_, false) => { |
| 107 | + // captured_vars is not empty, so `move` actually makes a difference and we |
| 108 | + // should not remove it from this closure. |
| 109 | + }, |
| 110 | + } |
| 111 | + } |
| 112 | +} |
| 113 | + |
| 114 | +impl<'tcx> LateLintPass<'tcx> for NeedlessMove { |
| 115 | + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { |
| 116 | + if expr.span.from_expansion() { |
| 117 | + return; |
| 118 | + } |
| 119 | + |
| 120 | + let ExprKind::Closure(closure) = &expr.kind else { |
| 121 | + return; |
| 122 | + }; |
| 123 | + |
| 124 | + self.check_closure(cx, expr, closure); |
| 125 | + } |
| 126 | +} |
| 127 | + |
| 128 | +struct MovedVariablesCtxt { |
| 129 | + // for each base variable, we remember: |
| 130 | + /// The places where it was captured (and consumed, e.g. moved into the closure). |
| 131 | + moved_vars: FxIndexMap<HirId, Vec<UpvarId>>, |
| 132 | + /// The places where it was captured by reference (and not consumed). |
| 133 | + /// If this vector is not empty, then the `move` keyword makes a difference. |
| 134 | + /// There are a few special cases though, such as: |
| 135 | + /// 1. We also handle the case in which there's both a borrow and a move of the |
| 136 | + /// same value into the closure, e.g.: |
| 137 | + /// |
| 138 | + /// ```no_run |
| 139 | + /// let x = String::new(); |
| 140 | + /// let closure = move || { |
| 141 | + /// let s = x.as_str(); // L1 |
| 142 | + /// println!("{s}"); |
| 143 | + /// drop(x); // L2 |
| 144 | + /// }; |
| 145 | + /// ``` |
| 146 | + /// |
| 147 | + /// In this case, the `x` `String` gets moved into the closure (because of L2), but |
| 148 | + /// it is also borrowed prior to that at L1. |
| 149 | + /// |
| 150 | + /// How we handle this is by removing the entries that point to `x` if it was captured |
| 151 | + /// by value (and therefore moved into the closure). |
| 152 | + captured_vars: FxIndexMap<HirId, Vec<(UpvarId, HirId, ty::BorrowKind)>>, |
| 153 | +} |
| 154 | + |
| 155 | +impl MovedVariablesCtxt { |
| 156 | + fn move_common(&mut self, cmt: &euv::PlaceWithHirId<'_>, _: HirId) { |
| 157 | + if let euv::PlaceBase::Upvar(vid) = cmt.place.base { |
| 158 | + self.moved_vars.entry(vid.var_path.hir_id).or_default().push(vid); |
| 159 | + } |
| 160 | + } |
| 161 | + |
| 162 | + fn borrow_common(&mut self, cmt: &euv::PlaceWithHirId<'_>, borrow_hir_id: HirId, bk: ty::BorrowKind) { |
| 163 | + if let euv::PlaceBase::Upvar(vid) = cmt.place.base { |
| 164 | + self.captured_vars |
| 165 | + .entry(vid.var_path.hir_id) |
| 166 | + .or_default() |
| 167 | + .push((vid, borrow_hir_id, bk)); |
| 168 | + } |
| 169 | + } |
| 170 | +} |
| 171 | + |
| 172 | +impl<'tcx> euv::Delegate<'tcx> for MovedVariablesCtxt { |
| 173 | + fn consume(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) { |
| 174 | + self.move_common(cmt, hir_id); |
| 175 | + } |
| 176 | + |
| 177 | + fn borrow(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId, bk: ty::BorrowKind) { |
| 178 | + self.borrow_common(cmt, hir_id, bk); |
| 179 | + } |
| 180 | + |
| 181 | + fn mutate(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: HirId) {} |
| 182 | + |
| 183 | + fn fake_read(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {} |
| 184 | +} |
0 commit comments