Skip to content

Commit 26e245d

Browse files
committed
Auto merge of #10554 - samueltardieu:redundant-async-block, r=Jarcho
Make redundant_async_block a more complete late pass This lets us detect more complex situations: `async { x.await }` is simplified into `x` if: - `x` is an expression without side-effect - or `x` is an `async` block itself In both cases, no part of the `async` expression can be part of a macro expansion. Fixes #10509. Fixes #10525. changelog: [`redundant_async_block`] Do not lint expressions with side effects.
2 parents 5d149c5 + 2891d8f commit 26e245d

File tree

5 files changed

+338
-151
lines changed

5 files changed

+338
-151
lines changed

clippy_lints/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
945945
store.register_late_pass(|_| Box::new(no_mangle_with_rust_abi::NoMangleWithRustAbi));
946946
store.register_late_pass(|_| Box::new(collection_is_never_read::CollectionIsNeverRead));
947947
store.register_late_pass(|_| Box::new(missing_assert_message::MissingAssertMessage));
948-
store.register_early_pass(|| Box::new(redundant_async_block::RedundantAsyncBlock));
948+
store.register_late_pass(|_| Box::new(redundant_async_block::RedundantAsyncBlock));
949949
store.register_late_pass(|_| Box::new(let_with_type_underscore::UnderscoreTyped));
950950
store.register_late_pass(|_| Box::new(allow_attributes::AllowAttribute));
951951
store.register_late_pass(move |_| Box::new(manual_main_separator_str::ManualMainSeparatorStr::new(msrv())));
Lines changed: 67 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
use clippy_utils::{diagnostics::span_lint_and_sugg, source::snippet};
2-
use rustc_ast::ast::{Expr, ExprKind, Stmt, StmtKind};
3-
use rustc_ast::visit::Visitor as AstVisitor;
1+
use std::ops::ControlFlow;
2+
3+
use clippy_utils::{
4+
diagnostics::span_lint_and_sugg,
5+
peel_blocks,
6+
source::{snippet, walk_span_to_context},
7+
visitors::for_each_expr,
8+
};
49
use rustc_errors::Applicability;
5-
use rustc_lint::{EarlyContext, EarlyLintPass};
10+
use rustc_hir::{AsyncGeneratorKind, Closure, Expr, ExprKind, GeneratorKind, MatchSource};
11+
use rustc_lint::{LateContext, LateLintPass};
12+
use rustc_middle::{lint::in_external_macro, ty::UpvarCapture};
613
use rustc_session::{declare_lint_pass, declare_tool_lint};
714

815
declare_clippy_lint! {
@@ -14,106 +21,88 @@ declare_clippy_lint! {
1421
///
1522
/// ### Example
1623
/// ```rust
17-
/// async fn f() -> i32 {
18-
/// 1 + 2
19-
/// }
20-
///
24+
/// let f = async {
25+
/// 1 + 2
26+
/// };
2127
/// let fut = async {
22-
/// f().await
28+
/// f.await
2329
/// };
2430
/// ```
2531
/// Use instead:
2632
/// ```rust
27-
/// async fn f() -> i32 {
28-
/// 1 + 2
29-
/// }
30-
///
31-
/// let fut = f();
33+
/// let f = async {
34+
/// 1 + 2
35+
/// };
36+
/// let fut = f;
3237
/// ```
3338
#[clippy::version = "1.69.0"]
3439
pub REDUNDANT_ASYNC_BLOCK,
35-
nursery,
40+
complexity,
3641
"`async { future.await }` can be replaced by `future`"
3742
}
3843
declare_lint_pass!(RedundantAsyncBlock => [REDUNDANT_ASYNC_BLOCK]);
3944

40-
impl EarlyLintPass for RedundantAsyncBlock {
41-
fn check_expr(&mut self, cx: &EarlyContext<'_>, expr: &Expr) {
42-
if expr.span.from_expansion() {
43-
return;
44-
}
45-
if let ExprKind::Async(_, _, block) = &expr.kind && block.stmts.len() == 1 &&
46-
let Some(Stmt { kind: StmtKind::Expr(last), .. }) = block.stmts.last() &&
47-
let ExprKind::Await(future) = &last.kind &&
48-
!future.span.from_expansion() &&
49-
!await_in_expr(future)
45+
impl<'tcx> LateLintPass<'tcx> for RedundantAsyncBlock {
46+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
47+
let span = expr.span;
48+
if !in_external_macro(cx.tcx.sess, span) &&
49+
let Some(body_expr) = desugar_async_block(cx, expr) &&
50+
let Some(expr) = desugar_await(peel_blocks(body_expr)) &&
51+
// The await prefix must not come from a macro as its content could change in the future.
52+
expr.span.ctxt() == body_expr.span.ctxt() &&
53+
// An async block does not have immediate side-effects from a `.await` point-of-view.
54+
(!expr.can_have_side_effects() || desugar_async_block(cx, expr).is_some()) &&
55+
let Some(shortened_span) = walk_span_to_context(expr.span, span.ctxt())
5056
{
51-
if captures_value(last) {
52-
// If the async block captures variables then there is no equivalence.
53-
return;
54-
}
55-
5657
span_lint_and_sugg(
5758
cx,
5859
REDUNDANT_ASYNC_BLOCK,
59-
expr.span,
60+
span,
6061
"this async expression only awaits a single future",
6162
"you can reduce it to",
62-
snippet(cx, future.span, "..").into_owned(),
63+
snippet(cx, shortened_span, "..").into_owned(),
6364
Applicability::MachineApplicable,
6465
);
6566
}
6667
}
6768
}
6869

69-
/// Check whether an expression contains `.await`
70-
fn await_in_expr(expr: &Expr) -> bool {
71-
let mut detector = AwaitDetector::default();
72-
detector.visit_expr(expr);
73-
detector.await_found
74-
}
75-
76-
#[derive(Default)]
77-
struct AwaitDetector {
78-
await_found: bool,
79-
}
80-
81-
impl<'ast> AstVisitor<'ast> for AwaitDetector {
82-
fn visit_expr(&mut self, ex: &'ast Expr) {
83-
match (&ex.kind, self.await_found) {
84-
(ExprKind::Await(_), _) => self.await_found = true,
85-
(_, false) => rustc_ast::visit::walk_expr(self, ex),
86-
_ => (),
87-
}
70+
/// If `expr` is a desugared `async` block, return the original expression if it does not capture
71+
/// any variable by ref.
72+
fn desugar_async_block<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> {
73+
if let ExprKind::Closure(Closure { body, def_id, .. }) = expr.kind &&
74+
let body = cx.tcx.hir().body(*body) &&
75+
matches!(body.generator_kind, Some(GeneratorKind::Async(AsyncGeneratorKind::Block)))
76+
{
77+
cx
78+
.typeck_results()
79+
.closure_min_captures
80+
.get(def_id)
81+
.map_or(true, |m| {
82+
m.values().all(|places| {
83+
places
84+
.iter()
85+
.all(|place| matches!(place.info.capture_kind, UpvarCapture::ByValue))
86+
})
87+
})
88+
.then_some(body.value)
89+
} else {
90+
None
8891
}
8992
}
9093

91-
/// Check whether an expression may have captured a local variable.
92-
/// This is done by looking for paths with only one segment, except as
93-
/// a prefix of `.await` since this would be captured by value.
94-
///
95-
/// This function will sometimes return `true` even tough there are no
96-
/// captures happening: at the AST level, it is impossible to
97-
/// dinstinguish a function call from a call to a closure which comes
98-
/// from the local environment.
99-
fn captures_value(expr: &Expr) -> bool {
100-
let mut detector = CaptureDetector::default();
101-
detector.visit_expr(expr);
102-
detector.capture_found
103-
}
104-
105-
#[derive(Default)]
106-
struct CaptureDetector {
107-
capture_found: bool,
108-
}
109-
110-
impl<'ast> AstVisitor<'ast> for CaptureDetector {
111-
fn visit_expr(&mut self, ex: &'ast Expr) {
112-
match (&ex.kind, self.capture_found) {
113-
(ExprKind::Await(fut), _) if matches!(fut.kind, ExprKind::Path(..)) => (),
114-
(ExprKind::Path(_, path), _) if path.segments.len() == 1 => self.capture_found = true,
115-
(_, false) => rustc_ast::visit::walk_expr(self, ex),
116-
_ => (),
117-
}
94+
/// If `expr` is a desugared `.await`, return the original expression if it does not come from a
95+
/// macro expansion.
96+
fn desugar_await<'tcx>(expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> {
97+
if let ExprKind::Match(match_value, _, MatchSource::AwaitDesugar) = expr.kind &&
98+
let ExprKind::Call(_, [into_future_arg]) = match_value.kind &&
99+
let ctxt = expr.span.ctxt() &&
100+
for_each_expr(into_future_arg, |e|
101+
walk_span_to_context(e.span, ctxt)
102+
.map_or(ControlFlow::Break(()), |_| ControlFlow::Continue(()))).is_none()
103+
{
104+
Some(into_future_arg)
105+
} else {
106+
None
118107
}
119108
}

tests/ui/redundant_async_block.fixed

Lines changed: 115 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// run-rustfix
22

3-
#![allow(unused)]
3+
#![allow(unused, clippy::manual_async_fn)]
44
#![warn(clippy::redundant_async_block)]
55

66
use std::future::Future;
@@ -16,40 +16,16 @@ async fn func2() -> String {
1616
x.await
1717
}
1818

19-
macro_rules! await_in_macro {
20-
($e:expr) => {
21-
std::convert::identity($e).await
22-
};
23-
}
24-
25-
async fn func3(n: usize) -> usize {
26-
// Do not lint (suggestion would be `std::convert::identity(func1(n))`
27-
// which copies code from inside the macro)
28-
async move { await_in_macro!(func1(n)) }.await
29-
}
30-
31-
// This macro should never be linted as `$e` might contain `.await`
32-
macro_rules! async_await_parameter_in_macro {
33-
($e:expr) => {
34-
async { $e.await }
35-
};
36-
}
37-
38-
// MISSED OPPORTUNITY: this macro could be linted as the `async` block does not
39-
// contain code coming from the parameters
40-
macro_rules! async_await_in_macro {
41-
($f:expr) => {
42-
($f)(async { func2().await })
43-
};
44-
}
45-
4619
fn main() {
4720
let fut1 = async { 17 };
21+
// Lint
4822
let fut2 = fut1;
4923

5024
let fut1 = async { 25 };
25+
// Lint
5126
let fut2 = fut1;
5227

28+
// Lint
5329
let fut = async { 42 };
5430

5531
// Do not lint: not a single expression
@@ -60,15 +36,12 @@ fn main() {
6036

6137
// Do not lint: expression contains `.await`
6238
let fut = async { func1(func2().await.len()).await };
63-
64-
let fut = async_await_parameter_in_macro!(func2());
65-
let fut = async_await_in_macro!(std::convert::identity);
6639
}
6740

6841
#[allow(clippy::let_and_return)]
6942
fn capture_local() -> impl Future<Output = i32> {
70-
// Lint
7143
let fut = async { 17 };
44+
// Lint
7245
fut
7346
}
7447

@@ -80,11 +53,39 @@ fn capture_local_closure(s: &str) -> impl Future<Output = &str> {
8053

8154
#[allow(clippy::let_and_return)]
8255
fn capture_arg(s: &str) -> impl Future<Output = &str> {
83-
// Lint
8456
let fut = async move { s };
57+
// Lint
8558
fut
8659
}
8760

61+
fn capture_future_arg<T>(f: impl Future<Output = T>) -> impl Future<Output = T> {
62+
// Lint
63+
f
64+
}
65+
66+
fn capture_func_result<FN, F, T>(f: FN) -> impl Future<Output = T>
67+
where
68+
F: Future<Output = T>,
69+
FN: FnOnce() -> F,
70+
{
71+
// Do not lint, as f() would be evaluated prematurely
72+
async { f().await }
73+
}
74+
75+
fn double_future(f: impl Future<Output = impl Future<Output = u32>>) -> impl Future<Output = u32> {
76+
// Do not lint, we will get a `.await` outside a `.async`
77+
async { f.await.await }
78+
}
79+
80+
fn await_in_async<F, R>(f: F) -> impl Future<Output = u32>
81+
where
82+
F: FnOnce() -> R,
83+
R: Future<Output = u32>,
84+
{
85+
// Lint
86+
async { f().await + 1 }
87+
}
88+
8889
#[derive(Debug, Clone)]
8990
struct F {}
9091

@@ -109,3 +110,84 @@ fn capture() {
109110
// Do not lint: `val` would not live long enough
110111
spawn(async { work(&{ val }).await });
111112
}
113+
114+
fn await_from_macro() -> impl Future<Output = u32> {
115+
macro_rules! mac {
116+
($e:expr) => {
117+
$e.await
118+
};
119+
}
120+
// Do not lint: the macro may change in the future
121+
// or return different things depending on its argument
122+
async { mac!(async { 42 }) }
123+
}
124+
125+
fn async_expr_from_macro() -> impl Future<Output = u32> {
126+
macro_rules! mac {
127+
() => {
128+
async { 42 }
129+
};
130+
}
131+
// Do not lint: the macro may change in the future
132+
async { mac!().await }
133+
}
134+
135+
fn async_expr_from_macro_deep() -> impl Future<Output = u32> {
136+
macro_rules! mac {
137+
() => {
138+
async { 42 }
139+
};
140+
}
141+
// Do not lint: the macro may change in the future
142+
async { ({ mac!() }).await }
143+
}
144+
145+
fn all_from_macro() -> impl Future<Output = u32> {
146+
macro_rules! mac {
147+
() => {
148+
// Lint
149+
async { 42 }
150+
};
151+
}
152+
mac!()
153+
}
154+
155+
fn parts_from_macro() -> impl Future<Output = u32> {
156+
macro_rules! mac {
157+
($e: expr) => {
158+
// Do not lint: `$e` might not always be side-effect free
159+
async { $e.await }
160+
};
161+
}
162+
mac!(async { 42 })
163+
}
164+
165+
fn safe_parts_from_macro() -> impl Future<Output = u32> {
166+
macro_rules! mac {
167+
($e: expr) => {
168+
// Lint
169+
async { $e }
170+
};
171+
}
172+
mac!(42)
173+
}
174+
175+
fn parts_from_macro_deep() -> impl Future<Output = u32> {
176+
macro_rules! mac {
177+
($e: expr) => {
178+
// Do not lint: `$e` might not always be side-effect free
179+
async { ($e,).0.await }
180+
};
181+
}
182+
let f = std::future::ready(42);
183+
mac!(f)
184+
}
185+
186+
fn await_from_macro_deep() -> impl Future<Output = u32> {
187+
macro_rules! mac {
188+
($e:expr) => {{ $e }.await};
189+
}
190+
// Do not lint: the macro may change in the future
191+
// or return different things depending on its argument
192+
async { mac!(async { 42 }) }
193+
}

0 commit comments

Comments
 (0)