-
Notifications
You must be signed in to change notification settings - Fork 1.7k
new lint: unnecessary_box_pin
#13598
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
y21
wants to merge
1
commit into
rust-lang:master
Choose a base branch
from
y21:unnecessary_pin_box
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
use clippy_config::msrvs::Msrv; | ||
use clippy_config::{Conf, msrvs}; | ||
use clippy_utils::diagnostics::span_lint_and_then; | ||
use clippy_utils::visitors::for_each_local_use_after_expr; | ||
use clippy_utils::{is_from_proc_macro, std_or_core}; | ||
use rustc_errors::Applicability; | ||
use rustc_hir::def_id::LocalDefId; | ||
use rustc_hir::{Expr, ExprKind, LetStmt, Node, PatKind, QPath}; | ||
use rustc_lint::{LateContext, LateLintPass, LintContext}; | ||
use rustc_middle::lint::in_external_macro; | ||
use rustc_session::impl_lint_pass; | ||
use rustc_span::{Span, sym}; | ||
use std::ops::ControlFlow; | ||
|
||
declare_clippy_lint! { | ||
/// ### What it does | ||
/// Checks for calls to `Box::pin` with the `Pin<Box<_>>` not moved and only used in places where a `Pin<&mut _>` | ||
/// suffices, in which case the `pin!` macro can be used. | ||
/// | ||
/// ### Why is this bad? | ||
/// `Box::pin` creates an extra heap allocation for the pointee, while `pin!` creates a local `Pin<&mut T>`, | ||
/// so this saves an extra heap allocation. | ||
/// | ||
/// See the documentation for [`pin!`](https://doc.rust-lang.org/stable/std/pin/macro.pin.html) | ||
/// for a more detailed explanation on how these two differ. | ||
/// | ||
/// ### Known issues | ||
/// Currently the lint is fairly limited and only emits a warning if the pinned box is used through `.as_mut()` | ||
/// to prevent false positives w.r.t. lifetimes | ||
/// (`Pin<Box<_>>` returned by `Box::pin` is `'static`, `Pin<&mut _>` returned by `pin!` is not). | ||
/// | ||
/// The following works with `Box::pin` but not with `pin!`: | ||
/// ``` | ||
/// fn assert_static<T: 'static>(_: T) {} | ||
/// assert_static(Box::pin(async {})); | ||
/// ``` | ||
/// | ||
/// Restricting to only lint `.as_mut()` means that we end up with a temporary in both cases, | ||
/// so if it compiled with `.as_mut()`, then it ought to work with `pin!` as well. | ||
/// | ||
/// ### Example | ||
/// ```no_run | ||
/// # #![feature(noop_waker)] | ||
/// # use std::task::{Poll, Waker, Context}; | ||
/// # use std::future::Future; | ||
/// | ||
/// fn now_or_never<F: Future>(fut: F) -> Option<F::Output> { | ||
/// let mut fut = Box::pin(fut); | ||
/// | ||
/// match fut.as_mut().poll(&mut Context::from_waker(Waker::noop())) { | ||
/// Poll::Ready(val) => Some(val), | ||
/// Poll::Pending => None | ||
/// } | ||
/// } | ||
/// ``` | ||
/// Use instead: | ||
/// ```no_run | ||
/// # #![feature(noop_waker)] | ||
/// # use std::task::{Poll, Waker, Context}; | ||
/// # use std::future::Future; | ||
/// | ||
/// fn now_or_never<F: Future>(fut: F) -> Option<F::Output> { | ||
/// let mut fut = std::pin::pin!(fut); | ||
/// | ||
/// match fut.as_mut().poll(&mut Context::from_waker(Waker::noop())) { | ||
/// Poll::Ready(val) => Some(val), | ||
/// Poll::Pending => None | ||
/// } | ||
/// } | ||
/// ``` | ||
#[clippy::version = "1.84.0"] | ||
pub UNNECESSARY_BOX_PIN, | ||
perf, | ||
"using `Box::pin` where `pin!` suffices" | ||
} | ||
|
||
pub struct UnnecessaryBoxPin { | ||
msrv: Msrv, | ||
} | ||
|
||
impl UnnecessaryBoxPin { | ||
pub fn new(conf: &'static Conf) -> Self { | ||
Self { | ||
msrv: conf.msrv.clone(), | ||
} | ||
} | ||
} | ||
|
||
impl_lint_pass!(UnnecessaryBoxPin => [UNNECESSARY_BOX_PIN]); | ||
|
||
impl<'tcx> LateLintPass<'tcx> for UnnecessaryBoxPin { | ||
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) { | ||
if let ExprKind::Call(callee, [_]) = expr.kind | ||
&& let ExprKind::Path(QPath::TypeRelative(bx, segment)) = &callee.kind | ||
&& cx.typeck_results().node_type(bx.hir_id).is_box() | ||
&& segment.ident.name == sym::pin | ||
&& let Some(enclosing_body) = cx.enclosing_body | ||
&& let Some(std_or_core) = std_or_core(cx) | ||
&& self.msrv.meets(msrvs::PIN_MACRO) | ||
&& !in_external_macro(cx.sess(), expr.span) | ||
&& !is_from_proc_macro(cx, expr) | ||
{ | ||
let enclosing_body_def_id = cx.tcx.hir().body_owner_def_id(enclosing_body); | ||
|
||
if let ControlFlow::Continue(as_mut_span) = check_pin_box_use(cx, expr, false, enclosing_body_def_id) { | ||
span_lint_and_then( | ||
cx, | ||
UNNECESSARY_BOX_PIN, | ||
expr.span, | ||
"pinning a value with `Box::pin` when local pinning suffices", | ||
|diag| { | ||
let mut replacements = vec![(callee.span, format!("{std_or_core}::pin::pin!"))]; | ||
replacements.extend(as_mut_span.map(|span| (span, String::new()))); | ||
|
||
diag.multipart_suggestion_verbose( | ||
"use the `pin!` macro", | ||
replacements, | ||
Applicability::MachineApplicable, | ||
); | ||
}, | ||
); | ||
} | ||
} | ||
} | ||
|
||
extract_msrv_attr!(LateContext); | ||
} | ||
|
||
/// Checks how a `Pin<Box<_>>` is used. Returns `Continue(span)` if this use is valid with | ||
/// `Box::pin` changed to `pin!`. | ||
/// | ||
/// The span is the `.as_mut()` span that can be safely removed. | ||
/// Note that it's currently only returned if `Box::pin()` is the receiver of it (and not first | ||
/// stored in a binding) to avoid move errors. | ||
/// | ||
/// That is, `as_mut` can be safely removed here: | ||
/// ```ignore | ||
/// - Box::pin(async {}).as_mut().poll(...); | ||
/// + pin!(async {}).poll(...); | ||
/// ``` | ||
/// | ||
/// but not here, as the poll call consumes it and the binding cannot be used again in subsequent | ||
/// iterations: | ||
/// ```ignore | ||
/// - let mut bx = Box::pin(async {}); | ||
/// + let mut bx = pin!(async {}); | ||
/// loop { | ||
/// - bx.as_mut().poll(...); | ||
/// + bx.poll(...); | ||
/// } | ||
/// ``` | ||
fn check_pin_box_use<'tcx>( | ||
cx: &LateContext<'tcx>, | ||
expr: &'tcx Expr<'tcx>, | ||
moved: bool, | ||
enclosing_body: LocalDefId, | ||
) -> ControlFlow<(), Option<Span>> { | ||
match cx.tcx.parent_hir_node(expr.hir_id) { | ||
Node::Expr(as_mut_expr) | ||
if let ExprKind::MethodCall(segment, recv, [], span) = as_mut_expr.kind | ||
&& recv.hir_id == expr.hir_id | ||
&& segment.ident.name.as_str() == "as_mut" => | ||
{ | ||
ControlFlow::Continue((!moved).then(|| span.with_lo(recv.span.hi()))) | ||
}, | ||
Node::LetStmt(LetStmt { pat, ty: None, .. }) | ||
if let PatKind::Binding(_, local_id, ..) = pat.kind | ||
&& !moved => | ||
{ | ||
for_each_local_use_after_expr(cx, local_id, expr.hir_id, |expr| { | ||
if check_pin_box_use(cx, expr, true, enclosing_body).is_continue() | ||
// Make sure the `Pin` is not captured by a closure. | ||
&& cx.tcx.hir().enclosing_body_owner(expr.hir_id) == enclosing_body | ||
{ | ||
ControlFlow::Continue(()) | ||
} else { | ||
ControlFlow::Break(()) | ||
} | ||
})?; | ||
ControlFlow::Continue(None) | ||
}, | ||
_ => ControlFlow::Break(()), | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
//@aux-build:proc_macros.rs | ||
#![warn(clippy::unnecessary_box_pin)] | ||
|
||
extern crate proc_macros; | ||
|
||
use std::convert::identity; | ||
use std::future::Future; | ||
use std::pin::Pin; | ||
use std::task::Context; | ||
|
||
async fn fut() {} | ||
|
||
fn accept_unpin_fut(_: impl Future + Unpin) {} | ||
|
||
fn assert_static(_: impl Send + Sync + 'static) {} | ||
|
||
fn test(cx: &mut Context<'_>) { | ||
accept_unpin_fut(std::pin::pin!(fut())); | ||
//~^ unnecessary_box_pin | ||
|
||
let mut bx = std::pin::pin!(fut()); | ||
//~^ unnecessary_box_pin | ||
accept_unpin_fut(bx.as_mut()); | ||
|
||
#[allow(clippy::let_underscore_future)] | ||
let _: Pin<&mut _> = std::pin::pin!(fut()); | ||
//~^ unnecessary_box_pin | ||
|
||
let bx = Box::pin(fut()); | ||
assert_static(|| bx); | ||
assert_static(|| Box::pin(fut())); | ||
|
||
std::pin::pin!(fut()).poll(cx); | ||
//~^ unnecessary_box_pin | ||
|
||
assert_static(identity(Box::pin(async {}))); | ||
|
||
let mut bx = std::pin::pin!(fut()); | ||
//~^ unnecessary_box_pin | ||
loop { | ||
bx.as_mut().poll(cx); | ||
} | ||
|
||
proc_macros::with_span! { | ||
span | ||
let mut bx = Box::pin(fut()); | ||
accept_unpin_fut(bx.as_mut()); | ||
} | ||
proc_macros::external! { | ||
let mut bx = Box::pin(fut()); | ||
accept_unpin_fut(bx.as_mut()); | ||
} | ||
} | ||
|
||
#[clippy::msrv = "1.67.0"] | ||
fn too_low_msrv() { | ||
let mut bx = Box::pin(fut()); | ||
accept_unpin_fut(bx.as_mut()); | ||
} | ||
|
||
fn main() {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
//@aux-build:proc_macros.rs | ||
#![warn(clippy::unnecessary_box_pin)] | ||
|
||
extern crate proc_macros; | ||
|
||
use std::convert::identity; | ||
use std::future::Future; | ||
use std::pin::Pin; | ||
use std::task::Context; | ||
|
||
async fn fut() {} | ||
|
||
fn accept_unpin_fut(_: impl Future + Unpin) {} | ||
|
||
fn assert_static(_: impl Send + Sync + 'static) {} | ||
|
||
fn test(cx: &mut Context<'_>) { | ||
accept_unpin_fut(Box::pin(fut()).as_mut()); | ||
//~^ unnecessary_box_pin | ||
|
||
let mut bx = Box::pin(fut()); | ||
//~^ unnecessary_box_pin | ||
accept_unpin_fut(bx.as_mut()); | ||
|
||
#[allow(clippy::let_underscore_future)] | ||
let _: Pin<&mut _> = Box::pin(fut()).as_mut(); | ||
//~^ unnecessary_box_pin | ||
|
||
let bx = Box::pin(fut()); | ||
assert_static(|| bx); | ||
assert_static(|| Box::pin(fut())); | ||
|
||
Box::pin(fut()).as_mut().poll(cx); | ||
//~^ unnecessary_box_pin | ||
|
||
assert_static(identity(Box::pin(async {}))); | ||
|
||
let mut bx = Box::pin(fut()); | ||
//~^ unnecessary_box_pin | ||
loop { | ||
bx.as_mut().poll(cx); | ||
} | ||
|
||
proc_macros::with_span! { | ||
span | ||
let mut bx = Box::pin(fut()); | ||
accept_unpin_fut(bx.as_mut()); | ||
} | ||
proc_macros::external! { | ||
let mut bx = Box::pin(fut()); | ||
accept_unpin_fut(bx.as_mut()); | ||
} | ||
} | ||
|
||
#[clippy::msrv = "1.67.0"] | ||
fn too_low_msrv() { | ||
let mut bx = Box::pin(fut()); | ||
accept_unpin_fut(bx.as_mut()); | ||
} | ||
|
||
fn main() {} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this (and the similar line in
book/src/README.md
) be captured in this patch?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wdym by captured?
cargo dev update_lints
automatically updates this and fails CI if this lint count is not up to date.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I didn't know that the CI would fail. I would consider this a problem though, as several proposed PR contain this mandatory change right now, while others don't and might need it if they are to be merged first.
Wouldn't it be better if this was updated at release time rather than when adding a lint? (and yes, this is unrelated to this particular PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's as much of a problem. Whichever PR gets merged first "wins" and all other PRs can just drop their change during the rebase. New lint PRs getting merged often create merge conflicts on most other lint PRs anyway so you often have to rebase either way.
Old PRs that don't have the lint count updated but need it will still fail when
r+
'd as bors makes sure that CI still passes with the PR integrated into master, and fixing it is very easy withcargo dev update_lints
.It also happens rather infrequently (last change on that line was 10 months ago) that I don't know if it's worth complicating this process (this number replacement is very little code in
update_lints
which already handles all other kinds of content generation).But if you want to discuss it more, I'd suggest opening a thread on Zulip