@@ -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