@@ -18,14 +18,17 @@ dylint_linting::declare_late_lint! {
1818 ///
1919 /// Detects blocking waits in actor event loops, such as awaiting an actor
2020 /// mailbox request/reply method or an async handler callback inside
21- /// `select_loop!`.
21+ /// `select_loop!`. It also detects async mailbox request/reply helpers that
22+ /// create a oneshot channel, enqueue a message with the sender, and then
23+ /// await the receiver before returning.
2224 ///
2325 /// ### Why is this bad?
2426 ///
2527 /// Actor loops must keep polling their mailbox and other event sources.
2628 /// Awaiting another actor or an application callback from a branch body can
27- /// starve the loop. Spawn or pool the work and select on its completion
28- /// instead.
29+ /// starve the loop. Hiding the wait behind an async mailbox helper makes it
30+ /// too easy for actor loops to block accidentally. Spawn or pool the work
31+ /// and select on its completion instead.
2932 ///
3033 /// ### Example
3134 ///
@@ -62,6 +65,20 @@ impl<'tcx> LateLintPass<'tcx> for BlockingInActorLoop {
6265 Some ( kind) => BlockingAwaitVisitor { cx, kind } . visit_expr ( body. value ) ,
6366 None => { }
6467 }
68+
69+ if let Some ( span) = request_reply_helper_wait ( cx, body) {
70+ cx. emit_span_lint (
71+ BLOCKING_IN_ACTOR_LOOP ,
72+ span,
73+ DiagDecorator ( |diag| {
74+ diag. primary_message ( "async mailbox request/reply helper hides an actor wait" ) ;
75+ diag. span_help (
76+ span,
77+ "return the oneshot receiver and let callers select or await explicitly" ,
78+ ) ;
79+ } ) ,
80+ ) ;
81+ }
6582 }
6683}
6784
@@ -135,6 +152,50 @@ fn actor_loop_kind(cx: &LateContext<'_>, body: &Body<'_>) -> Option<ActorLoopKin
135152 None
136153}
137154
155+ fn request_reply_helper_wait ( cx : & LateContext < ' _ > , body : & Body < ' _ > ) -> Option < Span > {
156+ let snippet = cx
157+ . sess ( )
158+ . source_map ( )
159+ . span_to_snippet ( body. value . span )
160+ . ok ( ) ?;
161+
162+ if !snippet. contains ( "oneshot::channel" )
163+ || !snippet. contains ( ".enqueue(Message::" )
164+ || !snippet. contains ( ".await" )
165+ {
166+ return None ;
167+ }
168+
169+ AwaitVisitor :: default ( ) . await_span ( body. value )
170+ }
171+
172+ #[ derive( Default ) ]
173+ struct AwaitVisitor {
174+ span : Option < Span > ,
175+ }
176+
177+ impl AwaitVisitor {
178+ fn await_span < ' tcx > ( mut self , expr : & ' tcx Expr < ' tcx > ) -> Option < Span > {
179+ self . visit_expr ( expr) ;
180+ self . span
181+ }
182+ }
183+
184+ impl < ' tcx > Visitor < ' tcx > for AwaitVisitor {
185+ fn visit_expr ( & mut self , expr : & ' tcx Expr < ' tcx > ) {
186+ if self . span . is_some ( ) {
187+ return ;
188+ }
189+
190+ if matches ! ( expr. kind, ExprKind :: Match ( _, _, MatchSource :: AwaitDesugar ) ) {
191+ self . span = Some ( expr. span ) ;
192+ return ;
193+ }
194+
195+ walk_expr ( self , expr) ;
196+ }
197+ }
198+
138199fn awaited_call < ' tcx > ( expr : & ' tcx Expr < ' tcx > ) -> Option < AwaitedCall < ' tcx > > {
139200 let ExprKind :: Match ( awaited, _, MatchSource :: AwaitDesugar ) = expr. kind else {
140201 return None ;
0 commit comments