Skip to main content

wowlab_tidy/languages/rust/rules/interop/
manual_async_fn.rs

1use ra_ap_syntax::ast::{self, HasName, TypeBoundKind};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7    Example {
8        label: "impl Future wrapping one async block",
9        code: "fn f() -> impl Future<Output = u8> { async { 1 } }",
10        pass: false,
11    },
12    Example {
13        label: "qualified Future with async move block",
14        code: "fn f() -> impl std::future::Future<Output = ()> { async move { } }",
15        pass: false,
16    },
17    Example {
18        label: "async fn",
19        code: "async fn f() -> u8 { 1 }",
20        pass: true,
21    },
22    Example {
23        label: "body does more than one async block",
24        code: "fn f() -> impl Future<Output = u8> { let x = 1; async move { x } }",
25        pass: true,
26    },
27    Example {
28        label: "trait declaration",
29        code: "trait T { fn f() -> impl Future<Output = ()>; }",
30        pass: true,
31    },
32    Example {
33        label: "trait default body",
34        code: "trait T { fn f() -> impl Future<Output = ()> { async { } } }",
35        pass: true,
36    },
37    Example {
38        label: "trait impl block",
39        code: "struct S;\nimpl T for S { fn f() -> impl Future<Output = ()> { async { } } }",
40        pass: true,
41    },
42    Example {
43        label: "inherent impl method",
44        code: "struct S;\nimpl S { fn f() -> impl Future<Output = ()> { async { } } }",
45        pass: false,
46    },
47    Example {
48        label: "non-Future impl trait return",
49        code: "fn f() -> impl Iterator<Item = u8> { std::iter::once(1) }",
50        pass: true,
51    },
52    Example {
53        label: "manual future in test module",
54        code: "#[cfg(test)]\nmod tests {\n    fn f() -> impl Future<Output = ()> { async { } }\n}",
55        pass: true,
56    },
57];
58
59crate::ast_rule!(
60    manual_async_fn,
61    "Flag non-async functions that return `impl Future` by wrapping the whole body in one `async` block.",
62    "`async fn` reads normally and avoids signature noise; explicit `impl Future` returns are only warranted in traits or for hot-path size control (M-ASYNC-FN).",
63    Low,
64);
65
66fn check_manual_async_fn(ctx: &AstCtx<'_>) -> Vec<Violation> {
67    let eligible_functions = ctx
68        .nodes::<ast::Fn>()
69        .filter(|function| {
70            super::support::is_item_or_impl_fn(function) && !ctx.is_in_test(function)
71        })
72        .filter(|function| !super::support::is_inside_trait(function));
73    let future_functions = eligible_functions
74        .filter(|function| {
75            super::support::enclosing_impl(function)
76                .is_none_or(|item_impl| item_impl.trait_().is_none())
77        })
78        .filter(|function| function.async_token().is_none() && returns_impl_future(function));
79
80    future_functions
81        .filter(body_is_single_async_block)
82        .filter_map(|function| {
83            let name = function.name()?;
84
85            Some(ctx.violation(
86                &name,
87                format!(
88                    "fn `{name}` returns `impl Future` from a single `async` block — declare it `async fn` instead"
89                ),
90            ))
91        })
92        .collect()
93}
94
95fn returns_impl_future(function: &ast::Fn) -> bool {
96    let Some(ast::Type::ImplTraitType(impl_trait)) = function.ret_type().and_then(|ret| ret.ty())
97    else {
98        return false;
99    };
100
101    impl_trait.type_bound_list().is_some_and(|bounds| {
102        bounds.bounds().any(|bound| {
103            matches!(
104                bound.kind(),
105                Some(TypeBoundKind::PathType(_, path_type))
106                    if path_type.path().is_some_and(|path| super::support::path_last_is(path, "Future"))
107            )
108        })
109    })
110}
111
112fn body_is_single_async_block(function: &ast::Fn) -> bool {
113    let Some(statements) = function.body().and_then(|body| body.stmt_list()) else {
114        return false;
115    };
116    let stmts: Vec<_> = statements.statements().collect();
117
118    match (stmts.as_slice(), statements.tail_expr()) {
119        ([], Some(ast::Expr::BlockExpr(block))) => block.async_token().is_some(),
120        ([ast::Stmt::ExprStmt(statement)], None) => {
121            matches!(statement.expr(), Some(ast::Expr::BlockExpr(block)) if block.async_token().is_some())
122        }
123        _ => false,
124    }
125}
126
127crate::tidy_ast_test!(check_manual_async_fn, {
128    crate::example_tests!(EXAMPLES, check_manual_async_fn);
129});