1
1
use 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} ;
4
3
use rustc_ast:: Mutability ;
5
4
use 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 } ;
8
7
use rustc_lint:: { LateContext , LateLintPass } ;
9
8
use rustc_session:: declare_lint_pass;
10
9
use rustc_span:: sym;
11
10
use std:: ops:: ControlFlow ;
11
+ use ControlFlow :: { Break , Continue } ;
12
12
13
13
declare_clippy_lint ! {
14
14
/// ### What it does
@@ -48,52 +48,21 @@ impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
48
48
&& match_def_path ( cx, child_adt. did ( ) , & paths:: CHILD )
49
49
{
50
50
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) ;
87
56
88
57
// 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 ) {
90
59
return ;
91
60
}
92
61
93
62
// Don't emit a suggestion since the binding is used later
94
63
check ( cx, expr, local. hir_id , false ) ;
95
64
} ,
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 => {
97
66
// `let _ = child;`, also dropped immediately without `wait()`ing
98
67
check ( cx, expr, hir_id, true ) ;
99
68
} ,
@@ -111,6 +80,132 @@ impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
111
80
}
112
81
}
113
82
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
+
114
209
/// This function has shared logic between the different kinds of nodes that can trigger the lint.
115
210
///
116
211
/// 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
126
221
127
222
let mut vis = ExitPointFinder {
128
223
cx,
129
- state : ExitPointState :: LookingForSpawnExpr ( spawn_expr. hir_id ) ,
224
+ state : ExitPointState :: WalkUpTo ( spawn_expr. hir_id ) ,
130
225
} ;
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.
135
228
return ;
136
229
}
137
230
@@ -194,7 +287,7 @@ fn is_exit_expression(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
194
287
#[ derive( Debug ) ]
195
288
enum ExitPointState {
196
289
/// Still walking up to the expression that initiated the visitor.
197
- LookingForSpawnExpr ( HirId ) ,
290
+ WalkUpTo ( HirId ) ,
198
291
/// We're inside of a control flow construct (e.g. `if`, `match`, `loop`)
199
292
/// Within this, we shouldn't accept any `exit()` calls in here, but we can leave all of these
200
293
/// constructs later and still continue looking for an `exit()` call afterwards. Example:
@@ -221,8 +314,6 @@ enum ExitPointState {
221
314
InControlFlow { depth : u32 } ,
222
315
/// No exit call found yet, but looking for one.
223
316
NoExit ,
224
- /// Found an expression that exits the process.
225
- ExitFound ,
226
317
}
227
318
228
319
fn expr_enters_control_flow ( expr : & Expr < ' _ > ) -> bool {
@@ -234,31 +325,37 @@ struct ExitPointFinder<'a, 'tcx> {
234
325
cx : & ' a LateContext < ' tcx > ,
235
326
}
236
327
328
+ struct ExitCallFound ;
329
+
237
330
impl < ' 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 {
239
334
match self . state {
240
- ExitPointState :: LookingForSpawnExpr ( id) if expr. hir_id == id => {
335
+ ExitPointState :: WalkUpTo ( id) if expr. hir_id == id => {
241
336
self . state = ExitPointState :: NoExit ;
242
- walk_expr ( self , expr) ;
337
+ walk_expr ( self , expr)
243
338
} ,
244
339
ExitPointState :: NoExit if expr_enters_control_flow ( expr) => {
245
340
self . state = ExitPointState :: InControlFlow { depth : 1 } ;
246
- walk_expr ( self , expr) ;
341
+ walk_expr ( self , expr) ? ;
247
342
if let ExitPointState :: InControlFlow { .. } = self . state {
248
343
self . state = ExitPointState :: NoExit ;
249
344
}
345
+ Continue ( ( ) )
250
346
} ,
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 ) ,
252
348
ExitPointState :: InControlFlow { ref mut depth } if expr_enters_control_flow ( expr) => {
253
349
* depth += 1 ;
254
- walk_expr ( self , expr) ;
350
+ walk_expr ( self , expr) ? ;
255
351
match self . state {
256
352
ExitPointState :: InControlFlow { depth : 1 } => self . state = ExitPointState :: NoExit ,
257
353
ExitPointState :: InControlFlow { ref mut depth } => * depth -= 1 ,
258
354
_ => { } ,
259
355
}
356
+ Continue ( ( ) )
260
357
} ,
261
- _ => { } ,
358
+ _ => Continue ( ( ) ) ,
262
359
}
263
360
}
264
361
}
0 commit comments