Skip to content

Commit df46e4c

Browse files
authored
add targeted help messages to zombie_processes diagnostic (#13760)
Fixes #13748 Diagnostic for that issue with this change: ``` warning: spawned process is not `wait()`ed on in all code paths --> x.rs:167:19 | 167 | let mut cmd = cmd.unwrap(); | ^^^^^^^^^^^^ | note: no `wait()` call exists on the code path to this early return --> x.rs:178:47 | 178 | std::io::ErrorKind::BrokenPipe => return Some(0), | ^^^^^^^^^^^^^^ note: `wait()` call exists, but it is unreachable due to the early return --> x.rs:185:10 | 185 | Some(cmd.wait().unwrap().code().unwrap()) // <-- wait()! | ^^^ = help: consider calling `.wait()` in all code paths = note: not doing so might leave behind zombie processes = note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning ``` Instead of saying "wait() is **never** called", it now says it's not called by all code paths and points out the early return in particular. changelog: none
2 parents 74dd50c + 1329547 commit df46e4c

File tree

3 files changed

+293
-108
lines changed

3 files changed

+293
-108
lines changed

clippy_lints/src/zombie_processes.rs

+184-91
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ use ControlFlow::{Break, Continue};
22
use clippy_utils::diagnostics::span_lint_and_then;
33
use clippy_utils::{fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, path_to_local_id, paths};
44
use rustc_ast::Mutability;
5+
use rustc_ast::visit::visit_opt;
56
use rustc_errors::Applicability;
67
use rustc_hir::intravisit::{Visitor, walk_block, walk_expr, walk_local};
78
use rustc_hir::{Expr, ExprKind, HirId, LetStmt, Node, PatKind, Stmt, StmtKind};
89
use rustc_lint::{LateContext, LateLintPass};
910
use rustc_middle::hir::nested_filter;
1011
use rustc_session::declare_lint_pass;
11-
use rustc_span::sym;
12+
use rustc_span::{Span, sym};
1213
use std::ops::ControlFlow;
1314

1415
declare_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

122151
impl<'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

Comments
 (0)