Skip to content

Commit 97d5914

Browse files
committed
feat: needless_move lint
An implementation for the lint described in rust-lang#11721
1 parent e8e9510 commit 97d5914

File tree

7 files changed

+848
-0
lines changed

7 files changed

+848
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5334,6 +5334,7 @@ Released 2018-09-13
53345334
[`needless_late_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
53355335
[`needless_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
53365336
[`needless_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_match
5337+
[`needless_move`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_move
53375338
[`needless_option_as_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_option_as_deref
53385339
[`needless_option_take`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_option_take
53395340
[`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

+1
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

+2
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_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
10661067
store.register_late_pass(move |_| Box::new(manual_hash_one::ManualHashOne::new(msrv())));
10671068
store.register_late_pass(|_| Box::new(iter_without_into_iter::IterWithoutIntoIter));
10681069
store.register_late_pass(|_| Box::new(iter_over_hash_type::IterOverHashType));
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

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
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

Comments
 (0)