|
| 1 | +//! This lint works by looking at the min_captures that `rustc` uses, |
| 2 | +//! and checks that for the expression `capture_kind_expr_id`, it would |
| 3 | +//! actually borrow normally, if it weren't for the move keyword. |
| 4 | +//! |
| 5 | +//! In such cases, the move keyword changes the semantics of the code (e.g. |
| 6 | +//! without it that capture would be a normal by reference capture, but with |
| 7 | +//! move it would get captured by value, and therefore we do not remove the `move` |
| 8 | +//! keyword from the closure). |
| 9 | +//! |
| 10 | +//! A small caveat for the approach above: |
| 11 | +//! There's both a borrow and a move of the same value into the closure, e.g.: |
| 12 | +//! |
| 13 | +//! ```no_run |
| 14 | +//! let x = String::new(); |
| 15 | +//! let closure = move || { |
| 16 | +//! let s = x.as_str(); // L1 |
| 17 | +//! println!("{s}"); |
| 18 | +//! drop(x); // L2 |
| 19 | +//! }; |
| 20 | +//! ``` |
| 21 | +//! |
| 22 | +//! In this case, the `x` `String` gets moved into the closure (because of L2), but |
| 23 | +//! it is also borrowed prior to that at L1. |
| 24 | +//! |
| 25 | +//! `rustc`, in the presence of the `move` keyword automatically assumes that if |
| 26 | +//! it borrows a value, it's going to move it into the closure (in the example above at L1, |
| 27 | +//! so capture_kind_expr_id would point to the use on L1), but here, in the case |
| 28 | +//! of this lint, we should behave a little differently, namely we should first look |
| 29 | +//! at all the locations where a place is captured, and if any of them actually moves it, |
| 30 | +//! the closure would consume the value. |
| 31 | +//! |
| 32 | +//! The logic for this is handled in `MovedVariablesCtxt::get_required_kind`, where we |
| 33 | +//! try to infer the actual min capture kind needed. |
| 34 | +
|
| 35 | +use clippy_utils::diagnostics::span_lint_and_then; |
| 36 | +use clippy_utils::sugg::DiagnosticExt; |
| 37 | +use rustc_errors::Applicability; |
| 38 | +use rustc_hir::{CaptureBy, Closure, Expr, ExprKind, HirId}; |
| 39 | +use rustc_hir_typeck::expr_use_visitor as euv; |
| 40 | +use rustc_infer::infer::TyCtxtInferExt; |
| 41 | +use rustc_lint::{LateContext, LateLintPass}; |
| 42 | +use rustc_middle::mir::FakeReadCause; |
| 43 | +use rustc_middle::ty; |
| 44 | +use rustc_middle::ty::UpvarCapture; |
| 45 | +use rustc_session::{declare_lint_pass, declare_tool_lint}; |
| 46 | + |
| 47 | +declare_clippy_lint! { |
| 48 | + /// ### What it does |
| 49 | + /// Checks for closures and `async` blocks where the `move` is not necessary. |
| 50 | + /// E.g. all the values are captured by value into the closure / `async` block. |
| 51 | + /// |
| 52 | + /// ### Why is this bad? |
| 53 | + /// Pedantry |
| 54 | + /// ### Example |
| 55 | + /// ```no_run |
| 56 | + /// let a = String::new(); |
| 57 | + /// let closure = move || { |
| 58 | + /// drop(a); |
| 59 | + /// }; |
| 60 | + /// ``` |
| 61 | + /// Use instead: |
| 62 | + /// ```no_run |
| 63 | + /// let a = String::new(); |
| 64 | + /// let closure = || { |
| 65 | + /// drop(a); |
| 66 | + /// }; |
| 67 | + /// ``` |
| 68 | + #[clippy::version = "1.76.0"] |
| 69 | + pub NEEDLESS_MOVE, |
| 70 | + pedantic, |
| 71 | + "checks for needless `move`s on closures / `async` blocks" |
| 72 | +} |
| 73 | + |
| 74 | +declare_lint_pass!(NeedlessMove => [NEEDLESS_MOVE]); |
| 75 | + |
| 76 | +impl NeedlessMove { |
| 77 | + fn check_closure<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, closure: &'tcx Closure<'tcx>) { |
| 78 | + let CaptureBy::Value { move_kw } = closure.capture_clause else { |
| 79 | + return; |
| 80 | + }; |
| 81 | + |
| 82 | + if move_kw.is_dummy() { |
| 83 | + // async fn ...() {} convert the body to an `async move {}` block, |
| 84 | + // with a DUMMY_SP for the move_kw |
| 85 | + return; |
| 86 | + } |
| 87 | + |
| 88 | + // Collect moved & borrowed variables from the closure, which the closure *actually* needs. |
| 89 | + let ctx = { |
| 90 | + let mut ctx = MovedVariablesCtxt::default(); |
| 91 | + let body = cx.tcx.hir().body(closure.body); |
| 92 | + let infcx = cx.tcx.infer_ctxt().build(); |
| 93 | + euv::ExprUseVisitor::new(&mut ctx, &infcx, closure.def_id, cx.param_env, cx.typeck_results()) |
| 94 | + .consume_body(body); |
| 95 | + ctx |
| 96 | + }; |
| 97 | + |
| 98 | + enum LintResult { |
| 99 | + /// do not remove the `move` keyword. |
| 100 | + NeedMove, |
| 101 | + Consumed, |
| 102 | + NothingCaptured, |
| 103 | + } |
| 104 | + |
| 105 | + let mut lint_result = LintResult::NothingCaptured; |
| 106 | + |
| 107 | + for captured_place in cx.typeck_results().closure_min_captures_flattened(closure.def_id) { |
| 108 | + let place = &captured_place.place; |
| 109 | + if let Some(ck_expr_id) = captured_place.info.capture_kind_expr_id { |
| 110 | + let required_ck = ctx.get_required_kind(place, ck_expr_id); |
| 111 | + match required_ck { |
| 112 | + UpvarCapture::ByRef(_) => { |
| 113 | + // no matter what the old `lint_result` is, we keep the move. |
| 114 | + lint_result = LintResult::NeedMove; |
| 115 | + }, |
| 116 | + UpvarCapture::ByValue => { |
| 117 | + lint_result = match lint_result { |
| 118 | + LintResult::NothingCaptured | LintResult::Consumed => LintResult::Consumed, |
| 119 | + LintResult::NeedMove => LintResult::NeedMove, |
| 120 | + } |
| 121 | + }, |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + let lint = |note_msg: &'static str| { |
| 127 | + span_lint_and_then( |
| 128 | + cx, |
| 129 | + NEEDLESS_MOVE, |
| 130 | + expr.span, |
| 131 | + "you seem to use `move`, but the `move` is unnecessary", |
| 132 | + |diag| { |
| 133 | + diag.suggest_remove_item(cx, move_kw, "remove the `move`", Applicability::MachineApplicable); |
| 134 | + diag.note(note_msg); |
| 135 | + }, |
| 136 | + ); |
| 137 | + }; |
| 138 | + |
| 139 | + match lint_result { |
| 140 | + LintResult::NothingCaptured => { |
| 141 | + lint("there were no captured variables, so the `move` is unnecessary"); |
| 142 | + }, |
| 143 | + LintResult::Consumed => { |
| 144 | + lint("there were consumed variables, but no borrowed variables, so the `move` is unnecessary"); |
| 145 | + }, |
| 146 | + LintResult::NeedMove => { |
| 147 | + // there was a value which would be borrowed if it weren't for the move keyword, |
| 148 | + // so we should keep it, as removing it would change semantics. |
| 149 | + }, |
| 150 | + } |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +impl<'tcx> LateLintPass<'tcx> for NeedlessMove { |
| 155 | + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { |
| 156 | + if expr.span.from_expansion() { |
| 157 | + return; |
| 158 | + } |
| 159 | + |
| 160 | + let ExprKind::Closure(closure) = &expr.kind else { |
| 161 | + return; |
| 162 | + }; |
| 163 | + |
| 164 | + Self::check_closure(cx, expr, closure); |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +#[derive(Debug, Default)] |
| 169 | +struct MovedVariablesCtxt<'tcx> { |
| 170 | + // for each base variable, we remember: |
| 171 | + /// The places where it was captured (and consumed, e.g. moved into the closure). |
| 172 | + moved: Vec<(euv::Place<'tcx>, HirId)>, |
| 173 | + /// The places where it was captured by reference (and not consumed). |
| 174 | + captured: Vec<(euv::Place<'tcx>, HirId, ty::BorrowKind)>, |
| 175 | +} |
| 176 | + |
| 177 | +impl<'tcx> MovedVariablesCtxt<'tcx> { |
| 178 | + fn move_common(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) { |
| 179 | + if let euv::PlaceBase::Upvar(_) = cmt.place.base { |
| 180 | + self.moved.push((cmt.place.clone(), hir_id)); |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + fn borrow_common(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, borrow_hir_id: HirId, bk: ty::BorrowKind) { |
| 185 | + if let euv::PlaceBase::Upvar(_) = cmt.place.base { |
| 186 | + self.captured.push((cmt.place.clone(), borrow_hir_id, bk)); |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + fn get_required_kind(&self, place: &euv::Place<'tcx>, ref_hir_id: HirId) -> UpvarCapture { |
| 191 | + match self |
| 192 | + .moved |
| 193 | + .iter() |
| 194 | + .any(|upvar_ref| upvar_ref.0 == *place || upvar_ref.1 == ref_hir_id) |
| 195 | + { |
| 196 | + true => UpvarCapture::ByValue, |
| 197 | + false => self |
| 198 | + .captured |
| 199 | + .iter() |
| 200 | + .find(|upvar_ref| upvar_ref.1 == ref_hir_id) |
| 201 | + .map(|it| UpvarCapture::ByRef(it.2)) |
| 202 | + .unwrap(), |
| 203 | + } |
| 204 | + } |
| 205 | +} |
| 206 | + |
| 207 | +impl<'tcx> euv::Delegate<'tcx> for MovedVariablesCtxt<'tcx> { |
| 208 | + fn consume(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) { |
| 209 | + self.move_common(cmt, hir_id); |
| 210 | + } |
| 211 | + |
| 212 | + fn borrow(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId, bk: ty::BorrowKind) { |
| 213 | + self.borrow_common(cmt, hir_id, bk); |
| 214 | + } |
| 215 | + |
| 216 | + fn mutate(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) { |
| 217 | + self.borrow(cmt, hir_id, ty::BorrowKind::MutBorrow); |
| 218 | + } |
| 219 | + |
| 220 | + fn fake_read(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {} |
| 221 | +} |
0 commit comments