@@ -2,13 +2,14 @@ use ControlFlow::{Break, Continue};
22use  clippy_utils:: diagnostics:: span_lint_and_then; 
33use  clippy_utils:: { fn_def_id,  get_enclosing_block,  match_any_def_paths,  match_def_path,  path_to_local_id,  paths} ; 
44use  rustc_ast:: Mutability ; 
5+ use  rustc_ast:: visit:: visit_opt; 
56use  rustc_errors:: Applicability ; 
67use  rustc_hir:: intravisit:: { Visitor ,  walk_block,  walk_expr,  walk_local} ; 
78use  rustc_hir:: { Expr ,  ExprKind ,  HirId ,  LetStmt ,  Node ,  PatKind ,  Stmt ,  StmtKind } ; 
89use  rustc_lint:: { LateContext ,  LateLintPass } ; 
910use  rustc_middle:: hir:: nested_filter; 
1011use  rustc_session:: declare_lint_pass; 
11- use  rustc_span:: sym; 
12+ use  rustc_span:: { Span ,   sym} ; 
1213use  std:: ops:: ControlFlow ; 
1314
1415declare_clippy_lint !  { 
@@ -22,6 +23,17 @@ declare_clippy_lint! {
2223/// which can eventually lead to resource exhaustion, so it's recommended to call `wait()` in long-running applications. 
2324/// Such processes are called "zombie processes". 
2425/// 
26+ /// To reduce the rate of false positives, if the spawned process is assigned to a binding, the lint actually works the other way around; it 
27+ /// conservatively checks that all uses of a variable definitely don't call `wait()` and only then emits a warning. 
28+ /// For that reason, a seemingly unrelated use can get called out as calling `wait()` in help messages. 
29+ /// 
30+ /// ### Control flow 
31+ /// If a `wait()` call exists in an if/then block but not in the else block (or there is no else block), 
32+ /// then this still gets linted as not calling `wait()` in all code paths. 
33+ /// Likewise, when early-returning from the function, `wait()` calls that appear after the return expression 
34+ /// are also not accepted. 
35+ /// In other words, the `wait()` call must be unconditionally reachable after the spawn expression. 
36+ /// 
2537/// ### Example 
2638/// ```rust 
2739/// use std::process::Command; 
@@ -53,48 +65,58 @@ impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
5365                    if  let  PatKind :: Binding ( _,  local_id,  ..)  = local. pat . kind 
5466                        && let  Some ( enclosing_block)  = get_enclosing_block ( cx,  expr. hir_id )  =>
5567                { 
56-                     let  mut  vis = WaitFinder :: WalkUpTo ( cx,  local_id) ; 
57- 
58-                     // If it does have a `wait()` call, we're done. Don't lint. 
59-                     if  let  Break ( BreakReason :: WaitFound )  = walk_block ( & mut  vis,  enclosing_block)  { 
60-                         return ; 
61-                     } 
68+                     let  mut  vis = WaitFinder  { 
69+                         cx, 
70+                         local_id, 
71+                         state :  VisitorState :: WalkUpToLocal , 
72+                         early_return :  None , 
73+                         missing_wait_branch :  None , 
74+                     } ; 
75+ 
76+                     let  res = ( 
77+                         walk_block ( & mut  vis,  enclosing_block) , 
78+                         vis. missing_wait_branch , 
79+                         vis. early_return , 
80+                     ) ; 
81+ 
82+                     let  cause = match  res { 
83+                         ( Break ( MaybeWait ( wait_span) ) ,  _,  Some ( return_span) )  => { 
84+                             Cause :: EarlyReturn  {  wait_span,  return_span } 
85+                         } , 
86+                         ( Break ( MaybeWait ( _) ) ,  _,  None )  => return , 
87+                         ( Continue ( ( ) ) ,  None ,  _)  => Cause :: NeverWait , 
88+                         ( Continue ( ( ) ) ,  Some ( MissingWaitBranch :: MissingElse  {  if_span,  wait_span } ) ,  _)  => { 
89+                             Cause :: MissingElse  {  wait_span,  if_span } 
90+                         } , 
91+                         ( Continue ( ( ) ) ,  Some ( MissingWaitBranch :: MissingWaitInBranch  {  branch_span,  wait_span } ) ,  _)  => { 
92+                             Cause :: MissingWaitInBranch  {  wait_span,  branch_span } 
93+                         } , 
94+                     } ; 
6295
6396                    // Don't emit a suggestion since the binding is used later 
64-                     check ( cx,  expr,  false ) ; 
97+                     check ( cx,  expr,  cause ,   false ) ; 
6598                } , 
6699                Node :: LetStmt ( & LetStmt  {  pat,  .. } )  if  let  PatKind :: Wild  = pat. kind  => { 
67100                    // `let _ = child;`, also dropped immediately without `wait()`ing 
68-                     check ( cx,  expr,  true ) ; 
101+                     check ( cx,  expr,  Cause :: NeverWait ,   true ) ; 
69102                } , 
70103                Node :: Stmt ( & Stmt  { 
71104                    kind :  StmtKind :: Semi ( _) , 
72105                    ..
73106                } )  => { 
74107                    // Immediately dropped. E.g. `std::process::Command::new("echo").spawn().unwrap();` 
75-                     check ( cx,  expr,  true ) ; 
108+                     check ( cx,  expr,  Cause :: NeverWait ,   true ) ; 
76109                } , 
77110                _ => { } , 
78111            } 
79112        } 
80113    } 
81114} 
82115
83- enum  BreakReason  { 
84-     WaitFound , 
85-     EarlyReturn , 
86- } 
116+ struct  MaybeWait ( Span ) ; 
87117
88118/// A visitor responsible for finding a `wait()` call on a local variable. 
89119/// 
90- /// Conditional `wait()` calls are assumed to not call wait: 
91- /// ```ignore 
92- /// let mut c = Command::new("").spawn().unwrap(); 
93- /// if true { 
94- ///     c.wait(); 
95- /// } 
96- /// ``` 
97- /// 
98120/// Note that this visitor does NOT explicitly look for `wait()` calls directly, but rather does the 
99121/// inverse -- checking if all uses of the local are either: 
100122/// - a field access (`child.{stderr,stdin,stdout}`) 
@@ -104,73 +126,84 @@ enum BreakReason {
104126/// 
105127/// None of these are sufficient to prevent zombie processes. 
106128/// Doing it like this means more FNs, but FNs are better than FPs. 
107- /// 
108- /// `return` expressions, conditional or not, short-circuit the visitor because 
109- /// if a `wait()` call hadn't been found at that point, it might never reach one at a later point: 
110- /// ```ignore 
111- /// let mut c = Command::new("").spawn().unwrap(); 
112- /// if true { 
113- ///     return; // Break(BreakReason::EarlyReturn) 
114- /// } 
115- /// c.wait(); // this might not be reachable 
116- /// ``` 
117- enum  WaitFinder < ' a ,  ' tcx >  { 
118-     WalkUpTo ( & ' a  LateContext < ' tcx > ,  HirId ) , 
119-     Found ( & ' a  LateContext < ' tcx > ,  HirId ) , 
129+ struct  WaitFinder < ' a ,  ' tcx >  { 
130+     cx :  & ' a  LateContext < ' tcx > , 
131+     local_id :  HirId , 
132+     state :  VisitorState , 
133+     early_return :  Option < Span > , 
134+     // When joining two if branches where one of them doesn't call `wait()`, stores its span for more targetted help 
135+     // messages 
136+     missing_wait_branch :  Option < MissingWaitBranch > , 
137+ } 
138+ 
139+ #[ derive( PartialEq ) ]  
140+ enum  VisitorState  { 
141+     WalkUpToLocal , 
142+     LocalFound , 
143+ } 
144+ 
145+ #[ derive( Copy ,  Clone ) ]  
146+ enum  MissingWaitBranch  { 
147+     MissingElse  {  if_span :  Span ,  wait_span :  Span  } , 
148+     MissingWaitInBranch  {  branch_span :  Span ,  wait_span :  Span  } , 
120149} 
121150
122151impl < ' tcx >  Visitor < ' tcx >  for  WaitFinder < ' _ ,  ' tcx >  { 
123152    type  NestedFilter  = nested_filter:: OnlyBodies ; 
124-     type  Result  = ControlFlow < BreakReason > ; 
153+     type  Result  = ControlFlow < MaybeWait > ; 
125154
126155    fn  visit_local ( & mut  self ,  l :  & ' tcx  LetStmt < ' tcx > )  -> Self :: Result  { 
127-         if  let   Self :: WalkUpTo ( cx ,  local_id )  =  * self 
156+         if  self . state  ==  VisitorState :: WalkUpToLocal 
128157            && let  PatKind :: Binding ( _,  pat_id,  ..)  = l. pat . kind 
129-             && local_id == pat_id
158+             && self . local_id  == pat_id
130159        { 
131-             * self  = Self :: Found ( cx ,  local_id ) ; 
160+             self . state  = VisitorState :: LocalFound ; 
132161        } 
133162
134163        walk_local ( self ,  l) 
135164    } 
136165
137166    fn  visit_expr ( & mut  self ,  ex :  & ' tcx  Expr < ' tcx > )  -> Self :: Result  { 
138-         let   Self :: Found ( cx ,  local_id )  =  * self   else  { 
167+         if   self . state  !=  VisitorState :: LocalFound  { 
139168            return  walk_expr ( self ,  ex) ; 
140-         } ; 
169+         } 
141170
142-         if  path_to_local_id ( ex,  local_id)  { 
143-             match  cx. tcx . parent_hir_node ( ex. hir_id )  { 
171+         if  path_to_local_id ( ex,  self . local_id )  { 
172+             match  self . cx . tcx . parent_hir_node ( ex. hir_id )  { 
144173                Node :: Stmt ( Stmt  { 
145174                    kind :  StmtKind :: Semi ( _) , 
146175                    ..
147176                } )  => { } , 
148177                Node :: Expr ( expr)  if  let  ExprKind :: Field ( ..)  = expr. kind  => { } , 
149178                Node :: Expr ( expr)  if  let  ExprKind :: AddrOf ( _,  Mutability :: Not ,  _)  = expr. kind  => { } , 
150179                Node :: Expr ( expr) 
151-                     if  let  Some ( fn_did)  = fn_def_id ( cx,  expr) 
152-                         && match_any_def_paths ( cx,  fn_did,  & [ & paths:: CHILD_ID ,  & paths:: CHILD_KILL ] ) . is_some ( )  => { } , 
180+                     if  let  Some ( fn_did)  = fn_def_id ( self . cx ,  expr) 
181+                         && match_any_def_paths ( self . cx ,  fn_did,  & [ & paths:: CHILD_ID ,  & paths:: CHILD_KILL ] ) . is_some ( )  => { 
182+                 } , 
153183
154184                // Conservatively assume that all other kinds of nodes call `.wait()` somehow. 
155-                 _ => return  Break ( BreakReason :: WaitFound ) , 
185+                 _ => return  Break ( MaybeWait ( ex . span ) ) , 
156186            } 
157187        }  else  { 
158188            match  ex. kind  { 
159-                 ExprKind :: Ret ( ..)  => return  Break ( BreakReason :: EarlyReturn ) , 
189+                 ExprKind :: Ret ( e)  => { 
190+                     visit_opt ! ( self ,  visit_expr,  e) ; 
191+                     if  self . early_return . is_none ( )  { 
192+                         self . early_return  = Some ( ex. span ) ; 
193+                     } 
194+ 
195+                     return  Continue ( ( ) ) ; 
196+                 } , 
160197                ExprKind :: If ( cond,  then,  None )  => { 
161198                    walk_expr ( self ,  cond) ?; 
162199
163-                     // A `wait()` call in an if expression with no else is not enough: 
164-                     // 
165-                     // let c = spawn(); 
166-                     // if true { 
167-                     //   c.wait(); 
168-                     // } 
169-                     // 
170-                     // This might not call wait(). However, early returns are propagated, 
171-                     // because they might lead to a later wait() not being called. 
172-                     if  let  Break ( BreakReason :: EarlyReturn )  = walk_expr ( self ,  then)  { 
173-                         return  Break ( BreakReason :: EarlyReturn ) ; 
200+                     if  let  Break ( MaybeWait ( wait_span) )  = walk_expr ( self ,  then) 
201+                         && self . missing_wait_branch . is_none ( ) 
202+                     { 
203+                         self . missing_wait_branch  = Some ( MissingWaitBranch :: MissingElse  { 
204+                             if_span :  ex. span , 
205+                             wait_span, 
206+                         } ) ; 
174207                    } 
175208
176209                    return  Continue ( ( ) ) ; 
@@ -179,22 +212,31 @@ impl<'tcx> Visitor<'tcx> for WaitFinder<'_, 'tcx> {
179212                ExprKind :: If ( cond,  then,  Some ( else_) )  => { 
180213                    walk_expr ( self ,  cond) ?; 
181214
182-                     #[ expect( clippy:: unnested_or_patterns) ]  
183215                    match  ( walk_expr ( self ,  then) ,  walk_expr ( self ,  else_) )  { 
184-                         ( Continue ( ( ) ) ,  Continue ( ( ) ) ) 
216+                         ( Continue ( ( ) ) ,  Continue ( ( ) ) )  =>  { } , 
185217
186218                        // `wait()` in one branch but nothing in the other does not count 
187-                         | ( Continue ( ( ) ) ,  Break ( BreakReason :: WaitFound ) ) 
188-                         | ( Break ( BreakReason :: WaitFound ) ,  Continue ( ( ) ) )  => { } , 
189- 
190-                         // `wait()` in both branches is ok 
191-                         ( Break ( BreakReason :: WaitFound ) ,  Break ( BreakReason :: WaitFound ) )  => { 
192-                             return  Break ( BreakReason :: WaitFound ) ; 
219+                         ( Continue ( ( ) ) ,  Break ( MaybeWait ( wait_span) ) )  => { 
220+                             if  self . missing_wait_branch . is_none ( )  { 
221+                                 self . missing_wait_branch  = Some ( MissingWaitBranch :: MissingWaitInBranch  { 
222+                                     branch_span :  ex. span . shrink_to_lo ( ) . to ( then. span ) , 
223+                                     wait_span, 
224+                                 } ) ; 
225+                             } 
226+                         } , 
227+                         ( Break ( MaybeWait ( wait_span) ) ,  Continue ( ( ) ) )  => { 
228+                             if  self . missing_wait_branch . is_none ( )  { 
229+                                 self . missing_wait_branch  = Some ( MissingWaitBranch :: MissingWaitInBranch  { 
230+                                     branch_span :  then. span . shrink_to_hi ( ) . to ( else_. span ) , 
231+                                     wait_span, 
232+                                 } ) ; 
233+                             } 
193234                        } , 
194235
195-                         // Propagate early returns in either branch 
196-                         ( Break ( BreakReason :: EarlyReturn ) ,  _)  | ( _,  Break ( BreakReason :: EarlyReturn ) )  => { 
197-                             return  Break ( BreakReason :: EarlyReturn ) ; 
236+                         // `wait()` in both branches is ok 
237+                         ( Break ( MaybeWait ( wait_span) ) ,  Break ( MaybeWait ( _) ) )  => { 
238+                             self . missing_wait_branch  = None ; 
239+                             return  Break ( MaybeWait ( wait_span) ) ; 
198240                        } , 
199241                    } 
200242
@@ -208,8 +250,40 @@ impl<'tcx> Visitor<'tcx> for WaitFinder<'_, 'tcx> {
208250    } 
209251
210252    fn  nested_visit_map ( & mut  self )  -> Self :: Map  { 
211-         let  ( Self :: Found ( cx,  _)  | Self :: WalkUpTo ( cx,  _) )  = self ; 
212-         cx. tcx . hir ( ) 
253+         self . cx . tcx . hir ( ) 
254+     } 
255+ } 
256+ 
257+ #[ derive( Copy ,  Clone ) ]  
258+ enum  Cause  { 
259+     /// No call to `wait()` at all 
260+ NeverWait , 
261+     /// `wait()` call exists, but not all code paths definitely lead to one due to 
262+ /// an early return 
263+ EarlyReturn  {  wait_span :  Span ,  return_span :  Span  } , 
264+     /// `wait()` call exists in some if branches but not this one 
265+ MissingWaitInBranch  {  wait_span :  Span ,  branch_span :  Span  } , 
266+     /// `wait()` call exists in an if/then branch but it is missing an else block 
267+ MissingElse  {  wait_span :  Span ,  if_span :  Span  } , 
268+ } 
269+ 
270+ impl  Cause  { 
271+     fn  message ( self )  -> & ' static  str  { 
272+         match  self  { 
273+             Cause :: NeverWait  => "spawned process is never `wait()`ed on" , 
274+             Cause :: EarlyReturn  {  .. }  | Cause :: MissingWaitInBranch  {  .. }  | Cause :: MissingElse  {  .. }  => { 
275+                 "spawned process is not `wait()`ed on in all code paths" 
276+             } , 
277+         } 
278+     } 
279+ 
280+     fn  fallback_help ( self )  -> & ' static  str  { 
281+         match  self  { 
282+             Cause :: NeverWait  => "consider calling `.wait()`" , 
283+             Cause :: EarlyReturn  {  .. }  | Cause :: MissingWaitInBranch  {  .. }  | Cause :: MissingElse  {  .. }  => { 
284+                 "consider calling `.wait()` in all code paths" 
285+             } , 
286+         } 
213287    } 
214288} 
215289
@@ -220,7 +294,7 @@ impl<'tcx> Visitor<'tcx> for WaitFinder<'_, 'tcx> {
220294/// `let _ = <expr that spawns child>;`. 
221295/// 
222296/// This checks if the program doesn't unconditionally exit after the spawn expression. 
223- fn  check < ' tcx > ( cx :  & LateContext < ' tcx > ,  spawn_expr :  & ' tcx  Expr < ' tcx > ,  emit_suggestion :  bool )  { 
297+ fn  check < ' tcx > ( cx :  & LateContext < ' tcx > ,  spawn_expr :  & ' tcx  Expr < ' tcx > ,  cause :   Cause ,   emit_suggestion :  bool )  { 
224298    let  Some ( block)  = get_enclosing_block ( cx,  spawn_expr. hir_id )  else  { 
225299        return ; 
226300    } ; 
@@ -234,27 +308,46 @@ fn check<'tcx>(cx: &LateContext<'tcx>, spawn_expr: &'tcx Expr<'tcx>, emit_sugges
234308        return ; 
235309    } 
236310
237-     span_lint_and_then ( 
238-         cx, 
239-         ZOMBIE_PROCESSES , 
240-         spawn_expr. span , 
241-         "spawned process is never `wait()`ed on" , 
242-         |diag| { 
243-             if  emit_suggestion { 
244-                 diag. span_suggestion ( 
245-                     spawn_expr. span . shrink_to_hi ( ) , 
246-                     "try" , 
247-                     ".wait()" , 
248-                     Applicability :: MaybeIncorrect , 
311+     span_lint_and_then ( cx,  ZOMBIE_PROCESSES ,  spawn_expr. span ,  cause. message ( ) ,  |diag| { 
312+         match  cause { 
313+             Cause :: EarlyReturn  {  wait_span,  return_span }  => { 
314+                 diag. span_note ( 
315+                     return_span, 
316+                     "no `wait()` call exists on the code path to this early return" , 
249317                ) ; 
250-             }  else  { 
251-                 diag. note ( "consider calling `.wait()`" ) ; 
252-             } 
318+                 diag. span_note ( 
319+                     wait_span, 
320+                     "`wait()` call exists, but it is unreachable due to the early return" , 
321+                 ) ; 
322+             } , 
323+             Cause :: MissingWaitInBranch  {  wait_span,  branch_span }  => { 
324+                 diag. span_note ( branch_span,  "`wait()` is not called in this if branch" ) ; 
325+                 diag. span_note ( wait_span,  "`wait()` is called in the other branch" ) ; 
326+             } , 
327+             Cause :: MissingElse  {  if_span,  wait_span }  => { 
328+                 diag. span_note ( 
329+                     if_span, 
330+                     "this if expression has a `wait()` call, but it is missing an else block" , 
331+                 ) ; 
332+                 diag. span_note ( wait_span,  "`wait()` called here" ) ; 
333+             } , 
334+             Cause :: NeverWait  => { } , 
335+         } 
336+ 
337+         if  emit_suggestion { 
338+             diag. span_suggestion ( 
339+                 spawn_expr. span . shrink_to_hi ( ) , 
340+                 "try" , 
341+                 ".wait()" , 
342+                 Applicability :: MaybeIncorrect , 
343+             ) ; 
344+         }  else  { 
345+             diag. help ( cause. fallback_help ( ) ) ; 
346+         } 
253347
254-             diag. note ( "not doing so might leave behind zombie processes" ) 
255-                 . note ( "see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning" ) ; 
256-         } , 
257-     ) ; 
348+         diag. note ( "not doing so might leave behind zombie processes" ) 
349+             . note ( "see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning" ) ; 
350+     } ) ; 
258351} 
259352
260353/// Checks if the given expression exits the process. 
0 commit comments