summaryrefslogtreecommitdiffstats
path: root/src/tools/clippy/clippy_lints/src/redundant_async_block.rs
blob: 05e52e6b38b1294a16c8cb1032d72f4148149b30 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
use std::ops::ControlFlow;

use clippy_utils::{
    diagnostics::span_lint_and_sugg,
    peel_blocks,
    source::{snippet, walk_span_to_context},
    visitors::for_each_expr,
};
use rustc_errors::Applicability;
use rustc_hir::{AsyncGeneratorKind, Closure, Expr, ExprKind, GeneratorKind, MatchSource};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::{lint::in_external_macro, ty::UpvarCapture};
use rustc_session::{declare_lint_pass, declare_tool_lint};

declare_clippy_lint! {
    /// ### What it does
    /// Checks for `async` block that only returns `await` on a future.
    ///
    /// ### Why is this bad?
    /// It is simpler and more efficient to use the future directly.
    ///
    /// ### Example
    /// ```rust
    /// let f = async {
    ///    1 + 2
    /// };
    /// let fut = async {
    ///     f.await
    /// };
    /// ```
    /// Use instead:
    /// ```rust
    /// let f = async {
    ///    1 + 2
    /// };
    /// let fut = f;
    /// ```
    #[clippy::version = "1.70.0"]
    pub REDUNDANT_ASYNC_BLOCK,
    complexity,
    "`async { future.await }` can be replaced by `future`"
}
declare_lint_pass!(RedundantAsyncBlock => [REDUNDANT_ASYNC_BLOCK]);

impl<'tcx> LateLintPass<'tcx> for RedundantAsyncBlock {
    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
        let span = expr.span;
        if !in_external_macro(cx.tcx.sess, span) &&
            let Some(body_expr) = desugar_async_block(cx, expr) &&
            let Some(expr) = desugar_await(peel_blocks(body_expr)) &&
            // The await prefix must not come from a macro as its content could change in the future.
            expr.span.ctxt() == body_expr.span.ctxt() &&
            // An async block does not have immediate side-effects from a `.await` point-of-view.
            (!expr.can_have_side_effects() || desugar_async_block(cx, expr).is_some()) &&
            let Some(shortened_span) = walk_span_to_context(expr.span, span.ctxt())
        {
            span_lint_and_sugg(
                cx,
                REDUNDANT_ASYNC_BLOCK,
                span,
                "this async expression only awaits a single future",
                "you can reduce it to",
                snippet(cx, shortened_span, "..").into_owned(),
                Applicability::MachineApplicable,
            );
        }
    }
}

/// If `expr` is a desugared `async` block, return the original expression if it does not capture
/// any variable by ref.
fn desugar_async_block<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> {
    if let ExprKind::Closure(Closure { body, def_id, .. }) = expr.kind &&
        let body = cx.tcx.hir().body(*body) &&
        matches!(body.generator_kind, Some(GeneratorKind::Async(AsyncGeneratorKind::Block)))
    {
        cx
            .typeck_results()
            .closure_min_captures
            .get(def_id)
            .map_or(true, |m| {
                m.values().all(|places| {
                    places
                        .iter()
                        .all(|place| matches!(place.info.capture_kind, UpvarCapture::ByValue))
                })
            })
            .then_some(body.value)
    } else {
        None
    }
}

/// If `expr` is a desugared `.await`, return the original expression if it does not come from a
/// macro expansion.
fn desugar_await<'tcx>(expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> {
    if let ExprKind::Match(match_value, _, MatchSource::AwaitDesugar) = expr.kind &&
        let ExprKind::Call(_, [into_future_arg]) = match_value.kind &&
        let ctxt = expr.span.ctxt() &&
        for_each_expr(into_future_arg, |e|
            walk_span_to_context(e.span, ctxt)
                .map_or(ControlFlow::Break(()), |_| ControlFlow::Continue(()))).is_none()
    {
        Some(into_future_arg)
    } else {
        None
    }
}