@@ -2,13 +2,14 @@ use ControlFlow::{Break, Continue};
2
2
use clippy_utils:: diagnostics:: span_lint_and_then;
3
3
use clippy_utils:: { fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, path_to_local_id, paths} ;
4
4
use rustc_ast:: Mutability ;
5
+ use rustc_ast:: visit:: visit_opt;
5
6
use rustc_errors:: Applicability ;
6
7
use rustc_hir:: intravisit:: { Visitor , walk_block, walk_expr, walk_local} ;
7
8
use rustc_hir:: { Expr , ExprKind , HirId , LetStmt , Node , PatKind , Stmt , StmtKind } ;
8
9
use rustc_lint:: { LateContext , LateLintPass } ;
9
10
use rustc_middle:: hir:: nested_filter;
10
11
use rustc_session:: declare_lint_pass;
11
- use rustc_span:: sym;
12
+ use rustc_span:: { Span , sym} ;
12
13
use std:: ops:: ControlFlow ;
13
14
14
15
declare_clippy_lint ! {
@@ -22,6 +23,17 @@ declare_clippy_lint! {
22
23
/// which can eventually lead to resource exhaustion, so it's recommended to call `wait()` in long-running applications.
23
24
/// Such processes are called "zombie processes".
24
25
///
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
+ ///
25
37
/// ### Example
26
38
/// ```rust
27
39
/// use std::process::Command;
@@ -53,48 +65,58 @@ impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
53
65
if let PatKind :: Binding ( _, local_id, ..) = local. pat . kind
54
66
&& let Some ( enclosing_block) = get_enclosing_block ( cx, expr. hir_id ) =>
55
67
{
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
+ } ;
62
95
63
96
// Don't emit a suggestion since the binding is used later
64
- check ( cx, expr, false ) ;
97
+ check ( cx, expr, cause , false ) ;
65
98
} ,
66
99
Node :: LetStmt ( & LetStmt { pat, .. } ) if let PatKind :: Wild = pat. kind => {
67
100
// `let _ = child;`, also dropped immediately without `wait()`ing
68
- check ( cx, expr, true ) ;
101
+ check ( cx, expr, Cause :: NeverWait , true ) ;
69
102
} ,
70
103
Node :: Stmt ( & Stmt {
71
104
kind : StmtKind :: Semi ( _) ,
72
105
..
73
106
} ) => {
74
107
// Immediately dropped. E.g. `std::process::Command::new("echo").spawn().unwrap();`
75
- check ( cx, expr, true ) ;
108
+ check ( cx, expr, Cause :: NeverWait , true ) ;
76
109
} ,
77
110
_ => { } ,
78
111
}
79
112
}
80
113
}
81
114
}
82
115
83
- enum BreakReason {
84
- WaitFound ,
85
- EarlyReturn ,
86
- }
116
+ struct MaybeWait ( Span ) ;
87
117
88
118
/// A visitor responsible for finding a `wait()` call on a local variable.
89
119
///
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
120
/// Note that this visitor does NOT explicitly look for `wait()` calls directly, but rather does the
99
121
/// inverse -- checking if all uses of the local are either:
100
122
/// - a field access (`child.{stderr,stdin,stdout}`)
@@ -104,73 +126,84 @@ enum BreakReason {
104
126
///
105
127
/// None of these are sufficient to prevent zombie processes.
106
128
/// 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 } ,
120
149
}
121
150
122
151
impl < ' tcx > Visitor < ' tcx > for WaitFinder < ' _ , ' tcx > {
123
152
type NestedFilter = nested_filter:: OnlyBodies ;
124
- type Result = ControlFlow < BreakReason > ;
153
+ type Result = ControlFlow < MaybeWait > ;
125
154
126
155
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
128
157
&& let PatKind :: Binding ( _, pat_id, ..) = l. pat . kind
129
- && local_id == pat_id
158
+ && self . local_id == pat_id
130
159
{
131
- * self = Self :: Found ( cx , local_id ) ;
160
+ self . state = VisitorState :: LocalFound ;
132
161
}
133
162
134
163
walk_local ( self , l)
135
164
}
136
165
137
166
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 {
139
168
return walk_expr ( self , ex) ;
140
- } ;
169
+ }
141
170
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 ) {
144
173
Node :: Stmt ( Stmt {
145
174
kind : StmtKind :: Semi ( _) ,
146
175
..
147
176
} ) => { } ,
148
177
Node :: Expr ( expr) if let ExprKind :: Field ( ..) = expr. kind => { } ,
149
178
Node :: Expr ( expr) if let ExprKind :: AddrOf ( _, Mutability :: Not , _) = expr. kind => { } ,
150
179
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
+ } ,
153
183
154
184
// Conservatively assume that all other kinds of nodes call `.wait()` somehow.
155
- _ => return Break ( BreakReason :: WaitFound ) ,
185
+ _ => return Break ( MaybeWait ( ex . span ) ) ,
156
186
}
157
187
} else {
158
188
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
+ } ,
160
197
ExprKind :: If ( cond, then, None ) => {
161
198
walk_expr ( self , cond) ?;
162
199
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
+ } ) ;
174
207
}
175
208
176
209
return Continue ( ( ) ) ;
@@ -179,22 +212,31 @@ impl<'tcx> Visitor<'tcx> for WaitFinder<'_, 'tcx> {
179
212
ExprKind :: If ( cond, then, Some ( else_) ) => {
180
213
walk_expr ( self , cond) ?;
181
214
182
- #[ expect( clippy:: unnested_or_patterns) ]
183
215
match ( walk_expr ( self , then) , walk_expr ( self , else_) ) {
184
- ( Continue ( ( ) ) , Continue ( ( ) ) )
216
+ ( Continue ( ( ) ) , Continue ( ( ) ) ) => { } ,
185
217
186
218
// `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
+ }
193
234
} ,
194
235
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) ) ;
198
240
} ,
199
241
}
200
242
@@ -208,8 +250,40 @@ impl<'tcx> Visitor<'tcx> for WaitFinder<'_, 'tcx> {
208
250
}
209
251
210
252
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
+ }
213
287
}
214
288
}
215
289
@@ -220,7 +294,7 @@ impl<'tcx> Visitor<'tcx> for WaitFinder<'_, 'tcx> {
220
294
/// `let _ = <expr that spawns child>;`.
221
295
///
222
296
/// 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 ) {
224
298
let Some ( block) = get_enclosing_block ( cx, spawn_expr. hir_id ) else {
225
299
return ;
226
300
} ;
@@ -234,27 +308,46 @@ fn check<'tcx>(cx: &LateContext<'tcx>, spawn_expr: &'tcx Expr<'tcx>, emit_sugges
234
308
return ;
235
309
}
236
310
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" ,
249
317
) ;
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
+ }
253
347
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
+ } ) ;
258
351
}
259
352
260
353
/// Checks if the given expression exits the process.
0 commit comments