Skip to content

Commit 417e6db

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

File tree

7 files changed

+829
-0
lines changed

7 files changed

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

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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) => {
98+
lint("there were consumed variables, but no borrowed variables, so the `move` is unnecessary")
99+
},
100+
(_, false) => {
101+
// captured_vars is not empty, so `move` actually makes a difference and we
102+
// should not remove it from this closure.
103+
},
104+
}
105+
}
106+
}
107+
108+
impl<'tcx> LateLintPass<'tcx> for NeedlessMove {
109+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
110+
if expr.span.from_expansion() {
111+
return;
112+
}
113+
114+
let ExprKind::Closure(closure) = &expr.kind else {
115+
return;
116+
};
117+
118+
self.check_closure(cx, expr, closure);
119+
}
120+
}
121+
122+
struct MovedVariablesCtxt {
123+
// for each base variable, we remember:
124+
/// The places where it was captured (and consumed, e.g. moved into the closure).
125+
moved_vars: FxIndexMap<HirId, Vec<UpvarId>>,
126+
/// The places where it was captured by reference (and not consumed).
127+
/// If this vector is not empty, then the `move` keyword makes a difference.
128+
/// There are a few special cases though, such as:
129+
/// 1. We also handle the case in which there's both a borrow and a move of the
130+
/// same value into the closure, e.g.:
131+
///
132+
/// ```no_run
133+
/// let x = String::new();
134+
/// let closure = move || {
135+
/// let s = x.as_str(); // L1
136+
/// println!("{s}");
137+
/// drop(x); // L2
138+
/// };
139+
/// ```
140+
///
141+
/// In this case, the `x` `String` gets moved into the closure (because of L2), but
142+
/// it is also borrowed prior to that at L1.
143+
///
144+
/// How we handle this is by removing the entries that point to `x` if it was captured
145+
/// by value (and therefore moved into the closure).
146+
captured_vars: FxIndexMap<HirId, Vec<(UpvarId, HirId, ty::BorrowKind)>>,
147+
}
148+
149+
impl MovedVariablesCtxt {
150+
fn move_common(&mut self, cmt: &euv::PlaceWithHirId<'_>, _: HirId) {
151+
if let euv::PlaceBase::Upvar(vid) = cmt.place.base {
152+
self.moved_vars.entry(vid.var_path.hir_id).or_default().push(vid);
153+
}
154+
}
155+
156+
fn borrow_common(&mut self, cmt: &euv::PlaceWithHirId<'_>, borrow_hir_id: HirId, bk: ty::BorrowKind) {
157+
if let euv::PlaceBase::Upvar(vid) = cmt.place.base {
158+
self.captured_vars
159+
.entry(vid.var_path.hir_id)
160+
.or_default()
161+
.push((vid, borrow_hir_id, bk));
162+
}
163+
}
164+
}
165+
166+
impl<'tcx> euv::Delegate<'tcx> for MovedVariablesCtxt {
167+
fn consume(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
168+
self.move_common(cmt, hir_id);
169+
}
170+
171+
fn borrow(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId, bk: ty::BorrowKind) {
172+
self.borrow_common(cmt, hir_id, bk);
173+
}
174+
175+
fn mutate(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: HirId) {}
176+
177+
fn fake_read(&mut self, _: &rustc_hir_typeck::expr_use_visitor::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {}
178+
}

0 commit comments

Comments
 (0)