Skip to content

Commit eebbd7a

Browse files
committed
don't lint if process unconditionally exits
1 parent 5e487a4 commit eebbd7a

File tree

4 files changed

+212
-10
lines changed

4 files changed

+212
-10
lines changed

clippy_lints/src/zombie_processes.rs

+148-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
22
use clippy_utils::visitors::for_each_local_use_after_expr;
3-
use clippy_utils::{fn_def_id, match_any_def_paths, match_def_path, paths};
3+
use clippy_utils::{fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, paths};
44
use rustc_ast::Mutability;
5-
use rustc_hir::{Expr, ExprKind, Node, PatKind, Stmt, StmtKind};
5+
use rustc_hir::def::DefKind;
6+
use rustc_hir::intravisit::{walk_expr, Visitor};
7+
use rustc_hir::{Expr, ExprKind, HirId, Local, Node, PatKind, Stmt, StmtKind};
68
use rustc_lint::{LateContext, LateLintPass};
79
use rustc_session::{declare_lint_pass, declare_tool_lint};
8-
use rustc_span::Span;
10+
use rustc_span::{sym, Span};
911
use std::ops::ControlFlow;
1012

1113
declare_clippy_lint! {
@@ -95,20 +97,157 @@ impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
9597
}
9698
}).is_continue();
9799

98-
if has_no_wait {
99-
emit_lint(cx, expr.span);
100+
// If it does have a `wait()` call, we're done. Don't lint.
101+
if !has_no_wait {
102+
return;
100103
}
104+
105+
check(cx, expr, local.hir_id);
101106
},
102-
Node::Local(local) if let PatKind::Wild = local.pat.kind => {
107+
Node::Local(&Local { pat, hir_id, .. }) if let PatKind::Wild = pat.kind => {
103108
// `let _ = child;`, also dropped immediately without `wait()`ing
104-
emit_lint(cx, expr.span);
109+
check(cx, expr, hir_id);
105110
}
106-
Node::Stmt(Stmt { kind: StmtKind::Semi(_), .. }) => {
111+
Node::Stmt(&Stmt { kind: StmtKind::Semi(_), hir_id, .. }) => {
107112
// Immediately dropped. E.g. `std::process::Command::new("echo").spawn().unwrap();`
108-
emit_lint(cx, expr.span);
113+
check(cx, expr, hir_id);
109114
}
110115
_ => {}
111116
}
112117
}
113118
}
114119
}
120+
121+
/// This function has shared logic between the different kinds of nodes that can trigger the lint.
122+
///
123+
/// In particular, `let <binding> = <expr that spawns child>;` requires some custom additional logic
124+
/// such as checking that the binding is not used in certain ways, which isn't necessary for
125+
/// `let _ = <expr that spawns child>;`.
126+
///
127+
/// This checks if the program doesn't unconditionally exit after the spawn expression and that it
128+
/// isn't the last statement of the program.
129+
fn check<'tcx>(cx: &LateContext<'tcx>, spawn_expr: &'tcx Expr<'tcx>, node_id: HirId) {
130+
let Some(block) = get_enclosing_block(cx, spawn_expr.hir_id) else {
131+
return;
132+
};
133+
134+
let mut vis = ExitPointFinder {
135+
cx,
136+
state: ExitPointState::LookingForSpawnExpr(spawn_expr.hir_id),
137+
};
138+
vis.visit_block(block);
139+
140+
// Visitor found an unconditional `exit()` call, so don't lint.
141+
if let ExitPointState::ExitFound = vis.state {
142+
return;
143+
}
144+
145+
// This might be the last effective node of the program (main function).
146+
// There's no need to lint in that case either, as this is basically equivalent to calling `exit()`
147+
if is_last_node_in_main(cx, node_id) {
148+
return;
149+
}
150+
151+
emit_lint(cx, spawn_expr.span);
152+
}
153+
154+
/// The hir id id may either correspond to a `Local` or `Stmt`, depending on how we got here.
155+
/// This function gets the enclosing function, checks if it's `main` and if so,
156+
/// check if the last statement modulo blocks is the given id.
157+
fn is_last_node_in_main(cx: &LateContext<'_>, id: HirId) -> bool {
158+
let hir = cx.tcx.hir();
159+
let body_owner = hir.enclosing_body_owner(id);
160+
let enclosing_body = hir.body(hir.body_owned_by(body_owner));
161+
162+
if cx.tcx.opt_def_kind(body_owner) == Some(DefKind::Fn)
163+
&& cx.tcx.opt_item_name(body_owner.to_def_id()) == Some(sym::main)
164+
&& let ExprKind::Block(block, _) = &enclosing_body.value.peel_blocks().kind
165+
&& let [.., stmt] = block.stmts
166+
{
167+
matches!(stmt.kind, StmtKind::Local(local) if local.hir_id == id)
168+
|| matches!(stmt.kind, StmtKind::Semi(..) if stmt.hir_id == id)
169+
} else {
170+
false
171+
}
172+
}
173+
174+
/// Checks if the given expression exits the process.
175+
fn is_exit_expression(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
176+
if let Some(fn_did) = fn_def_id(cx, expr) {
177+
match_any_def_paths(cx, fn_did, &[&paths::EXIT, &paths::ABORT]).is_some()
178+
} else {
179+
false
180+
}
181+
}
182+
183+
#[derive(Debug)]
184+
enum ExitPointState {
185+
/// Still walking up to the expression that initiated the visitor.
186+
LookingForSpawnExpr(HirId),
187+
/// We're inside of a control flow construct (e.g. `if`, `match`, `loop`)
188+
/// Within this, we shouldn't accept any `exit()` calls in here, but we can leave all of these
189+
/// constructs later and still continue looking for an `exit()` call afterwards. Example:
190+
/// ```ignore
191+
/// Command::new("").spawn().unwrap();
192+
///
193+
/// if true { // depth=1
194+
/// if true { // depth=2
195+
/// match () { // depth=3
196+
/// () => loop { // depth=4
197+
///
198+
/// std::process::exit();
199+
/// ^^^^^^^^^^^^^^^^^^^^^ conditional exit call, ignored
200+
///
201+
/// } // depth=3
202+
/// } // depth=2
203+
/// } // depth=1
204+
/// } // depth=0
205+
///
206+
/// std::process::exit();
207+
/// ^^^^^^^^^^^^^^^^^^^^^ this exit call is accepted because we're now unconditionally calling it
208+
/// ```
209+
/// We can only get into this state from `NoExit`.
210+
InControlFlow { depth: u32 },
211+
/// No exit call found yet, but looking for one.
212+
NoExit,
213+
/// Found an expression that exits the process.
214+
ExitFound,
215+
}
216+
217+
fn expr_enters_control_flow(expr: &Expr<'_>) -> bool {
218+
matches!(expr.kind, ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Loop(..))
219+
}
220+
221+
struct ExitPointFinder<'a, 'tcx> {
222+
state: ExitPointState,
223+
cx: &'a LateContext<'tcx>,
224+
}
225+
226+
impl<'a, 'tcx> Visitor<'tcx> for ExitPointFinder<'a, 'tcx> {
227+
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
228+
match self.state {
229+
ExitPointState::LookingForSpawnExpr(id) if expr.hir_id == id => {
230+
self.state = ExitPointState::NoExit;
231+
walk_expr(self, expr);
232+
},
233+
ExitPointState::NoExit if expr_enters_control_flow(expr) => {
234+
self.state = ExitPointState::InControlFlow { depth: 1 };
235+
walk_expr(self, expr);
236+
if let ExitPointState::InControlFlow { .. } = self.state {
237+
self.state = ExitPointState::NoExit;
238+
}
239+
},
240+
ExitPointState::NoExit if is_exit_expression(self.cx, expr) => self.state = ExitPointState::ExitFound,
241+
ExitPointState::InControlFlow { ref mut depth } if expr_enters_control_flow(expr) => {
242+
*depth += 1;
243+
walk_expr(self, expr);
244+
match self.state {
245+
ExitPointState::InControlFlow { depth: 1 } => self.state = ExitPointState::NoExit,
246+
ExitPointState::InControlFlow { ref mut depth } => *depth -= 1,
247+
_ => {},
248+
}
249+
},
250+
_ => {},
251+
}
252+
}
253+
}

clippy_utils/src/paths.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! Whenever possible, please consider diagnostic items over hardcoded paths.
55
//! See <https://github.com/rust-lang/rust-clippy/issues/5393> for more information.
66
7+
pub const ABORT: [&str; 3] = ["std", "process", "abort"];
78
#[cfg(feature = "internal")]
89
pub const APPLICABILITY: [&str; 2] = ["rustc_lint_defs", "Applicability"];
910
#[cfg(feature = "internal")]

tests/ui/zombie_processes.rs

+44
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,50 @@ fn main() {
4444
let v = &x;
4545
// (allow shared refs is fine because one cannot call `.wait()` through that)
4646
}
47+
48+
// https://github.com/rust-lang/rust-clippy/pull/11476#issuecomment-1718456033
49+
// Unconditionally exiting the process in various ways (should not lint)
50+
{
51+
let mut x = Command::new("").spawn().unwrap();
52+
std::process::exit(0);
53+
}
54+
{
55+
let mut x = Command::new("").spawn().unwrap();
56+
std::process::abort(); // same as above, but abort instead of exit
57+
}
58+
{
59+
let mut x = Command::new("").spawn().unwrap();
60+
if true { /* nothing */ }
61+
std::process::abort(); // still unconditionally exits
62+
}
63+
64+
// Conditionally exiting
65+
// It should assume that it might not exit and still lint
66+
{
67+
let mut x = Command::new("").spawn().unwrap();
68+
//~^ ERROR: spawned process is never `wait()`-ed on
69+
if true {
70+
std::process::exit(0);
71+
}
72+
}
73+
{
74+
let mut x = Command::new("").spawn().unwrap();
75+
//~^ ERROR: spawned process is never `wait()`-ed on
76+
if true {
77+
while false {}
78+
// Calling `exit()` after leaving a while loop should still be linted.
79+
std::process::exit(0);
80+
}
81+
}
82+
83+
// Checking that it won't lint if spawn is the last statement of a main function.
84+
// IMPORTANT: this case must always be at the very end so it always tests the right thing.
85+
// Don't move this.
86+
{
87+
{
88+
Command::new("").spawn().unwrap();
89+
}
90+
}
4791
}
4892

4993
fn spawn_proc() -> Child {

tests/ui/zombie_processes.stderr

+19-1
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,23 @@ LL | let mut x = Command::new("").spawn().unwrap();
4545
= help: consider calling `.wait()`
4646
= note: also see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
4747

48-
error: aborting due to 5 previous errors
48+
error: spawned process is never `wait()`-ed on and leaves behind a zombie process
49+
--> $DIR/zombie_processes.rs:67:21
50+
|
51+
LL | let mut x = Command::new("").spawn().unwrap();
52+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
53+
|
54+
= help: consider calling `.wait()`
55+
= note: also see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
56+
57+
error: spawned process is never `wait()`-ed on and leaves behind a zombie process
58+
--> $DIR/zombie_processes.rs:74:21
59+
|
60+
LL | let mut x = Command::new("").spawn().unwrap();
61+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
62+
|
63+
= help: consider calling `.wait()`
64+
= note: also see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
65+
66+
error: aborting due to 7 previous errors
4967

0 commit comments

Comments
 (0)