Skip to content

Commit d4fb5d2

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

13 files changed

+4278
-12
lines changed

CHANGELOG.md

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

+1
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/endian_bytes.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ fn maybe_lint_endian_bytes(cx: &LateContext<'_>, expr: &Expr<'_>, prefix: Prefix
203203
lint.as_name(prefix),
204204
if prefix == Prefix::To { " method" } else { "" },
205205
),
206-
move |diag| {
206+
|diag| {
207207
if let Some(help) = help {
208208
diag.help(help);
209209
}

clippy_lints/src/fallible_impl_from.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ fn lint_impl_body(cx: &LateContext<'_>, impl_span: Span, impl_items: &[hir::Impl
116116
FALLIBLE_IMPL_FROM,
117117
impl_span,
118118
"consider implementing `TryFrom` instead",
119-
move |diag| {
119+
|diag| {
120120
diag.help(
121121
"`From` is intended for infallible conversions only. \
122122
Use `TryFrom` if there's a possibility for the conversion to fail",

clippy_lints/src/lib.rs

+8-6
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;
@@ -685,7 +686,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
685686
store.register_late_pass(move |_| Box::new(from_over_into::FromOverInto::new(msrv())));
686687
store.register_late_pass(move |_| Box::new(use_self::UseSelf::new(msrv())));
687688
store.register_late_pass(move |_| Box::new(missing_const_for_fn::MissingConstForFn::new(msrv())));
688-
store.register_late_pass(move |_| Box::new(needless_question_mark::NeedlessQuestionMark));
689+
store.register_late_pass(|_| Box::new(needless_question_mark::NeedlessQuestionMark));
689690
store.register_late_pass(move |_| Box::new(casts::Casts::new(msrv())));
690691
store.register_early_pass(move || Box::new(unnested_or_patterns::UnnestedOrPatterns::new(msrv())));
691692
store.register_late_pass(|_| Box::new(size_of_in_element_count::SizeOfInElementCount));
@@ -752,7 +753,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
752753
store.register_late_pass(|_| Box::new(mixed_read_write_in_expression::EvalOrderDependence));
753754
store.register_late_pass(move |_| Box::new(missing_doc::MissingDoc::new(missing_docs_in_crate_items)));
754755
store.register_late_pass(|_| Box::new(missing_inline::MissingInline));
755-
store.register_late_pass(move |_| Box::new(exhaustive_items::ExhaustiveItems));
756+
store.register_late_pass(|_| Box::new(exhaustive_items::ExhaustiveItems));
756757
store.register_late_pass(|_| Box::new(match_result_ok::MatchResultOk));
757758
store.register_late_pass(|_| Box::new(partialeq_ne_impl::PartialEqNeImpl));
758759
store.register_late_pass(|_| Box::new(unused_io_amount::UnusedIoAmount));
@@ -895,7 +896,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
895896
store.register_late_pass(|_| Box::new(from_str_radix_10::FromStrRadix10));
896897
store.register_late_pass(move |_| Box::new(if_then_some_else_none::IfThenSomeElseNone::new(msrv())));
897898
store.register_late_pass(|_| Box::new(bool_assert_comparison::BoolAssertComparison));
898-
store.register_early_pass(move || Box::new(module_style::ModStyle));
899+
store.register_early_pass(|| Box::new(module_style::ModStyle));
899900
store.register_late_pass(|_| Box::<unused_async::UnusedAsync>::default());
900901
store.register_late_pass(move |_| Box::new(disallowed_types::DisallowedTypes::new(disallowed_types.clone())));
901902
store.register_late_pass(move |_| {
@@ -905,9 +906,9 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
905906
});
906907
store.register_early_pass(move || Box::new(disallowed_script_idents::DisallowedScriptIdents::new(allowed_scripts)));
907908
store.register_late_pass(|_| Box::new(strlen_on_c_strings::StrlenOnCStrings));
908-
store.register_late_pass(move |_| Box::new(self_named_constructors::SelfNamedConstructors));
909-
store.register_late_pass(move |_| Box::new(iter_not_returning_iterator::IterNotReturningIterator));
910-
store.register_late_pass(move |_| Box::new(manual_assert::ManualAssert));
909+
store.register_late_pass(|_| Box::new(self_named_constructors::SelfNamedConstructors));
910+
store.register_late_pass(|_| Box::new(iter_not_returning_iterator::IterNotReturningIterator));
911+
store.register_late_pass(|_| Box::new(manual_assert::ManualAssert));
911912
store.register_late_pass(move |_| {
912913
Box::new(non_send_fields_in_send_ty::NonSendFieldInSendTy::new(
913914
enable_raw_pointer_heuristic_for_send,
@@ -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/loops/manual_memcpy.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ fn get_assignments<'a, 'tcx>(
408408
// just increases complexity. (cc #3188 and #4193)
409409
stmts
410410
.iter()
411-
.filter_map(move |stmt| match stmt.kind {
411+
.filter_map(|stmt| match stmt.kind {
412412
StmtKind::Local(..) | StmtKind::Item(..) => None,
413413
StmtKind::Expr(e) | StmtKind::Semi(e) => Some(e),
414414
})

clippy_lints/src/needless_move.rs

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::sugg::DiagnosticExt;
3+
use rustc_data_structures::fx::FxIndexMap;
4+
use rustc_errors::Applicability;
5+
use rustc_hir::{CaptureBy, Closure, Expr, ExprKind, HirId};
6+
use rustc_hir_typeck::expr_use_visitor as euv;
7+
use rustc_infer::infer::TyCtxtInferExt;
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_middle::mir::FakeReadCause;
10+
use rustc_middle::ty;
11+
use rustc_middle::ty::UpvarCapture;
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>(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 ctx = {
57+
let mut ctx = MovedVariablesCtxt {
58+
captured: FxIndexMap::default(),
59+
moved: FxIndexMap::default(),
60+
};
61+
let body = cx.tcx.hir().body(closure.body);
62+
let infcx = cx.tcx.infer_ctxt().build();
63+
euv::ExprUseVisitor::new(&mut ctx, &infcx, closure.def_id, cx.param_env, cx.typeck_results())
64+
.consume_body(body);
65+
ctx
66+
};
67+
68+
enum LintResult {
69+
/// do not remove the `move` keyword.
70+
NeedMove,
71+
Consumed,
72+
NothingCaptured,
73+
}
74+
75+
let mut lint_result = LintResult::NothingCaptured;
76+
77+
for captured_place in cx.typeck_results().closure_min_captures_flattened(closure.def_id) {
78+
if let Some(ck_expr_id) = captured_place.info.capture_kind_expr_id {
79+
let required_ck = ctx.get_required_kind(ck_expr_id);
80+
match required_ck {
81+
UpvarCapture::ByRef(_) => {
82+
// no matter what the old `lint_result` is, we keep the move.
83+
lint_result = LintResult::NeedMove;
84+
},
85+
UpvarCapture::ByValue => {
86+
lint_result = match lint_result {
87+
LintResult::NothingCaptured | LintResult::Consumed => LintResult::Consumed,
88+
LintResult::NeedMove => LintResult::NeedMove,
89+
}
90+
},
91+
}
92+
}
93+
}
94+
95+
let lint = |note_msg: &'static str| {
96+
span_lint_and_then(
97+
cx,
98+
NEEDLESS_MOVE,
99+
expr.span,
100+
"you seem to use `move`, but the `move` is unnecessary",
101+
|diag| {
102+
diag.suggest_remove_item(cx, move_kw, "remove the `move`", Applicability::MachineApplicable);
103+
diag.note(note_msg);
104+
},
105+
);
106+
};
107+
108+
match lint_result {
109+
LintResult::NothingCaptured => {
110+
lint("there were no captured variables, so the `move` is unnecessary");
111+
},
112+
LintResult::Consumed => {
113+
lint("there were consumed variables, but no borrowed variables, so the `move` is unnecessary");
114+
},
115+
LintResult::NeedMove => {
116+
// captured_vars is not empty, or mutated_vars is not empty, so `move` actually
117+
// makes a difference and we should not remove it from this closure.
118+
},
119+
}
120+
}
121+
}
122+
123+
impl<'tcx> LateLintPass<'tcx> for NeedlessMove {
124+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
125+
if expr.span.from_expansion() {
126+
return;
127+
}
128+
129+
let ExprKind::Closure(closure) = &expr.kind else {
130+
return;
131+
};
132+
133+
Self::check_closure(cx, expr, closure);
134+
}
135+
}
136+
137+
#[derive(Debug)]
138+
struct MovedVariablesCtxt<'tcx> {
139+
// for each base variable, we remember:
140+
/// The places where it was captured (and consumed, e.g. moved into the closure).
141+
moved: FxIndexMap<HirId, Vec<(ty::UpvarId, euv::Place<'tcx>, HirId)>>,
142+
/// The places where it was captured by reference (and not consumed).
143+
/// If this vector is not empty, then the `move` keyword makes a difference.
144+
/// There are a few special cases though, such as:
145+
/// 1. We also handle the case in which there's both a borrow and a move of the
146+
/// same value into the closure, e.g.:
147+
///
148+
/// ```no_run
149+
/// let x = String::new();
150+
/// let closure = move || {
151+
/// let s = x.as_str(); // L1
152+
/// println!("{s}");
153+
/// drop(x); // L2
154+
/// };
155+
/// ```
156+
///
157+
/// In this case, the `x` `String` gets moved into the closure (because of L2), but
158+
/// it is also borrowed prior to that at L1.
159+
///
160+
/// How we handle this is by removing the entries that point to `x` if it was captured
161+
/// by value (and therefore moved into the closure).
162+
captured: FxIndexMap<HirId, Vec<(ty::UpvarId, euv::Place<'tcx>, HirId, ty::BorrowKind)>>,
163+
}
164+
165+
impl<'tcx> MovedVariablesCtxt<'tcx> {
166+
fn move_common(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
167+
if let euv::PlaceBase::Upvar(vid) = cmt.place.base {
168+
self.moved
169+
.entry(vid.var_path.hir_id)
170+
.or_default()
171+
.push((vid, cmt.place.clone(), hir_id));
172+
}
173+
}
174+
175+
fn borrow_common(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, borrow_hir_id: HirId, bk: ty::BorrowKind) {
176+
if let euv::PlaceBase::Upvar(vid) = cmt.place.base {
177+
self.captured
178+
.entry(vid.var_path.hir_id)
179+
.or_default()
180+
.push((vid, cmt.place.clone(), borrow_hir_id, bk));
181+
}
182+
}
183+
184+
fn get_required_kind(&self, ref_hir_id: HirId) -> UpvarCapture {
185+
match self
186+
.moved
187+
.iter()
188+
.any(|it| it.1.iter().any(|upvar_ref| upvar_ref.2 == ref_hir_id))
189+
{
190+
true => UpvarCapture::ByValue,
191+
false => self
192+
.captured
193+
.iter()
194+
.find_map(|it| it.1.iter().find(|upvar_ref| upvar_ref.2 == ref_hir_id))
195+
.map(|it| UpvarCapture::ByRef(it.3))
196+
.unwrap(),
197+
}
198+
}
199+
}
200+
201+
impl<'tcx> euv::Delegate<'tcx> for MovedVariablesCtxt<'tcx> {
202+
fn consume(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
203+
self.move_common(cmt, hir_id);
204+
}
205+
206+
fn borrow(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId, bk: ty::BorrowKind) {
207+
self.borrow_common(cmt, hir_id, bk);
208+
}
209+
210+
fn mutate(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
211+
self.borrow(cmt, hir_id, ty::BorrowKind::MutBorrow);
212+
}
213+
214+
fn fake_read(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {}
215+
}

clippy_lints/src/panic_in_result_fn.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ fn lint_impl_body<'tcx>(cx: &LateContext<'tcx>, impl_span: Span, body: &'tcx hir
8484
PANIC_IN_RESULT_FN,
8585
impl_span,
8686
"used `panic!()` or assertion in a function that returns `Result`",
87-
move |diag| {
87+
|diag| {
8888
diag.help(
8989
"`panic!()` or assertions should not be used in a function that returns `Result` as `Result` is expected to return an error instead of crashing",
9090
);

clippy_lints/src/unwrap_in_result.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ fn lint_impl_body<'tcx>(cx: &LateContext<'tcx>, impl_span: Span, impl_item: &'tc
106106
UNWRAP_IN_RESULT,
107107
impl_span,
108108
"used unwrap or expect in a function that returns result or option",
109-
move |diag| {
109+
|diag| {
110110
diag.help("unwrap and expect should not be used in a function that returns result or option");
111111
diag.span_note(result, "potential non-recoverable error(s)");
112112
},

src/driver.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ pub fn main() {
186186
handler.note_without_error(format!("Clippy version: {version_info}"));
187187
});
188188

189-
exit(rustc_driver::catch_with_exit_code(move || {
189+
exit(rustc_driver::catch_with_exit_code(|| {
190190
let mut orig_args: Vec<String> = env::args().collect();
191191
let has_sysroot_arg = arg_value(&orig_args, "--sysroot", |_| true).is_some();
192192

0 commit comments

Comments
 (0)