Skip to content

Commit 0e66693

Browse files
committed
feat: needless_move lint
An implementation for the lint described in rust-lang#11721
1 parent cb87a57 commit 0e66693

12 files changed

+4374
-11
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5523,6 +5523,7 @@ Released 2018-09-13
55235523
[`needless_late_init`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_late_init
55245524
[`needless_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes
55255525
[`needless_match`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_match
5526+
[`needless_move`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_move
55265527
[`needless_option_as_deref`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_option_as_deref
55275528
[`needless_option_take`]: https://rust-lang.github.io/rust-clippy/master/index.html#needless_option_take
55285529
[`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
@@ -527,6 +527,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
527527
crate::needless_for_each::NEEDLESS_FOR_EACH_INFO,
528528
crate::needless_if::NEEDLESS_IF_INFO,
529529
crate::needless_late_init::NEEDLESS_LATE_INIT_INFO,
530+
crate::needless_move::NEEDLESS_MOVE_INFO,
530531
crate::needless_parens_on_range_literals::NEEDLESS_PARENS_ON_RANGE_LITERALS_INFO,
531532
crate::needless_pass_by_ref_mut::NEEDLESS_PASS_BY_REF_MUT_INFO,
532533
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
@@ -254,6 +254,7 @@ mod needless_else;
254254
mod needless_for_each;
255255
mod needless_if;
256256
mod needless_late_init;
257+
mod needless_move;
257258
mod needless_parens_on_range_literals;
258259
mod needless_pass_by_ref_mut;
259260
mod needless_pass_by_value;
@@ -716,7 +717,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
716717
store.register_late_pass(move |_| Box::new(from_over_into::FromOverInto::new(msrv())));
717718
store.register_late_pass(move |_| Box::new(use_self::UseSelf::new(msrv())));
718719
store.register_late_pass(move |_| Box::new(missing_const_for_fn::MissingConstForFn::new(msrv())));
719-
store.register_late_pass(move |_| Box::new(needless_question_mark::NeedlessQuestionMark));
720+
store.register_late_pass(|_| Box::new(needless_question_mark::NeedlessQuestionMark));
720721
store.register_late_pass(move |_| Box::new(casts::Casts::new(msrv())));
721722
store.register_early_pass(move || Box::new(unnested_or_patterns::UnnestedOrPatterns::new(msrv())));
722723
store.register_late_pass(|_| Box::new(size_of_in_element_count::SizeOfInElementCount));
@@ -784,7 +785,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
784785
store.register_late_pass(|_| Box::new(mixed_read_write_in_expression::EvalOrderDependence));
785786
store.register_late_pass(move |_| Box::new(missing_doc::MissingDoc::new(missing_docs_in_crate_items)));
786787
store.register_late_pass(|_| Box::new(missing_inline::MissingInline));
787-
store.register_late_pass(move |_| Box::new(exhaustive_items::ExhaustiveItems));
788+
store.register_late_pass(|_| Box::new(exhaustive_items::ExhaustiveItems));
788789
store.register_late_pass(|_| Box::new(match_result_ok::MatchResultOk));
789790
store.register_late_pass(|_| Box::new(partialeq_ne_impl::PartialEqNeImpl));
790791
store.register_late_pass(|_| Box::new(unused_io_amount::UnusedIoAmount));
@@ -932,7 +933,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
932933
store.register_late_pass(|_| Box::new(from_str_radix_10::FromStrRadix10));
933934
store.register_late_pass(move |_| Box::new(if_then_some_else_none::IfThenSomeElseNone::new(msrv())));
934935
store.register_late_pass(|_| Box::new(bool_assert_comparison::BoolAssertComparison));
935-
store.register_early_pass(move || Box::new(module_style::ModStyle));
936+
store.register_early_pass(|| Box::new(module_style::ModStyle));
936937
store.register_late_pass(|_| Box::<unused_async::UnusedAsync>::default());
937938
store.register_late_pass(move |_| Box::new(disallowed_types::DisallowedTypes::new(disallowed_types.clone())));
938939
store.register_late_pass(move |_| {
@@ -942,9 +943,9 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
942943
});
943944
store.register_early_pass(move || Box::new(disallowed_script_idents::DisallowedScriptIdents::new(allowed_scripts)));
944945
store.register_late_pass(|_| Box::new(strlen_on_c_strings::StrlenOnCStrings));
945-
store.register_late_pass(move |_| Box::new(self_named_constructors::SelfNamedConstructors));
946-
store.register_late_pass(move |_| Box::new(iter_not_returning_iterator::IterNotReturningIterator));
947-
store.register_late_pass(move |_| Box::new(manual_assert::ManualAssert));
946+
store.register_late_pass(|_| Box::new(self_named_constructors::SelfNamedConstructors));
947+
store.register_late_pass(|_| Box::new(iter_not_returning_iterator::IterNotReturningIterator));
948+
store.register_late_pass(|_| Box::new(manual_assert::ManualAssert));
948949
store.register_late_pass(move |_| {
949950
Box::new(non_send_fields_in_send_ty::NonSendFieldInSendTy::new(
950951
enable_raw_pointer_heuristic_for_send,
@@ -1131,6 +1132,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
11311132
store.register_late_pass(|_| Box::new(zero_repeat_side_effects::ZeroRepeatSideEffects));
11321133
store.register_late_pass(|_| Box::new(manual_unwrap_or_default::ManualUnwrapOrDefault));
11331134
store.register_late_pass(|_| Box::new(integer_division_remainder_used::IntegerDivisionRemainderUsed));
1135+
store.register_late_pass(|_| Box::new(needless_move::NeedlessMove));
11341136
// add lints here, do not remove this comment, it's used in `new_lint`
11351137
}
11361138

clippy_lints/src/loops/manual_memcpy.rs

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

clippy_lints/src/needless_move.rs

+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
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+
let mut lint_result = LintResult::NothingCaptured;
99+
100+
for captured_place in cx.typeck_results().closure_min_captures_flattened(closure.def_id) {
101+
let place = &captured_place.place;
102+
if let Some(ck_expr_id) = captured_place.info.capture_kind_expr_id {
103+
let required_ck = ctx.get_required_kind(place, ck_expr_id);
104+
match required_ck {
105+
UpvarCapture::ByRef(_) => {
106+
// no matter what the old `lint_result` is, we keep the move.
107+
lint_result = LintResult::NeedMove;
108+
},
109+
UpvarCapture::ByValue => {
110+
lint_result = match lint_result {
111+
LintResult::NothingCaptured | LintResult::Consumed => LintResult::Consumed,
112+
LintResult::NeedMove => LintResult::NeedMove,
113+
}
114+
},
115+
}
116+
}
117+
}
118+
119+
let lint = |note_msg: &'static str| {
120+
span_lint_and_then(
121+
cx,
122+
NEEDLESS_MOVE,
123+
expr.span,
124+
"you seem to use `move`, but the `move` is unnecessary",
125+
|diag| {
126+
diag.suggest_remove_item(cx, move_kw, "remove the `move`", Applicability::MachineApplicable);
127+
diag.note(note_msg);
128+
},
129+
);
130+
};
131+
132+
match lint_result {
133+
LintResult::NothingCaptured => {
134+
lint("there were no captured variables, so the `move` is unnecessary");
135+
},
136+
LintResult::Consumed => {
137+
lint("there were consumed variables, but no borrowed variables, so the `move` is unnecessary");
138+
},
139+
LintResult::NeedMove => {
140+
// there was a value which would be borrowed if it weren't for the move keyword,
141+
// so we should keep it, as removing it would change semantics.
142+
},
143+
}
144+
}
145+
}
146+
147+
impl<'tcx> LateLintPass<'tcx> for NeedlessMove {
148+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
149+
if expr.span.from_expansion() {
150+
return;
151+
}
152+
153+
let ExprKind::Closure(closure) = &expr.kind else {
154+
return;
155+
};
156+
157+
Self::check_closure(cx, expr, closure);
158+
}
159+
}
160+
161+
enum LintResult {
162+
/// do not remove the `move` keyword.
163+
NeedMove,
164+
Consumed,
165+
NothingCaptured,
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+
if self
192+
.moved
193+
.iter()
194+
.any(|upvar_ref| upvar_ref.0 == *place || upvar_ref.1 == ref_hir_id)
195+
{
196+
UpvarCapture::ByValue
197+
} else {
198+
self.captured
199+
.iter()
200+
.find(|upvar_ref| upvar_ref.1 == ref_hir_id)
201+
.map_or(UpvarCapture::ByRef(ty::BorrowKind::ImmBorrow), |it| {
202+
UpvarCapture::ByRef(it.2)
203+
})
204+
}
205+
}
206+
}
207+
208+
impl<'tcx> euv::Delegate<'tcx> for MovedVariablesCtxt<'tcx> {
209+
fn consume(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
210+
self.move_common(cmt, hir_id);
211+
}
212+
213+
fn borrow(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId, bk: ty::BorrowKind) {
214+
self.borrow_common(cmt, hir_id, bk);
215+
}
216+
217+
fn mutate(&mut self, cmt: &euv::PlaceWithHirId<'tcx>, hir_id: HirId) {
218+
self.borrow(cmt, hir_id, ty::BorrowKind::MutBorrow);
219+
}
220+
221+
fn fake_read(&mut self, _: &euv::PlaceWithHirId<'tcx>, _: FakeReadCause, _: HirId) {}
222+
}

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
},

0 commit comments

Comments
 (0)