wowlab_tidy/languages/rust/rules/interop/
manual_async_fn.rs1use 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});