Skip to content

Commit 1a3a4d0

Browse files
committed
feat: needless_move lint
An implementation for the lint described in rust-lang#11721
1 parent 9c3a365 commit 1a3a4d0

File tree

7 files changed

+1167
-0
lines changed

7 files changed

+1167
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5335,6 +5335,7 @@ Released 2018-09-13
53355335
[`needless_late_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
53365336
[`needless_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
53375337
[`needless_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_match
5338+
[`needless_move`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_move
53385339
[`needless_option_as_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_option_as_deref
53395340
[`needless_option_take`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_option_take
53405341
[`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
@@ -497,6 +497,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
497497
crate::needless_for_each::NEEDLESS_FOR_EACH_INFO,
498498
crate::needless_if::NEEDLESS_IF_INFO,
499499
crate::needless_late_init::NEEDLESS_LATE_INIT_INFO,
500+
crate::needless_move::NEEDLESS_MOVE_INFO,
500501
crate::needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS_INFO,
501502
crate::needless_pass_by_ref_mut::NEEDLESS_PASS_BY_REF_MUT_INFO,
502503
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
@@ -235,6 +235,7 @@ mod needless_else;
235235
mod needless_for_each;
236236
mod needless_if;
237237
mod needless_late_init;
238+
mod needless_move;
238239
mod needless_parens_on_range_literals;
239240
mod needless_pass_by_ref_mut;
240241
mod needless_pass_by_value;
@@ -1068,6 +1069,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
10681069
store.register_late_pass(|_| Box::new(iter_without_into_iter::IterWithoutIntoIter));
10691070
store.register_late_pass(|_| Box::new(iter_over_hash_type::IterOverHashType));
10701071
store.register_late_pass(|_| Box::new(impl_hash_with_borrow_str_and_bytes::ImplHashWithBorrowStrBytes));
1072+
store.register_late_pass(|_| Box::new(needless_move::NeedlessMove));
10711073
// add lints here, do not remove this comment, it's used in `new_lint`
10721074
}
10731075

clippy_lints/src/needless_move.rs

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

0 commit comments

Comments
 (0)