Skip to main content

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

1use ra_ap_syntax::{
2    AstNode, SyntaxNode,
3    ast::{self, HasName},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10    Example {
11        label: "Rc::new held across await",
12        code: "async fn f() { let rc = Rc::new(1); g().await; }",
13        pass: false,
14    },
15    Example {
16        label: "RefCell::new held across await",
17        code: "async fn f() { let cell = RefCell::new(1); g().await; }",
18        pass: false,
19    },
20    Example {
21        label: "Rc::clone held across await",
22        code: "async fn f(other: &Rc<u8>) { let rc = Rc::clone(other); g().await; }",
23        pass: false,
24    },
25    Example {
26        label: "Rc in async block before await",
27        code: "fn f() { let fut = async { let rc = Rc::new(1); g().await; }; }",
28        pass: false,
29    },
30    Example {
31        label: "await before the Rc binding",
32        code: "async fn f() { g().await; let rc = Rc::new(1); }",
33        pass: true,
34    },
35    Example {
36        label: "Arc is Send",
37        code: "async fn f() { let arc = Arc::new(1); g().await; }",
38        pass: true,
39    },
40    Example {
41        label: "Rc in sync fn",
42        code: "fn f() { let rc = Rc::new(1); }",
43        pass: true,
44    },
45    Example {
46        label: "await only inside a nested async block",
47        code: "async fn f() { let rc = Rc::new(1); let fut = async { g().await; }; }",
48        pass: true,
49    },
50    Example {
51        label: "Rc across await in test module",
52        code: "#[cfg(test)]\nmod tests {\n    async fn f() { let rc = Rc::new(1); g().await; }\n}",
53        pass: true,
54    },
55];
56
57crate::ast_rule!(
58    nonsend_across_await,
59    "Flag `Rc`/`RefCell` bindings in async code when an `.await` occurs later in the same block.",
60    "!Send values held across an await point make the entire future !Send, breaking Tokio and other work-stealing runtimes (M-TYPES-SEND).",
61    Medium,
62);
63
64fn check_nonsend_across_await(ctx: &AstCtx<'_>) -> Vec<Violation> {
65    let mut violations = Vec::new();
66
67    for function in ctx.nodes::<ast::Fn>().filter(|function| {
68        super::support::is_item_or_impl_fn(function)
69            && !ctx.is_in_test(function)
70            && function.async_token().is_some()
71    }) {
72        if let Some(block) = function.body() {
73            scan_block(ctx, &block, &mut violations);
74        }
75    }
76
77    for block in ctx
78        .nodes::<ast::BlockExpr>()
79        .filter(|block| !ctx.is_in_test(block) && block.async_token().is_some())
80    {
81        scan_block(ctx, &block, &mut violations);
82    }
83
84    violations
85}
86
87fn ctor_label(path: ast::Path) -> Option<&'static str> {
88    let names = super::support::path_names(path);
89    let mut rev = names.iter().rev();
90    let method = rev.next()?;
91    let ty = rev.next()?;
92
93    if ty == "Rc" && method == "new" {
94        return Some("Rc::new");
95    }
96
97    if ty == "Rc" && method == "clone" {
98        return Some("Rc::clone");
99    }
100
101    if ty == "RefCell" && method == "new" {
102        return Some("RefCell::new");
103    }
104
105    None
106}
107
108fn nonsend_init(stmt: &ast::Stmt) -> Option<(String, &'static str)> {
109    let ast::Stmt::LetStmt(local) = stmt else {
110        return None;
111    };
112    let ast::Expr::CallExpr(call) = local.initializer()? else {
113        return None;
114    };
115    let ast::Expr::PathExpr(func) = call.expr()? else {
116        return None;
117    };
118    let ctor = ctor_label(func.path()?)?;
119    let name = match local.pat()? {
120        ast::Pat::IdentPat(pat) => pat.name()?.text().to_string(),
121        _ => "binding".to_string(),
122    };
123
124    Some((name, ctor))
125}
126
127fn stmt_awaits(stmt: &ast::Stmt) -> bool {
128    node_awaits(stmt.syntax())
129}
130
131fn node_awaits(root: &SyntaxNode) -> bool {
132    root.descendants()
133        .filter_map(ast::AwaitExpr::cast)
134        .any(|await_expr| {
135            !await_expr
136                .syntax()
137                .ancestors()
138                .skip(1)
139                .take_while(|node| node != root)
140                .any(|node| {
141                    ast::ClosureExpr::cast(node.clone()).is_some()
142                        || ast::Item::cast(node.clone()).is_some()
143                        || ast::BlockExpr::cast(node)
144                            .is_some_and(|block| block.async_token().is_some())
145                })
146        })
147}
148
149fn scan_block(ctx: &AstCtx<'_>, block: &ast::BlockExpr, violations: &mut Vec<Violation>) {
150    let Some(list) = block.stmt_list() else {
151        return;
152    };
153    let statements: Vec<_> = list.statements().collect();
154    let mut awaits: Vec<bool> = statements.iter().map(stmt_awaits).collect();
155
156    awaits.extend(
157        list.tail_expr()
158            .map(|expression| node_awaits(expression.syntax())),
159    );
160    let found: Vec<Violation> = statements
161        .iter()
162        .enumerate()
163        .filter(|&(i, _)| awaits.iter().skip(i + 1).any(|&later| later))
164        .filter_map(|(_, stmt)| {
165            nonsend_init(stmt).map(|(name, ctor)| {
166                ctx.violation(
167                    stmt,
168                    format!(
169                        "`{name}` is created with `{ctor}` and held across a later `.await` — !Send values across await points make the whole future !Send"
170                    ),
171                )
172            })
173        })
174        .collect();
175
176    violations.extend(found);
177}
178
179crate::tidy_ast_test!(check_nonsend_across_await, {
180    crate::example_tests!(EXAMPLES, check_nonsend_across_await);
181});