Skip to content

Commit 20b12fc

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

13 files changed

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

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//! This lint works by looking at the min_captures that `rustc` uses,
2+
//! and checks that for the expression `capture_kind_expr_id`, it would
3+
//! actually borrow normally, if it weren't for the move keyword.
4+
//!
5+
//! In such cases, the move keyword changes the semantics of the code (e.g.
6+
//! without it that capture would be a normal by reference capture, but with
7+
//! move it would get captured by value, and therefore we do not remove the `move`
8+
//! keyword from the closure).
9+
//!
10+
//! A small caveat for the approach above:
11+
//! There's both a borrow and a move of the same value into the closure, e.g.:
12+
//!
13+
//! ```no_run
14+
//! let x = String::new();
15+
//! let closure = move || {
16+
//! let s = x.as_str(); // L1
17+
//! println!("{s}");
18+
//! drop(x); // L2
19+
//! };
20+
//! ```
21+
//!
22+
//! In this case, the `x` `String` gets moved into the closure (because of L2), but
23+
//! it is also borrowed prior to that at L1.
24+
//!
25+
//! `rustc`, in the presence of the `move` keyword automatically assumes that if
26+
//! it borrows a value, it's going to move it into the closure (in the example above at L1,
27+
//! so capture_kind_expr_id would point to the use on L1), but here, in the case
28+
//! of this lint, we should behave a little differently, namely we should first look
29+
//! at all the locations where a place is captured, and if any of them actually moves it,
30+
//! the closure would consume the value.
31+
//!
32+
//! The logic for this is handled in `MovedVariablesCtxt::get_required_kind`, where we
33+
//! try to infer the actual min capture kind needed.
34+
35+
use clippy_utils::diagnostics::span_lint_and_then;
36+
use clippy_utils::sugg::DiagnosticExt;
37+
use rustc_errors::Applicability;
38+
use rustc_hir::{CaptureBy, Closure, Expr, ExprKind, HirId};
39+
use rustc_hir_typeck::expr_use_visitor as euv;
40+
use rustc_infer::infer::TyCtxtInferExt;
41+
use rustc_lint::{LateContext, LateLintPass};
42+
use rustc_middle::mir::FakeReadCause;
43+
use rustc_middle::ty;
44+
use rustc_middle::ty::UpvarCapture;
45+
use rustc_session::{declare_lint_pass, declare_tool_lint};
46+
47+
declare_clippy_lint! {
48+
/// ### What it does
49+
/// Checks for closures and `async` blocks where the `move` is not necessary.
50+
/// E.g. all the values are captured by value into the closure / `async` block.
51+
///
52+
/// ### Why is this bad?
53+
/// Pedantry
54+
/// ### Example
55+
/// ```no_run
56+
/// let a = String::new();
57+
/// let closure = move || {
58+
/// drop(a);
59+
/// };
60+
/// ```
61+
/// Use instead:
62+
/// ```no_run
63+
/// let a = String::new();
64+
/// let closure = || {
65+
/// drop(a);
66+
/// };
67+
/// ```
68+
#[clippy::version = "1.76.0"]
69+
pub NEEDLESS_MOVE,
70+
pedantic,
71+
"checks for needless `move`s on closures / `async` blocks"
72+
}
73+
74+
declare_lint_pass!(NeedlessMove => [NEEDLESS_MOVE]);
75+
76+
impl NeedlessMove {
77+
fn check_closure<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, closure: &'tcx Closure<'tcx>) {
78+
let CaptureBy::Value { move_kw } = closure.capture_clause else {
79+
return;
80+
};
81+
82+
if move_kw.is_dummy() {
83+
// async fn ...() {} convert the body to an `async move {}` block,
84+
// with a DUMMY_SP for the move_kw
85+
return;
86+
}
87+
88+
// Collect moved & borrowed variables from the closure, which the closure *actually* needs.
89+
let ctx = {
90+
let mut ctx = MovedVariablesCtxt::default();
91+
let body = cx.tcx.hir().body(closure.body);
92+
let infcx = cx.tcx.infer_ctxt().build();
93+
euv::ExprUseVisitor::new(&mut ctx, &infcx, closure.def_id, cx.param_env, cx.typeck_results())
94+
.consume_body(body);
95+
ctx
96+
};
97+
98+
enum LintResult {
99+
/// do not remove the `move` keyword.
100+
NeedMove,
101+
Consumed,
102+
NothingCaptured,
103+
}
104+
105+
let mut lint_result = LintResult::NothingCaptured;
106+
107+
for captured_place in cx.typeck_results().closure_min_captures_flattened(closure.def_id) {
108+
let place = &captured_place.place;
109+
if let Some(ck_expr_id) = captured_place.info.capture_kind_expr_id {
110+
let required_ck = ctx.get_required_kind(place, ck_expr_id);
111+
match required_ck {
112+
UpvarCapture::ByRef(_) => {
113+
// no matter what the old `lint_result` is, we keep the move.
114+
lint_result = LintResult::NeedMove;
115+
},
116+
UpvarCapture::ByValue => {
117+
lint_result = match lint_result {
118+
LintResult::NothingCaptured | LintResult::Consumed => LintResult::Consumed,
119+
LintResult::NeedMove => LintResult::NeedMove,
120+
}
121+
},
122+
}
123+
}
124+
}
125+
126+
let lint = |note_msg: &'static str| {
127+
span_lint_and_then(
128+
cx,
129+
NEEDLESS_MOVE,
130+
expr.span,
131+
"you seem to use `move`, but the `move` is unnecessary",
132+
|diag| {
133+
diag.suggest_remove_item(cx, move_kw, "remove the `move`", Applicability::MachineApplicable);
134+
diag.note(note_msg);
135+
},
136+
);
137+
};
138+
139+
match lint_result {
140+
LintResult::NothingCaptured => {
141+
lint("there were no captured variables, so the `move` is unnecessary");
142+
},
143+
LintResult::Consumed => {
144+
lint("there were consumed variables, but no borrowed variables, so the `move` is unnecessary");
145+
},
146+
LintResult::NeedMove => {
147+
// there was a value which would be borrowed if it weren't for the move keyword,
148+
// so we should keep it, as removing it would change semantics.
149+
},
150+
}
151+
}
152+
}
153+
154+
impl<'tcx> LateLintPass<'tcx> for NeedlessMove {
155+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
156+
if expr.span.from_expansion() {
157+
return;
158+
}
159+
160+
let ExprKind::Closure(closure) = &expr.kind else {
161+
return;
162+
};
163+
164+
Self::check_closure(cx, expr, closure);
165+
}
166+
}
167+
168+
#[derive(Debug, Default)]
169+
struct MovedVariablesCtxt<'tcx> {
170+
// for each base variable, we remember:
171+
/// The places where it was captured (and consumed, e.g. moved into the closure).
172+
moved: Vec<(euv::Place<'tcx>, HirId)>,
173+
/// The places where it was captured by reference (and not consumed).
174+
captured: Vec<(euv::Place<'tcx>, HirId, ty::BorrowKind)>,
175+
}
176+
177+
impl<'tcx> MovedVariablesCtxt<'tcx> {
178+
fn move_common(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
179+
if let euv::PlaceBase::Upvar(_) = cmt.place.base {
180+
self.moved.push((cmt.place.clone(), hir_id));
181+
}
182+
}
183+
184+
fn borrow_common(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, borrow_hir_id: HirId, bk: ty::BorrowKind) {
185+
if let euv::PlaceBase::Upvar(_) = cmt.place.base {
186+
self.captured.push((cmt.place.clone(), borrow_hir_id, bk));
187+
}
188+
}
189+
190+
fn get_required_kind(&self, place: &euv::Place<'tcx>, ref_hir_id: HirId) -> UpvarCapture {
191+
match self
192+
.moved
193+
.iter()
194+
.any(|upvar_ref| upvar_ref.0 == *place || upvar_ref.1 == ref_hir_id)
195+
{
196+
true => UpvarCapture::ByValue,
197+
false => self
198+
.captured
199+
.iter()
200+
.find(|upvar_ref| upvar_ref.1 == ref_hir_id)
201+
.map(|it| UpvarCapture::ByRef(it.2))
202+
.unwrap(),
203+
}
204+
}
205+
}
206+
207+
impl<'tcx> euv::Delegate<'tcx> for MovedVariablesCtxt<'tcx> {
208+
fn consume(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
209+
self.move_common(cmt, hir_id);
210+
}
211+
212+
fn borrow(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId, bk: ty::BorrowKind) {
213+
self.borrow_common(cmt, hir_id, bk);
214+
}
215+
216+
fn mutate(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
217+
self.borrow(cmt, hir_id, ty::BorrowKind::MutBorrow);
218+
}
219+
220+
fn fake_read(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {}
221+
}

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)