wowlab_tidy/languages/rust/rules/safety/
mutex_in_async.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "std Mutex in async fn",
12 code: "async fn f() { let _m = std::sync::Mutex::new(0); }",
13 pass: false,
14 },
15 Example {
16 label: "std Mutex in sync fn",
17 code: "fn f() { let _m = std::sync::Mutex::new(0); }",
18 pass: true,
19 },
20 Example {
21 label: "std Mutex type in async fn",
22 code: "async fn f() { let _m: std::sync::Mutex<i32> = std::sync::Mutex::new(0); }",
23 pass: false,
24 },
25 Example {
26 label: "Mutex in async in test",
27 code: "#[cfg(test)]\nmod tests {\n async fn t() { let _m = std::sync::Mutex::new(0); }\n}",
28 pass: true,
29 },
30];
31
32crate::ast_rule!(
33 mutex_in_async,
34 "Flag `std::sync::Mutex` usage in async functions (use tokio::sync::Mutex).",
35 "std::sync::Mutex blocks the entire async runtime thread while held. Use tokio::sync::Mutex in async code.",
36 High,
37);
38
39fn check_mutex_in_async(ctx: &AstCtx<'_>) -> Vec<Violation> {
40 let expression_paths = ctx
41 .nodes::<ast::PathExpr>()
42 .filter(|path| is_in_async_fn(path) && !ctx.is_in_test(path))
43 .filter(|path| path.path().is_some_and(|path| is_std_mutex_path(&path)));
44 let expression_violations = expression_paths
45 .map(|path| {
46 ctx.violation(
47 &path,
48 "std::sync::Mutex in async function — use tokio::sync::Mutex to avoid blocking the runtime",
49 )
50 });
51 let type_paths = ctx
52 .nodes::<ast::PathType>()
53 .filter(|path| is_in_async_fn(path) && !ctx.is_in_test(path))
54 .filter(|path| path.path().is_some_and(|path| is_std_mutex_path(&path)));
55 let type_violations = type_paths.map(|path| {
56 ctx.violation(
57 &path,
58 "std::sync::Mutex type in async function — use tokio::sync::Mutex",
59 )
60 });
61
62 expression_violations.chain(type_violations).collect()
63}
64
65fn is_in_async_fn<N>(node: &N) -> bool
66where
67 N: AstNode,
68{
69 node.syntax()
70 .ancestors()
71 .skip(1)
72 .find_map(ast::Fn::cast)
73 .is_some_and(|function| function.async_token().is_some())
74}
75
76fn is_std_mutex_path(path: &ast::Path) -> bool {
78 let current_is_mutex = path
79 .segment()
80 .and_then(|segment| segment.name_ref())
81 .is_some_and(|name| name.text() == "Mutex");
82 let sync_qualifier = path.qualifier().filter(|qualifier| {
83 qualifier
84 .segment()
85 .and_then(|segment| segment.name_ref())
86 .is_some_and(|name| name.text() == "sync")
87 });
88
89 if current_is_mutex && sync_qualifier.is_some() {
90 let parent_segment = sync_qualifier
91 .and_then(|sync| sync.qualifier())
92 .and_then(|qualifier| qualifier.segment());
93 let parent = parent_segment.and_then(|segment| segment.name_ref());
94
95 return parent.is_none_or(|name| name.text() != "tokio");
96 }
97
98 path.qualifier()
99 .is_some_and(|qualifier| is_std_mutex_path(&qualifier))
100}
101
102crate::tidy_ast_test!(check_mutex_in_async, {
103 crate::example_tests!(EXAMPLES, check_mutex_in_async);
104});