11use clippy_utils:: diagnostics:: span_lint_and_then;
2- use clippy_utils:: visitors:: for_each_local_use_after_expr;
3- use clippy_utils:: { fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, paths} ;
2+ use clippy_utils:: { fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, path_to_local_id, paths} ;
43use rustc_ast:: Mutability ;
54use rustc_errors:: Applicability ;
6- use rustc_hir:: intravisit:: { walk_expr, Visitor } ;
7- use rustc_hir:: { Expr , ExprKind , HirId , Local , Node , PatKind , Stmt , StmtKind } ;
5+ use rustc_hir:: intravisit:: { walk_block , walk_expr, walk_local , Visitor } ;
6+ use rustc_hir:: { Expr , ExprKind , HirId , LetStmt , Node , PatKind , Stmt , StmtKind } ;
87use rustc_lint:: { LateContext , LateLintPass } ;
98use rustc_session:: declare_lint_pass;
109use rustc_span:: sym;
1110use std:: ops:: ControlFlow ;
11+ use ControlFlow :: { Break , Continue } ;
1212
1313declare_clippy_lint ! {
1414 /// ### What it does
@@ -48,52 +48,21 @@ impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
4848 && match_def_path ( cx, child_adt. did ( ) , & paths:: CHILD )
4949 {
5050 match cx. tcx . parent_hir_node ( expr. hir_id ) {
51- Node :: Local ( local) if let PatKind :: Binding ( _, local_id, ..) = local. pat . kind => {
52- // If the `Child` is assigned to a variable, we want to check if the code never calls `.wait()`
53- // on the variable, and lint if not.
54- // This is difficult to do because expressions can be arbitrarily complex
55- // and the variable can "escape" in various ways, e.g. you can take a `&mut` to the variable
56- // and call `.wait()` through that, or pass it to another function...
57- // So instead we do the inverse, checking if all uses are either:
58- // - a field access (`child.{stderr,stdin,stdout}`)
59- // - calling `id` or `kill`
60- // - no use at all (e.g. `let _x = child;`)
61- // - taking a shared reference (`&`), `wait()` can't go through that
62- // None of these are sufficient to prevent zombie processes
63- // Doing it like this means more FNs, but FNs are better than FPs.
64- let has_no_wait = for_each_local_use_after_expr ( cx, local_id, expr. hir_id , |expr| {
65- match cx. tcx . parent_hir_node ( expr. hir_id ) {
66- Node :: Stmt ( Stmt {
67- kind : StmtKind :: Semi ( _) ,
68- ..
69- } ) => ControlFlow :: Continue ( ( ) ) ,
70- Node :: Expr ( expr) if let ExprKind :: Field ( ..) = expr. kind => ControlFlow :: Continue ( ( ) ) ,
71- Node :: Expr ( expr) if let ExprKind :: AddrOf ( _, Mutability :: Not , _) = expr. kind => {
72- ControlFlow :: Continue ( ( ) )
73- } ,
74- Node :: Expr ( expr)
75- if let Some ( fn_did) = fn_def_id ( cx, expr)
76- && match_any_def_paths ( cx, fn_did, & [ & paths:: CHILD_ID , & paths:: CHILD_KILL ] )
77- . is_some ( ) =>
78- {
79- ControlFlow :: Continue ( ( ) )
80- } ,
81-
82- // Conservatively assume that all other kinds of nodes call `.wait()` somehow.
83- _ => ControlFlow :: Break ( ( ) ) ,
84- }
85- } )
86- . is_continue ( ) ;
51+ Node :: LetStmt ( local)
52+ if let PatKind :: Binding ( _, local_id, ..) = local. pat . kind
53+ && let Some ( enclosing_block) = get_enclosing_block ( cx, expr. hir_id ) =>
54+ {
55+ let mut vis = WaitFinder :: WalkUpTo ( cx, local_id) ;
8756
8857 // If it does have a `wait()` call, we're done. Don't lint.
89- if !has_no_wait {
58+ if let Break ( BreakReason :: WaitFound ) = walk_block ( & mut vis , enclosing_block ) {
9059 return ;
9160 }
9261
9362 // Don't emit a suggestion since the binding is used later
9463 check ( cx, expr, local. hir_id , false ) ;
9564 } ,
96- Node :: Local ( & Local { pat, hir_id, .. } ) if let PatKind :: Wild = pat. kind => {
65+ Node :: LetStmt ( & LetStmt { pat, hir_id, .. } ) if let PatKind :: Wild = pat. kind => {
9766 // `let _ = child;`, also dropped immediately without `wait()`ing
9867 check ( cx, expr, hir_id, true ) ;
9968 } ,
@@ -111,6 +80,132 @@ impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
11180 }
11281}
11382
83+ enum BreakReason {
84+ WaitFound ,
85+ EarlyReturn ,
86+ }
87+
88+ /// A visitor responsible for finding a `wait()` call on a local variable.
89+ ///
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+ ///
98+ /// Note that this visitor does NOT explicitly look for `wait()` calls directly, but rather does the
99+ /// inverse -- checking if all uses of the local are either:
100+ /// - a field access (`child.{stderr,stdin,stdout}`)
101+ /// - calling `id` or `kill`
102+ /// - no use at all (e.g. `let _x = child;`)
103+ /// - taking a shared reference (`&`), `wait()` can't go through that
104+ /// None of these are sufficient to prevent zombie processes.
105+ /// Doing it like this means more FNs, but FNs are better than FPs.
106+ ///
107+ /// `return` expressions, conditional or not, short-circuit the visitor because
108+ /// if a `wait()` call hadn't been found at that point, it might never reach one at a later point:
109+ /// ```ignore
110+ /// let mut c = Command::new("").spawn().unwrap();
111+ /// if true {
112+ /// return; // Break(BreakReason::EarlyReturn)
113+ /// }
114+ /// c.wait(); // this might not be reachable
115+ /// ```
116+ enum WaitFinder < ' a , ' tcx > {
117+ WalkUpTo ( & ' a LateContext < ' tcx > , HirId ) ,
118+ Found ( & ' a LateContext < ' tcx > , HirId ) ,
119+ }
120+
121+ impl < ' a , ' tcx > Visitor < ' tcx > for WaitFinder < ' a , ' tcx > {
122+ type Result = ControlFlow < BreakReason > ;
123+
124+ fn visit_local ( & mut self , l : & ' tcx LetStmt < ' tcx > ) -> Self :: Result {
125+ if let Self :: WalkUpTo ( cx, local_id) = * self
126+ && let PatKind :: Binding ( _, pat_id, ..) = l. pat . kind
127+ && local_id == pat_id
128+ {
129+ * self = Self :: Found ( cx, local_id) ;
130+ }
131+
132+ walk_local ( self , l)
133+ }
134+
135+ fn visit_expr ( & mut self , ex : & ' tcx Expr < ' tcx > ) -> Self :: Result {
136+ let Self :: Found ( cx, local_id) = * self else {
137+ return walk_expr ( self , ex) ;
138+ } ;
139+
140+ if path_to_local_id ( ex, local_id) {
141+ match cx. tcx . parent_hir_node ( ex. hir_id ) {
142+ Node :: Stmt ( Stmt {
143+ kind : StmtKind :: Semi ( _) ,
144+ ..
145+ } ) => { } ,
146+ Node :: Expr ( expr) if let ExprKind :: Field ( ..) = expr. kind => { } ,
147+ Node :: Expr ( expr) if let ExprKind :: AddrOf ( _, Mutability :: Not , _) = expr. kind => { } ,
148+ Node :: Expr ( expr)
149+ if let Some ( fn_did) = fn_def_id ( cx, expr)
150+ && match_any_def_paths ( cx, fn_did, & [ & paths:: CHILD_ID , & paths:: CHILD_KILL ] ) . is_some ( ) => { } ,
151+
152+ // Conservatively assume that all other kinds of nodes call `.wait()` somehow.
153+ _ => return Break ( BreakReason :: WaitFound ) ,
154+ }
155+ } else {
156+ match ex. kind {
157+ ExprKind :: Ret ( ..) => return Break ( BreakReason :: EarlyReturn ) ,
158+ ExprKind :: If ( cond, then, None ) => {
159+ walk_expr ( self , cond) ?;
160+
161+ // A `wait()` call in an if expression with no else is not enough:
162+ //
163+ // let c = spawn();
164+ // if true {
165+ // c.wait();
166+ // }
167+ //
168+ // This might not call wait(). However, early returns are propagated,
169+ // because they might lead to a later wait() not being called.
170+ if let Break ( BreakReason :: EarlyReturn ) = walk_expr ( self , then) {
171+ return Break ( BreakReason :: EarlyReturn ) ;
172+ }
173+
174+ return Continue ( ( ) ) ;
175+ } ,
176+
177+ ExprKind :: If ( cond, then, Some ( else_) ) => {
178+ walk_expr ( self , cond) ?;
179+
180+ #[ expect( clippy:: unnested_or_patterns) ]
181+ match ( walk_expr ( self , then) , walk_expr ( self , else_) ) {
182+ ( Continue ( ( ) ) , Continue ( ( ) ) )
183+
184+ // `wait()` in one branch but nothing in the other does not count
185+ | ( Continue ( ( ) ) , Break ( BreakReason :: WaitFound ) )
186+ | ( Break ( BreakReason :: WaitFound ) , Continue ( ( ) ) ) => { } ,
187+
188+ // `wait()` in both branches is ok
189+ ( Break ( BreakReason :: WaitFound ) , Break ( BreakReason :: WaitFound ) ) => {
190+ return Break ( BreakReason :: WaitFound ) ;
191+ } ,
192+
193+ // Propagate early returns in either branch
194+ ( Break ( BreakReason :: EarlyReturn ) , _) | ( _, Break ( BreakReason :: EarlyReturn ) ) => {
195+ return Break ( BreakReason :: EarlyReturn ) ;
196+ } ,
197+ }
198+
199+ return Continue ( ( ) ) ;
200+ } ,
201+ _ => { } ,
202+ }
203+ }
204+
205+ walk_expr ( self , ex)
206+ }
207+ }
208+
114209/// This function has shared logic between the different kinds of nodes that can trigger the lint.
115210///
116211/// In particular, `let <binding> = <expr that spawns child>;` requires some custom additional logic
@@ -126,12 +221,10 @@ fn check<'tcx>(cx: &LateContext<'tcx>, spawn_expr: &'tcx Expr<'tcx>, node_id: Hi
126221
127222 let mut vis = ExitPointFinder {
128223 cx,
129- state : ExitPointState :: LookingForSpawnExpr ( spawn_expr. hir_id ) ,
224+ state : ExitPointState :: WalkUpTo ( spawn_expr. hir_id ) ,
130225 } ;
131- vis. visit_block ( block) ;
132-
133- // Visitor found an unconditional `exit()` call, so don't lint.
134- if let ExitPointState :: ExitFound = vis. state {
226+ if let Break ( ExitCallFound ) = vis. visit_block ( block) {
227+ // Visitor found an unconditional `exit()` call, so don't lint.
135228 return ;
136229 }
137230
@@ -194,7 +287,7 @@ fn is_exit_expression(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
194287#[ derive( Debug ) ]
195288enum ExitPointState {
196289 /// Still walking up to the expression that initiated the visitor.
197- LookingForSpawnExpr ( HirId ) ,
290+ WalkUpTo ( HirId ) ,
198291 /// We're inside of a control flow construct (e.g. `if`, `match`, `loop`)
199292 /// Within this, we shouldn't accept any `exit()` calls in here, but we can leave all of these
200293 /// constructs later and still continue looking for an `exit()` call afterwards. Example:
@@ -221,8 +314,6 @@ enum ExitPointState {
221314 InControlFlow { depth : u32 } ,
222315 /// No exit call found yet, but looking for one.
223316 NoExit ,
224- /// Found an expression that exits the process.
225- ExitFound ,
226317}
227318
228319fn expr_enters_control_flow ( expr : & Expr < ' _ > ) -> bool {
@@ -234,31 +325,37 @@ struct ExitPointFinder<'a, 'tcx> {
234325 cx : & ' a LateContext < ' tcx > ,
235326}
236327
328+ struct ExitCallFound ;
329+
237330impl < ' a , ' tcx > Visitor < ' tcx > for ExitPointFinder < ' a , ' tcx > {
238- fn visit_expr ( & mut self , expr : & ' tcx Expr < ' tcx > ) {
331+ type Result = ControlFlow < ExitCallFound > ;
332+
333+ fn visit_expr ( & mut self , expr : & ' tcx Expr < ' tcx > ) -> Self :: Result {
239334 match self . state {
240- ExitPointState :: LookingForSpawnExpr ( id) if expr. hir_id == id => {
335+ ExitPointState :: WalkUpTo ( id) if expr. hir_id == id => {
241336 self . state = ExitPointState :: NoExit ;
242- walk_expr ( self , expr) ;
337+ walk_expr ( self , expr)
243338 } ,
244339 ExitPointState :: NoExit if expr_enters_control_flow ( expr) => {
245340 self . state = ExitPointState :: InControlFlow { depth : 1 } ;
246- walk_expr ( self , expr) ;
341+ walk_expr ( self , expr) ? ;
247342 if let ExitPointState :: InControlFlow { .. } = self . state {
248343 self . state = ExitPointState :: NoExit ;
249344 }
345+ Continue ( ( ) )
250346 } ,
251- ExitPointState :: NoExit if is_exit_expression ( self . cx , expr) => self . state = ExitPointState :: ExitFound ,
347+ ExitPointState :: NoExit if is_exit_expression ( self . cx , expr) => Break ( ExitCallFound ) ,
252348 ExitPointState :: InControlFlow { ref mut depth } if expr_enters_control_flow ( expr) => {
253349 * depth += 1 ;
254- walk_expr ( self , expr) ;
350+ walk_expr ( self , expr) ? ;
255351 match self . state {
256352 ExitPointState :: InControlFlow { depth : 1 } => self . state = ExitPointState :: NoExit ,
257353 ExitPointState :: InControlFlow { ref mut depth } => * depth -= 1 ,
258354 _ => { } ,
259355 }
356+ Continue ( ( ) )
260357 } ,
261- _ => { } ,
358+ _ => Continue ( ( ) ) ,
262359 }
263360 }
264361}
0 commit comments