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