Skip to content

Commit 68e9a60

Browse files
committed
new lint: unnecessary_box_pin
1 parent 5873cb9 commit 68e9a60

File tree

12 files changed

+377
-3
lines changed

12 files changed

+377
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6053,6 +6053,7 @@ Released 2018-09-13
60536053
[`unit_hash`]: https://rust-lang.github.io/rust-clippy/master/index.html#unit_hash
60546054
[`unit_return_expecting_ord`]: https://rust-lang.github.io/rust-clippy/master/index.html#unit_return_expecting_ord
60556055
[`unknown_clippy_lints`]: https://rust-lang.github.io/rust-clippy/master/index.html#unknown_clippy_lints
6056+
[`unnecessary_box_pin`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_box_pin
60566057
[`unnecessary_box_returns`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_box_returns
60576058
[`unnecessary_cast`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_cast
60586059
[`unnecessary_clippy_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_clippy_cfg

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
77

8-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
8+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
99

1010
Lints are divided into categories, each with a default [lint level](https://doc.rust-lang.org/rustc/lints/levels.html).
1111
You can choose how much Clippy is supposed to ~~annoy~~ help you by changing the lint level by category.

book/src/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
A collection of lints to catch common mistakes and improve your
77
[Rust](https://github.com/rust-lang/rust) code.
88

9-
[There are over 700 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
9+
[There are over 750 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
1010

1111
Lints are divided into categories, each with a default [lint
1212
level](https://doc.rust-lang.org/rustc/lints/levels.html). You can choose how

book/src/lint_configuration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ The minimum rust version that the project supports. Defaults to the `rust-versio
726726
* [`type_repetition_in_bounds`](https://rust-lang.github.io/rust-clippy/master/index.html#type_repetition_in_bounds)
727727
* [`unchecked_duration_subtraction`](https://rust-lang.github.io/rust-clippy/master/index.html#unchecked_duration_subtraction)
728728
* [`uninlined_format_args`](https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args)
729+
* [`unnecessary_box_pin`](https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_box_pin)
729730
* [`unnecessary_lazy_evaluations`](https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations)
730731
* [`unnested_or_patterns`](https://rust-lang.github.io/rust-clippy/master/index.html#unnested_or_patterns)
731732
* [`unused_trait_names`](https://rust-lang.github.io/rust-clippy/master/index.html#unused_trait_names)

clippy_config/src/conf.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ define_Conf! {
586586
type_repetition_in_bounds,
587587
unchecked_duration_subtraction,
588588
uninlined_format_args,
589+
unnecessary_box_pin,
589590
unnecessary_lazy_evaluations,
590591
unnested_or_patterns,
591592
unused_trait_names,

clippy_config/src/msrvs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ msrv_aliases! {
2626
1,73,0 { MANUAL_DIV_CEIL }
2727
1,71,0 { TUPLE_ARRAY_CONVERSIONS, BUILD_HASHER_HASH_ONE }
2828
1,70,0 { OPTION_RESULT_IS_VARIANT_AND, BINARY_HEAP_RETAIN }
29-
1,68,0 { PATH_MAIN_SEPARATOR_STR }
29+
1,68,0 { PATH_MAIN_SEPARATOR_STR, PIN_MACRO }
3030
1,65,0 { LET_ELSE, POINTER_CAST_CONSTNESS }
3131
1,63,0 { CLONE_INTO }
3232
1,62,0 { BOOL_THEN_SOME, DEFAULT_ENUM_ATTRIBUTE, CONST_EXTERN_C_FN }

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
739739
crate::unit_types::UNIT_ARG_INFO,
740740
crate::unit_types::UNIT_CMP_INFO,
741741
crate::unnamed_address::FN_ADDRESS_COMPARISONS_INFO,
742+
crate::unnecessary_box_pin::UNNECESSARY_BOX_PIN_INFO,
742743
crate::unnecessary_box_returns::UNNECESSARY_BOX_RETURNS_INFO,
743744
crate::unnecessary_literal_bound::UNNECESSARY_LITERAL_BOUND_INFO,
744745
crate::unnecessary_map_on_constructor::UNNECESSARY_MAP_ON_CONSTRUCTOR_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ mod uninit_vec;
362362
mod unit_return_expecting_ord;
363363
mod unit_types;
364364
mod unnamed_address;
365+
mod unnecessary_box_pin;
365366
mod unnecessary_box_returns;
366367
mod unnecessary_literal_bound;
367368
mod unnecessary_map_on_constructor;
@@ -949,5 +950,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
949950
store.register_late_pass(move |_| Box::new(unused_trait_names::UnusedTraitNames::new(conf)));
950951
store.register_late_pass(|_| Box::new(manual_ignore_case_cmp::ManualIgnoreCaseCmp));
951952
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
953+
store.register_late_pass(move |_| Box::new(unnecessary_box_pin::UnnecessaryBoxPin::new(conf)));
952954
// add lints here, do not remove this comment, it's used in `new_lint`
953955
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use clippy_config::msrvs::Msrv;
2+
use clippy_config::{Conf, msrvs};
3+
use clippy_utils::diagnostics::span_lint_and_then;
4+
use clippy_utils::visitors::for_each_local_use_after_expr;
5+
use clippy_utils::{is_from_proc_macro, std_or_core};
6+
use rustc_errors::Applicability;
7+
use rustc_hir::def_id::LocalDefId;
8+
use rustc_hir::{Expr, ExprKind, LetStmt, Node, PatKind, QPath};
9+
use rustc_lint::{LateContext, LateLintPass, LintContext};
10+
use rustc_middle::lint::in_external_macro;
11+
use rustc_session::impl_lint_pass;
12+
use rustc_span::{Span, sym};
13+
use std::ops::ControlFlow;
14+
15+
declare_clippy_lint! {
16+
/// ### What it does
17+
/// Checks for calls to `Box::pin` with the `Pin<Box<_>>` not moved and only used in places where a `Pin<&mut _>`
18+
/// suffices, in which case the `pin!` macro can be used.
19+
///
20+
/// ### Why is this bad?
21+
/// `Box::pin` creates an extra heap allocation for the pointee, while `pin!` creates a local `Pin<&mut T>`,
22+
/// so this saves an extra heap allocation.
23+
///
24+
/// See the documentation for [`pin!`](https://doc.rust-lang.org/stable/std/pin/macro.pin.html)
25+
/// for a more detailed explanation on how these two differ.
26+
///
27+
/// ### Known issues
28+
/// Currently the lint is fairly limited and only emits a warning if the pinned box is used through `.as_mut()`
29+
/// to prevent false positives w.r.t. lifetimes
30+
/// (`Pin<Box<_>>` returned by `Box::pin` is `'static`, `Pin<&mut _>` returned by `pin!` is not).
31+
///
32+
/// The following works with `Box::pin` but not with `pin!`:
33+
/// ```
34+
/// fn assert_static<T: 'static>(_: T) {}
35+
/// assert_static(Box::pin(async {}));
36+
/// ```
37+
///
38+
/// Restricting to only lint `.as_mut()` means that we end up with a temporary in both cases,
39+
/// so if it compiled with `.as_mut()`, then it ought to work with `pin!` as well.
40+
///
41+
/// ### Example
42+
/// ```no_run
43+
/// # #![feature(noop_waker)]
44+
/// # use std::task::{Poll, Waker, Context};
45+
/// # use std::future::Future;
46+
///
47+
/// fn now_or_never<F: Future>(fut: F) -> Option<F::Output> {
48+
/// let mut fut = Box::pin(fut);
49+
///
50+
/// match fut.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
51+
/// Poll::Ready(val) => Some(val),
52+
/// Poll::Pending => None
53+
/// }
54+
/// }
55+
/// ```
56+
/// Use instead:
57+
/// ```no_run
58+
/// # #![feature(noop_waker)]
59+
/// # use std::task::{Poll, Waker, Context};
60+
/// # use std::future::Future;
61+
///
62+
/// fn now_or_never<F: Future>(fut: F) -> Option<F::Output> {
63+
/// let mut fut = std::pin::pin!(fut);
64+
///
65+
/// match fut.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
66+
/// Poll::Ready(val) => Some(val),
67+
/// Poll::Pending => None
68+
/// }
69+
/// }
70+
/// ```
71+
#[clippy::version = "1.84.0"]
72+
pub UNNECESSARY_BOX_PIN,
73+
perf,
74+
"using `Box::pin` where `pin!` suffices"
75+
}
76+
77+
pub struct UnnecessaryBoxPin {
78+
msrv: Msrv,
79+
}
80+
81+
impl UnnecessaryBoxPin {
82+
pub fn new(conf: &'static Conf) -> Self {
83+
Self {
84+
msrv: conf.msrv.clone(),
85+
}
86+
}
87+
}
88+
89+
impl_lint_pass!(UnnecessaryBoxPin => [UNNECESSARY_BOX_PIN]);
90+
91+
impl<'tcx> LateLintPass<'tcx> for UnnecessaryBoxPin {
92+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
93+
if let ExprKind::Call(callee, [_]) = expr.kind
94+
&& let ExprKind::Path(QPath::TypeRelative(bx, segment)) = &callee.kind
95+
&& cx.typeck_results().node_type(bx.hir_id).is_box()
96+
&& segment.ident.name == sym::pin
97+
&& let Some(enclosing_body) = cx.enclosing_body
98+
&& let Some(std_or_core) = std_or_core(cx)
99+
&& self.msrv.meets(msrvs::PIN_MACRO)
100+
&& !in_external_macro(cx.sess(), expr.span)
101+
&& !is_from_proc_macro(cx, expr)
102+
{
103+
let enclosing_body_def_id = cx.tcx.hir().body_owner_def_id(enclosing_body);
104+
105+
if let ControlFlow::Continue(as_mut_span) = check_pin_box_use(cx, expr, false, enclosing_body_def_id) {
106+
span_lint_and_then(
107+
cx,
108+
UNNECESSARY_BOX_PIN,
109+
expr.span,
110+
"pinning a value with `Box::pin` when local pinning suffices",
111+
|diag| {
112+
let mut replacements = vec![(callee.span, format!("{std_or_core}::pin::pin!"))];
113+
replacements.extend(as_mut_span.map(|span| (span, String::new())));
114+
115+
diag.multipart_suggestion_verbose(
116+
"use the `pin!` macro",
117+
replacements,
118+
Applicability::MachineApplicable,
119+
);
120+
},
121+
);
122+
}
123+
}
124+
}
125+
126+
extract_msrv_attr!(LateContext);
127+
}
128+
129+
/// Checks how a `Pin<Box<_>>` is used. Returns `Continue(span)` if this use is valid with
130+
/// `Box::pin` changed to `pin!`.
131+
///
132+
/// The span is the `.as_mut()` span that can be safely removed.
133+
/// Note that it's currently only returned if `Box::pin()` is the receiver of it (and not first
134+
/// stored in a binding) to avoid move errors.
135+
///
136+
/// That is, `as_mut` can be safely removed here:
137+
/// ```ignore
138+
/// - Box::pin(async {}).as_mut().poll(...);
139+
/// + pin!(async {}).poll(...);
140+
/// ```
141+
///
142+
/// but not here, as the poll call consumes it and the binding cannot be used again in subsequent
143+
/// iterations:
144+
/// ```ignore
145+
/// - let mut bx = Box::pin(async {});
146+
/// + let mut bx = pin!(async {});
147+
/// loop {
148+
/// - bx.as_mut().poll(...);
149+
/// + bx.poll(...);
150+
/// }
151+
/// ```
152+
fn check_pin_box_use<'tcx>(
153+
cx: &LateContext<'tcx>,
154+
expr: &'tcx Expr<'tcx>,
155+
moved: bool,
156+
enclosing_body: LocalDefId,
157+
) -> ControlFlow<(), Option<Span>> {
158+
match cx.tcx.parent_hir_node(expr.hir_id) {
159+
Node::Expr(as_mut_expr)
160+
if let ExprKind::MethodCall(segment, recv, [], span) = as_mut_expr.kind
161+
&& recv.hir_id == expr.hir_id
162+
&& segment.ident.name.as_str() == "as_mut" =>
163+
{
164+
ControlFlow::Continue((!moved).then(|| span.with_lo(recv.span.hi())))
165+
},
166+
Node::LetStmt(LetStmt { pat, ty: None, .. })
167+
if let PatKind::Binding(_, local_id, ..) = pat.kind
168+
&& !moved =>
169+
{
170+
for_each_local_use_after_expr(cx, local_id, expr.hir_id, |expr| {
171+
if check_pin_box_use(cx, expr, true, enclosing_body).is_continue()
172+
// Make sure the `Pin` is not captured by a closure.
173+
&& cx.tcx.hir().enclosing_body_owner(expr.hir_id) == enclosing_body
174+
{
175+
ControlFlow::Continue(())
176+
} else {
177+
ControlFlow::Break(())
178+
}
179+
})?;
180+
ControlFlow::Continue(None)
181+
},
182+
_ => ControlFlow::Break(()),
183+
}
184+
}

tests/ui/unnecessary_box_pin.fixed

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//@aux-build:proc_macros.rs
2+
#![warn(clippy::unnecessary_box_pin)]
3+
4+
extern crate proc_macros;
5+
6+
use std::convert::identity;
7+
use std::future::Future;
8+
use std::pin::Pin;
9+
use std::task::Context;
10+
11+
async fn fut() {}
12+
13+
fn accept_unpin_fut(_: impl Future + Unpin) {}
14+
15+
fn assert_static(_: impl Send + Sync + 'static) {}
16+
17+
fn test(cx: &mut Context<'_>) {
18+
accept_unpin_fut(std::pin::pin!(fut()));
19+
//~^ unnecessary_box_pin
20+
21+
let mut bx = std::pin::pin!(fut());
22+
//~^ unnecessary_box_pin
23+
accept_unpin_fut(bx.as_mut());
24+
25+
#[allow(clippy::let_underscore_future)]
26+
let _: Pin<&mut _> = std::pin::pin!(fut());
27+
//~^ unnecessary_box_pin
28+
29+
let bx = Box::pin(fut());
30+
assert_static(|| bx);
31+
assert_static(|| Box::pin(fut()));
32+
33+
std::pin::pin!(fut()).poll(cx);
34+
//~^ unnecessary_box_pin
35+
36+
assert_static(identity(Box::pin(async {})));
37+
38+
let mut bx = std::pin::pin!(fut());
39+
//~^ unnecessary_box_pin
40+
loop {
41+
bx.as_mut().poll(cx);
42+
}
43+
44+
proc_macros::with_span! {
45+
span
46+
let mut bx = Box::pin(fut());
47+
accept_unpin_fut(bx.as_mut());
48+
}
49+
proc_macros::external! {
50+
let mut bx = Box::pin(fut());
51+
accept_unpin_fut(bx.as_mut());
52+
}
53+
}
54+
55+
#[clippy::msrv = "1.67.0"]
56+
fn too_low_msrv() {
57+
let mut bx = Box::pin(fut());
58+
accept_unpin_fut(bx.as_mut());
59+
}
60+
61+
fn main() {}

0 commit comments

Comments
 (0)