Skip to content

Commit d1b3fdf

Browse files
committed
feat: needless_move lint
An implementation for the lint described in rust-lang#11721
1 parent 6d9516a commit d1b3fdf

File tree

7 files changed

+827
-0
lines changed

7 files changed

+827
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5274,6 +5274,7 @@ Released 2018-09-13
52745274
[`needless_late_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
52755275
[`needless_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
52765276
[`needless_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_match
5277+
[`needless_move`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_move
52775278
[`needless_option_as_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_option_as_deref
52785279
[`needless_option_take`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_option_take
52795280
[`needless_parens_on_range_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_parens_on_range_literals

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
496496
crate::needless_for_each::NEEDLESS_FOR_EACH_INFO,
497497
crate::needless_if::NEEDLESS_IF_INFO,
498498
crate::needless_late_init::NEEDLESS_LATE_INIT_INFO,
499+
crate::needless_move::NEEDLESS_MOVE_INFO,
499500
crate::needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS_INFO,
500501
crate::needless_pass_by_ref_mut::NEEDLESS_PASS_BY_REF_MUT_INFO,
501502
crate::needless_pass_by_value::NEEDLESS_PASS_BY_VALUE_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ mod needless_else;
234234
mod needless_for_each;
235235
mod needless_if;
236236
mod needless_late_init;
237+
mod needless_move;
237238
mod needless_parens_on_range_literals;
238239
mod needless_pass_by_ref_mut;
239240
mod needless_pass_by_value;
@@ -1066,6 +1067,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
10661067
});
10671068
store.register_late_pass(move |_| Box::new(manual_hash_one::ManualHashOne::new(msrv())));
10681069
store.register_late_pass(|_| Box::new(iter_without_into_iter::IterWithoutIntoIter));
1070+
store.register_late_pass(|_| Box::new(needless_move::NeedlessMove));
10691071
// add lints here, do not remove this comment, it's used in `new_lint`
10701072
}
10711073

clippy_lints/src/needless_move.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
// Collect moved & borrowed variables from the closure, which the closure *actually* needs.
50+
let MovedVariablesCtxt {
51+
moved_vars,
52+
mut captured_vars,
53+
} = {
54+
let mut ctx = MovedVariablesCtxt {
55+
captured_vars: Default::default(),
56+
moved_vars: Default::default(),
57+
};
58+
let body = cx.tcx.hir().body(closure.body);
59+
let infcx = cx.tcx.infer_ctxt().build();
60+
euv::ExprUseVisitor::new(&mut ctx, &infcx, closure.def_id, cx.param_env, cx.typeck_results())
61+
.consume_body(body);
62+
ctx
63+
};
64+
65+
// Remove the captured vars which were also `move`d.
66+
// See special case 1. below.
67+
for (hir_id, _upvars) in moved_vars.iter() {
68+
let Some(vars) = captured_vars.get_mut(hir_id) else {
69+
continue;
70+
};
71+
72+
if vars
73+
.iter()
74+
.any(|(_, hir_borrow_id, _)| is_copy(cx, cx.typeck_results().node_type(*hir_borrow_id)))
75+
{
76+
continue;
77+
}
78+
79+
captured_vars.remove(hir_id);
80+
}
81+
82+
let lint = |note_msg: &'static str| {
83+
span_lint_and_then(
84+
cx,
85+
NEEDLESS_MOVE,
86+
expr.span,
87+
"you seem to use `move`, but the `move` is unnecessary",
88+
|diag| {
89+
diag.suggest_remove_item(cx, move_kw, "remove the `move`", Applicability::MachineApplicable);
90+
diag.note(note_msg);
91+
},
92+
);
93+
};
94+
95+
match (moved_vars.is_empty(), captured_vars.is_empty()) {
96+
(true, true) => lint("there were no captured variables, so the `move` is unnecessary"),
97+
(false, true) => lint("there were consumed variables, but no borrowed variables, so the `move` is unnecessary"),
98+
(_, false) => {
99+
// captured_vars is not empty, so `move` actually makes a difference and we
100+
// should not remove it from this closure.
101+
},
102+
}
103+
}
104+
}
105+
106+
impl<'tcx> LateLintPass<'tcx> for NeedlessMove {
107+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
108+
if expr.span.from_expansion() {
109+
return;
110+
}
111+
112+
let ExprKind::Closure(closure) = &expr.kind else {
113+
return;
114+
};
115+
116+
self.check_closure(cx, expr, closure);
117+
}
118+
}
119+
120+
struct MovedVariablesCtxt {
121+
// for each base variable, we remember:
122+
/// The places where it was captured (and consumed, e.g. moved into the closure).
123+
moved_vars: FxIndexMap<HirId, Vec<UpvarId>>,
124+
/// The places where it was captured by reference (and not consumed).
125+
/// If this vector is not empty, then the `move` keyword makes a difference.
126+
/// There are a few special cases though, such as:
127+
/// 1. We also handle the case in which there's both a borrow and a move of the
128+
/// same value into the closure, e.g.:
129+
///
130+
/// ```no_run
131+
/// let x = String::new();
132+
/// let closure = move || {
133+
/// let s = x.as_str(); // L1
134+
/// println!("{s}");
135+
/// drop(x); // L2
136+
/// };
137+
/// ```
138+
///
139+
/// In this case, the `x` `String` gets moved into the closure (because of L2), but
140+
/// it is also borrowed prior to that at L1.
141+
///
142+
/// How we handle this is by removing the entries that point to `x` if it was captured
143+
/// by value (and therefore moved into the closure).
144+
captured_vars: FxIndexMap<HirId, Vec<(UpvarId, HirId, ty::BorrowKind)>>,
145+
}
146+
147+
impl MovedVariablesCtxt {
148+
fn move_common(&mut self, cmt: &euv::PlaceWithHirId<'_>, _: HirId) {
149+
if let euv::PlaceBase::Upvar(vid) = cmt.place.base {
150+
self.moved_vars.entry(vid.var_path.hir_id).or_default().push(vid);
151+
}
152+
}
153+
154+
fn borrow_common(&mut self, cmt: &euv::PlaceWithHirId<'_>, borrow_hir_id: HirId, bk: ty::BorrowKind) {
155+
if let euv::PlaceBase::Upvar(vid) = cmt.place.base {
156+
self.captured_vars
157+
.entry(vid.var_path.hir_id)
158+
.or_default()
159+
.push((vid, borrow_hir_id, bk));
160+
}
161+
}
162+
}
163+
164+
impl<'tcx> euv::Delegate<'tcx> for MovedVariablesCtxt {
165+
fn consume(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
166+
self.move_common(cmt, hir_id);
167+
}
168+
169+
fn borrow(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId, bk: ty::BorrowKind) {
170+
self.borrow_common(cmt, hir_id, bk);
171+
}
172+
173+
fn mutate(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: HirId) {}
174+
175+
fn fake_read(&mut self, _: &rustc_hir_typeck::expr_use_visitor::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {}
176+
}

0 commit comments

Comments
 (0)