mirror of
https://github.com/rust-lang/rust-clippy
synced 2024-11-10 07:04:18 +00:00
new lint: zombie_processes
This commit is contained in:
parent
603d5a19c9
commit
e8ac4ea418
14 changed files with 642 additions and 3 deletions
|
@ -6048,6 +6048,7 @@ Released 2018-09-13
|
||||||
[`zero_repeat_side_effects`]: https://rust-lang.github.io/rust-clippy/master/index.html#zero_repeat_side_effects
|
[`zero_repeat_side_effects`]: https://rust-lang.github.io/rust-clippy/master/index.html#zero_repeat_side_effects
|
||||||
[`zero_sized_map_values`]: https://rust-lang.github.io/rust-clippy/master/index.html#zero_sized_map_values
|
[`zero_sized_map_values`]: https://rust-lang.github.io/rust-clippy/master/index.html#zero_sized_map_values
|
||||||
[`zero_width_space`]: https://rust-lang.github.io/rust-clippy/master/index.html#zero_width_space
|
[`zero_width_space`]: https://rust-lang.github.io/rust-clippy/master/index.html#zero_width_space
|
||||||
|
[`zombie_processes`]: https://rust-lang.github.io/rust-clippy/master/index.html#zombie_processes
|
||||||
[`zst_offset`]: https://rust-lang.github.io/rust-clippy/master/index.html#zst_offset
|
[`zst_offset`]: https://rust-lang.github.io/rust-clippy/master/index.html#zst_offset
|
||||||
<!-- end autogenerated links to lint list -->
|
<!-- end autogenerated links to lint list -->
|
||||||
<!-- begin autogenerated links to configuration documentation -->
|
<!-- begin autogenerated links to configuration documentation -->
|
||||||
|
|
|
@ -29,7 +29,7 @@ pub fn run(port: u16, lint: Option<String>) -> ! {
|
||||||
}
|
}
|
||||||
if let Some(url) = url.take() {
|
if let Some(url) = url.take() {
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
Command::new(PYTHON)
|
let mut child = Command::new(PYTHON)
|
||||||
.arg("-m")
|
.arg("-m")
|
||||||
.arg("http.server")
|
.arg("http.server")
|
||||||
.arg(port.to_string())
|
.arg(port.to_string())
|
||||||
|
@ -40,6 +40,7 @@ pub fn run(port: u16, lint: Option<String>) -> ! {
|
||||||
thread::sleep(Duration::from_millis(500));
|
thread::sleep(Duration::from_millis(500));
|
||||||
// Launch browser after first export.py has completed and http.server is up
|
// Launch browser after first export.py has completed and http.server is up
|
||||||
let _result = opener::open(url);
|
let _result = opener::open(url);
|
||||||
|
child.wait().unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(1000));
|
thread::sleep(Duration::from_millis(1000));
|
||||||
|
|
|
@ -768,4 +768,5 @@ pub static LINTS: &[&crate::LintInfo] = &[
|
||||||
crate::zero_div_zero::ZERO_DIVIDED_BY_ZERO_INFO,
|
crate::zero_div_zero::ZERO_DIVIDED_BY_ZERO_INFO,
|
||||||
crate::zero_repeat_side_effects::ZERO_REPEAT_SIDE_EFFECTS_INFO,
|
crate::zero_repeat_side_effects::ZERO_REPEAT_SIDE_EFFECTS_INFO,
|
||||||
crate::zero_sized_map_values::ZERO_SIZED_MAP_VALUES_INFO,
|
crate::zero_sized_map_values::ZERO_SIZED_MAP_VALUES_INFO,
|
||||||
|
crate::zombie_processes::ZOMBIE_PROCESSES_INFO,
|
||||||
];
|
];
|
||||||
|
|
|
@ -386,6 +386,7 @@ mod write;
|
||||||
mod zero_div_zero;
|
mod zero_div_zero;
|
||||||
mod zero_repeat_side_effects;
|
mod zero_repeat_side_effects;
|
||||||
mod zero_sized_map_values;
|
mod zero_sized_map_values;
|
||||||
|
mod zombie_processes;
|
||||||
// end lints modules, do not remove this comment, it’s used in `update_lints`
|
// end lints modules, do not remove this comment, it’s used in `update_lints`
|
||||||
|
|
||||||
use clippy_config::{get_configuration_metadata, Conf};
|
use clippy_config::{get_configuration_metadata, Conf};
|
||||||
|
@ -933,5 +934,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
|
||||||
store.register_late_pass(|_| Box::new(set_contains_or_insert::SetContainsOrInsert));
|
store.register_late_pass(|_| Box::new(set_contains_or_insert::SetContainsOrInsert));
|
||||||
store.register_early_pass(|| Box::new(byte_char_slices::ByteCharSlice));
|
store.register_early_pass(|| Box::new(byte_char_slices::ByteCharSlice));
|
||||||
store.register_early_pass(|| Box::new(cfg_not_test::CfgNotTest));
|
store.register_early_pass(|| Box::new(cfg_not_test::CfgNotTest));
|
||||||
|
store.register_late_pass(|_| Box::new(zombie_processes::ZombieProcesses));
|
||||||
// add lints here, do not remove this comment, it's used in `new_lint`
|
// add lints here, do not remove this comment, it's used in `new_lint`
|
||||||
}
|
}
|
||||||
|
|
334
clippy_lints/src/zombie_processes.rs
Normal file
334
clippy_lints/src/zombie_processes.rs
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
use clippy_utils::diagnostics::span_lint_and_then;
|
||||||
|
use clippy_utils::{fn_def_id, get_enclosing_block, match_any_def_paths, match_def_path, path_to_local_id, paths};
|
||||||
|
use rustc_ast::Mutability;
|
||||||
|
use rustc_errors::Applicability;
|
||||||
|
use rustc_hir::intravisit::{walk_block, walk_expr, walk_local, Visitor};
|
||||||
|
use rustc_hir::{Expr, ExprKind, HirId, LetStmt, Node, PatKind, Stmt, StmtKind};
|
||||||
|
use rustc_lint::{LateContext, LateLintPass};
|
||||||
|
use rustc_session::declare_lint_pass;
|
||||||
|
use rustc_span::sym;
|
||||||
|
use std::ops::ControlFlow;
|
||||||
|
use ControlFlow::{Break, Continue};
|
||||||
|
|
||||||
|
declare_clippy_lint! {
|
||||||
|
/// ### What it does
|
||||||
|
/// Looks for code that spawns a process but never calls `wait()` on the child.
|
||||||
|
///
|
||||||
|
/// ### Why is this bad?
|
||||||
|
/// As explained in the [standard library documentation](https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning),
|
||||||
|
/// calling `wait()` is necessary on Unix platforms to properly release all OS resources associated with the process.
|
||||||
|
/// Not doing so will effectively leak process IDs and/or other limited global resources,
|
||||||
|
/// which can eventually lead to resource exhaustion, so it's recommended to call `wait()` in long-running applications.
|
||||||
|
/// Such processes are called "zombie processes".
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
/// ```rust
|
||||||
|
/// use std::process::Command;
|
||||||
|
///
|
||||||
|
/// let _child = Command::new("ls").spawn().expect("failed to execute child");
|
||||||
|
/// ```
|
||||||
|
/// Use instead:
|
||||||
|
/// ```rust
|
||||||
|
/// use std::process::Command;
|
||||||
|
///
|
||||||
|
/// let mut child = Command::new("ls").spawn().expect("failed to execute child");
|
||||||
|
/// child.wait().expect("failed to wait on child");
|
||||||
|
/// ```
|
||||||
|
#[clippy::version = "1.74.0"]
|
||||||
|
pub ZOMBIE_PROCESSES,
|
||||||
|
suspicious,
|
||||||
|
"not waiting on a spawned child process"
|
||||||
|
}
|
||||||
|
declare_lint_pass!(ZombieProcesses => [ZOMBIE_PROCESSES]);
|
||||||
|
|
||||||
|
impl<'tcx> LateLintPass<'tcx> for ZombieProcesses {
|
||||||
|
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
|
||||||
|
if let ExprKind::Call(..) | ExprKind::MethodCall(..) = expr.kind
|
||||||
|
&& let Some(child_adt) = cx.typeck_results().expr_ty(expr).ty_adt_def()
|
||||||
|
&& match_def_path(cx, child_adt.did(), &paths::CHILD)
|
||||||
|
{
|
||||||
|
match cx.tcx.parent_hir_node(expr.hir_id) {
|
||||||
|
Node::LetStmt(local)
|
||||||
|
if let PatKind::Binding(_, local_id, ..) = local.pat.kind
|
||||||
|
&& let Some(enclosing_block) = get_enclosing_block(cx, expr.hir_id) =>
|
||||||
|
{
|
||||||
|
let mut vis = WaitFinder::WalkUpTo(cx, local_id);
|
||||||
|
|
||||||
|
// If it does have a `wait()` call, we're done. Don't lint.
|
||||||
|
if let Break(BreakReason::WaitFound) = walk_block(&mut vis, enclosing_block) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't emit a suggestion since the binding is used later
|
||||||
|
check(cx, expr, false);
|
||||||
|
},
|
||||||
|
Node::LetStmt(&LetStmt { pat, .. }) if let PatKind::Wild = pat.kind => {
|
||||||
|
// `let _ = child;`, also dropped immediately without `wait()`ing
|
||||||
|
check(cx, expr, true);
|
||||||
|
},
|
||||||
|
Node::Stmt(&Stmt {
|
||||||
|
kind: StmtKind::Semi(_),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
// Immediately dropped. E.g. `std::process::Command::new("echo").spawn().unwrap();`
|
||||||
|
check(cx, expr, true);
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BreakReason {
|
||||||
|
WaitFound,
|
||||||
|
EarlyReturn,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A visitor responsible for finding a `wait()` call on a local variable.
|
||||||
|
///
|
||||||
|
/// Conditional `wait()` calls are assumed to not call wait:
|
||||||
|
/// ```ignore
|
||||||
|
/// let mut c = Command::new("").spawn().unwrap();
|
||||||
|
/// if true {
|
||||||
|
/// c.wait();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note that this visitor does NOT explicitly look for `wait()` calls directly, but rather does the
|
||||||
|
/// inverse -- checking if all uses of the local are either:
|
||||||
|
/// - a field access (`child.{stderr,stdin,stdout}`)
|
||||||
|
/// - calling `id` or `kill`
|
||||||
|
/// - no use at all (e.g. `let _x = child;`)
|
||||||
|
/// - taking a shared reference (`&`), `wait()` can't go through that
|
||||||
|
///
|
||||||
|
/// None of these are sufficient to prevent zombie processes.
|
||||||
|
/// Doing it like this means more FNs, but FNs are better than FPs.
|
||||||
|
///
|
||||||
|
/// `return` expressions, conditional or not, short-circuit the visitor because
|
||||||
|
/// if a `wait()` call hadn't been found at that point, it might never reach one at a later point:
|
||||||
|
/// ```ignore
|
||||||
|
/// let mut c = Command::new("").spawn().unwrap();
|
||||||
|
/// if true {
|
||||||
|
/// return; // Break(BreakReason::EarlyReturn)
|
||||||
|
/// }
|
||||||
|
/// c.wait(); // this might not be reachable
|
||||||
|
/// ```
|
||||||
|
enum WaitFinder<'a, 'tcx> {
|
||||||
|
WalkUpTo(&'a LateContext<'tcx>, HirId),
|
||||||
|
Found(&'a LateContext<'tcx>, HirId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'tcx> Visitor<'tcx> for WaitFinder<'a, 'tcx> {
|
||||||
|
type Result = ControlFlow<BreakReason>;
|
||||||
|
|
||||||
|
fn visit_local(&mut self, l: &'tcx LetStmt<'tcx>) -> Self::Result {
|
||||||
|
if let Self::WalkUpTo(cx, local_id) = *self
|
||||||
|
&& let PatKind::Binding(_, pat_id, ..) = l.pat.kind
|
||||||
|
&& local_id == pat_id
|
||||||
|
{
|
||||||
|
*self = Self::Found(cx, local_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
walk_local(self, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) -> Self::Result {
|
||||||
|
let Self::Found(cx, local_id) = *self else {
|
||||||
|
return walk_expr(self, ex);
|
||||||
|
};
|
||||||
|
|
||||||
|
if path_to_local_id(ex, local_id) {
|
||||||
|
match cx.tcx.parent_hir_node(ex.hir_id) {
|
||||||
|
Node::Stmt(Stmt {
|
||||||
|
kind: StmtKind::Semi(_),
|
||||||
|
..
|
||||||
|
}) => {},
|
||||||
|
Node::Expr(expr) if let ExprKind::Field(..) = expr.kind => {},
|
||||||
|
Node::Expr(expr) if let ExprKind::AddrOf(_, Mutability::Not, _) = expr.kind => {},
|
||||||
|
Node::Expr(expr)
|
||||||
|
if let Some(fn_did) = fn_def_id(cx, expr)
|
||||||
|
&& match_any_def_paths(cx, fn_did, &[&paths::CHILD_ID, &paths::CHILD_KILL]).is_some() => {},
|
||||||
|
|
||||||
|
// Conservatively assume that all other kinds of nodes call `.wait()` somehow.
|
||||||
|
_ => return Break(BreakReason::WaitFound),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match ex.kind {
|
||||||
|
ExprKind::Ret(..) => return Break(BreakReason::EarlyReturn),
|
||||||
|
ExprKind::If(cond, then, None) => {
|
||||||
|
walk_expr(self, cond)?;
|
||||||
|
|
||||||
|
// A `wait()` call in an if expression with no else is not enough:
|
||||||
|
//
|
||||||
|
// let c = spawn();
|
||||||
|
// if true {
|
||||||
|
// c.wait();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// This might not call wait(). However, early returns are propagated,
|
||||||
|
// because they might lead to a later wait() not being called.
|
||||||
|
if let Break(BreakReason::EarlyReturn) = walk_expr(self, then) {
|
||||||
|
return Break(BreakReason::EarlyReturn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Continue(());
|
||||||
|
},
|
||||||
|
|
||||||
|
ExprKind::If(cond, then, Some(else_)) => {
|
||||||
|
walk_expr(self, cond)?;
|
||||||
|
|
||||||
|
#[expect(clippy::unnested_or_patterns)]
|
||||||
|
match (walk_expr(self, then), walk_expr(self, else_)) {
|
||||||
|
(Continue(()), Continue(()))
|
||||||
|
|
||||||
|
// `wait()` in one branch but nothing in the other does not count
|
||||||
|
| (Continue(()), Break(BreakReason::WaitFound))
|
||||||
|
| (Break(BreakReason::WaitFound), Continue(())) => {},
|
||||||
|
|
||||||
|
// `wait()` in both branches is ok
|
||||||
|
(Break(BreakReason::WaitFound), Break(BreakReason::WaitFound)) => {
|
||||||
|
return Break(BreakReason::WaitFound);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Propagate early returns in either branch
|
||||||
|
(Break(BreakReason::EarlyReturn), _) | (_, Break(BreakReason::EarlyReturn)) => {
|
||||||
|
return Break(BreakReason::EarlyReturn);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return Continue(());
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
walk_expr(self, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function has shared logic between the different kinds of nodes that can trigger the lint.
|
||||||
|
///
|
||||||
|
/// In particular, `let <binding> = <expr that spawns child>;` requires some custom additional logic
|
||||||
|
/// such as checking that the binding is not used in certain ways, which isn't necessary for
|
||||||
|
/// `let _ = <expr that spawns child>;`.
|
||||||
|
///
|
||||||
|
/// This checks if the program doesn't unconditionally exit after the spawn expression.
|
||||||
|
fn check<'tcx>(cx: &LateContext<'tcx>, spawn_expr: &'tcx Expr<'tcx>, emit_suggestion: bool) {
|
||||||
|
let Some(block) = get_enclosing_block(cx, spawn_expr.hir_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut vis = ExitPointFinder {
|
||||||
|
cx,
|
||||||
|
state: ExitPointState::WalkUpTo(spawn_expr.hir_id),
|
||||||
|
};
|
||||||
|
if let Break(ExitCallFound) = vis.visit_block(block) {
|
||||||
|
// Visitor found an unconditional `exit()` call, so don't lint.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
span_lint_and_then(
|
||||||
|
cx,
|
||||||
|
ZOMBIE_PROCESSES,
|
||||||
|
spawn_expr.span,
|
||||||
|
"spawned process is never `wait()`ed on",
|
||||||
|
|diag| {
|
||||||
|
if emit_suggestion {
|
||||||
|
diag.span_suggestion(
|
||||||
|
spawn_expr.span.shrink_to_hi(),
|
||||||
|
"try",
|
||||||
|
".wait()",
|
||||||
|
Applicability::MaybeIncorrect,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
diag.note("consider calling `.wait()`");
|
||||||
|
}
|
||||||
|
|
||||||
|
diag.note("not doing so might leave behind zombie processes")
|
||||||
|
.note("see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the given expression exits the process.
|
||||||
|
fn is_exit_expression(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
|
||||||
|
fn_def_id(cx, expr).is_some_and(|fn_did| {
|
||||||
|
cx.tcx.is_diagnostic_item(sym::process_exit, fn_did) || match_def_path(cx, fn_did, &paths::ABORT)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ExitPointState {
|
||||||
|
/// Still walking up to the expression that initiated the visitor.
|
||||||
|
WalkUpTo(HirId),
|
||||||
|
/// We're inside of a control flow construct (e.g. `if`, `match`, `loop`)
|
||||||
|
/// Within this, we shouldn't accept any `exit()` calls in here, but we can leave all of these
|
||||||
|
/// constructs later and still continue looking for an `exit()` call afterwards. Example:
|
||||||
|
/// ```ignore
|
||||||
|
/// Command::new("").spawn().unwrap();
|
||||||
|
///
|
||||||
|
/// if true { // depth=1
|
||||||
|
/// if true { // depth=2
|
||||||
|
/// match () { // depth=3
|
||||||
|
/// () => loop { // depth=4
|
||||||
|
///
|
||||||
|
/// std::process::exit();
|
||||||
|
/// ^^^^^^^^^^^^^^^^^^^^^ conditional exit call, ignored
|
||||||
|
///
|
||||||
|
/// } // depth=3
|
||||||
|
/// } // depth=2
|
||||||
|
/// } // depth=1
|
||||||
|
/// } // depth=0
|
||||||
|
///
|
||||||
|
/// std::process::exit();
|
||||||
|
/// ^^^^^^^^^^^^^^^^^^^^^ this exit call is accepted because we're now unconditionally calling it
|
||||||
|
/// ```
|
||||||
|
/// We can only get into this state from `NoExit`.
|
||||||
|
InControlFlow { depth: u32 },
|
||||||
|
/// No exit call found yet, but looking for one.
|
||||||
|
NoExit,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expr_enters_control_flow(expr: &Expr<'_>) -> bool {
|
||||||
|
matches!(expr.kind, ExprKind::If(..) | ExprKind::Match(..) | ExprKind::Loop(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExitPointFinder<'a, 'tcx> {
|
||||||
|
state: ExitPointState,
|
||||||
|
cx: &'a LateContext<'tcx>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ExitCallFound;
|
||||||
|
|
||||||
|
impl<'a, 'tcx> Visitor<'tcx> for ExitPointFinder<'a, 'tcx> {
|
||||||
|
type Result = ControlFlow<ExitCallFound>;
|
||||||
|
|
||||||
|
fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) -> Self::Result {
|
||||||
|
match self.state {
|
||||||
|
ExitPointState::WalkUpTo(id) if expr.hir_id == id => {
|
||||||
|
self.state = ExitPointState::NoExit;
|
||||||
|
walk_expr(self, expr)
|
||||||
|
},
|
||||||
|
ExitPointState::NoExit if expr_enters_control_flow(expr) => {
|
||||||
|
self.state = ExitPointState::InControlFlow { depth: 1 };
|
||||||
|
walk_expr(self, expr)?;
|
||||||
|
if let ExitPointState::InControlFlow { .. } = self.state {
|
||||||
|
self.state = ExitPointState::NoExit;
|
||||||
|
}
|
||||||
|
Continue(())
|
||||||
|
},
|
||||||
|
ExitPointState::NoExit if is_exit_expression(self.cx, expr) => Break(ExitCallFound),
|
||||||
|
ExitPointState::InControlFlow { ref mut depth } if expr_enters_control_flow(expr) => {
|
||||||
|
*depth += 1;
|
||||||
|
walk_expr(self, expr)?;
|
||||||
|
match self.state {
|
||||||
|
ExitPointState::InControlFlow { depth: 1 } => self.state = ExitPointState::NoExit,
|
||||||
|
ExitPointState::InControlFlow { ref mut depth } => *depth -= 1,
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
Continue(())
|
||||||
|
},
|
||||||
|
_ => Continue(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@
|
||||||
//! Whenever possible, please consider diagnostic items over hardcoded paths.
|
//! Whenever possible, please consider diagnostic items over hardcoded paths.
|
||||||
//! See <https://github.com/rust-lang/rust-clippy/issues/5393> for more information.
|
//! See <https://github.com/rust-lang/rust-clippy/issues/5393> for more information.
|
||||||
|
|
||||||
|
pub const ABORT: [&str; 3] = ["std", "process", "abort"];
|
||||||
pub const APPLICABILITY: [&str; 2] = ["rustc_lint_defs", "Applicability"];
|
pub const APPLICABILITY: [&str; 2] = ["rustc_lint_defs", "Applicability"];
|
||||||
pub const APPLICABILITY_VALUES: [[&str; 3]; 4] = [
|
pub const APPLICABILITY_VALUES: [[&str; 3]; 4] = [
|
||||||
["rustc_lint_defs", "Applicability", "Unspecified"],
|
["rustc_lint_defs", "Applicability", "Unspecified"],
|
||||||
|
@ -23,6 +24,9 @@ pub const CORE_RESULT_OK_METHOD: [&str; 4] = ["core", "result", "Result", "ok"];
|
||||||
pub const CSTRING_AS_C_STR: [&str; 5] = ["alloc", "ffi", "c_str", "CString", "as_c_str"];
|
pub const CSTRING_AS_C_STR: [&str; 5] = ["alloc", "ffi", "c_str", "CString", "as_c_str"];
|
||||||
pub const EARLY_CONTEXT: [&str; 2] = ["rustc_lint", "EarlyContext"];
|
pub const EARLY_CONTEXT: [&str; 2] = ["rustc_lint", "EarlyContext"];
|
||||||
pub const EARLY_LINT_PASS: [&str; 3] = ["rustc_lint", "passes", "EarlyLintPass"];
|
pub const EARLY_LINT_PASS: [&str; 3] = ["rustc_lint", "passes", "EarlyLintPass"];
|
||||||
|
pub const CHILD: [&str; 3] = ["std", "process", "Child"];
|
||||||
|
pub const CHILD_ID: [&str; 4] = ["std", "process", "Child", "id"];
|
||||||
|
pub const CHILD_KILL: [&str; 4] = ["std", "process", "Child", "kill"];
|
||||||
pub const F32_EPSILON: [&str; 4] = ["core", "f32", "<impl f32>", "EPSILON"];
|
pub const F32_EPSILON: [&str; 4] = ["core", "f32", "<impl f32>", "EPSILON"];
|
||||||
pub const F64_EPSILON: [&str; 4] = ["core", "f64", "<impl f64>", "EPSILON"];
|
pub const F64_EPSILON: [&str; 4] = ["core", "f64", "<impl f64>", "EPSILON"];
|
||||||
pub const FILE_OPTIONS: [&str; 4] = ["std", "fs", "File", "options"];
|
pub const FILE_OPTIONS: [&str; 4] = ["std", "fs", "File", "options"];
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
#![allow(clippy::zombie_processes)]
|
||||||
fn main() {
|
fn main() {
|
||||||
// Things it should warn about:
|
// Things it should warn about:
|
||||||
std::process::Command::new("echo").args(["-n", "hello"]).spawn().unwrap();
|
std::process::Command::new("echo").args(["-n", "hello"]).spawn().unwrap();
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
#![allow(clippy::zombie_processes)]
|
||||||
fn main() {
|
fn main() {
|
||||||
// Things it should warn about:
|
// Things it should warn about:
|
||||||
std::process::Command::new("echo").arg("-n hello").spawn().unwrap();
|
std::process::Command::new("echo").arg("-n hello").spawn().unwrap();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
error: single argument that looks like it should be multiple arguments
|
error: single argument that looks like it should be multiple arguments
|
||||||
--> tests/ui/suspicious_command_arg_space.rs:3:44
|
--> tests/ui/suspicious_command_arg_space.rs:4:44
|
||||||
|
|
|
|
||||||
LL | std::process::Command::new("echo").arg("-n hello").spawn().unwrap();
|
LL | std::process::Command::new("echo").arg("-n hello").spawn().unwrap();
|
||||||
| ^^^^^^^^^^
|
| ^^^^^^^^^^
|
||||||
|
@ -12,7 +12,7 @@ LL | std::process::Command::new("echo").args(["-n", "hello"]).spawn().unwrap
|
||||||
| ~~~~ ~~~~~~~~~~~~~~~
|
| ~~~~ ~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
error: single argument that looks like it should be multiple arguments
|
error: single argument that looks like it should be multiple arguments
|
||||||
--> tests/ui/suspicious_command_arg_space.rs:6:43
|
--> tests/ui/suspicious_command_arg_space.rs:7:43
|
||||||
|
|
|
|
||||||
LL | std::process::Command::new("cat").arg("--number file").spawn().unwrap();
|
LL | std::process::Command::new("cat").arg("--number file").spawn().unwrap();
|
||||||
| ^^^^^^^^^^^^^^^
|
| ^^^^^^^^^^^^^^^
|
||||||
|
|
138
tests/ui/zombie_processes.rs
Normal file
138
tests/ui/zombie_processes.rs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
#![warn(clippy::zombie_processes)]
|
||||||
|
#![allow(clippy::if_same_then_else, clippy::ifs_same_cond)]
|
||||||
|
|
||||||
|
use std::process::{Child, Command};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
{
|
||||||
|
// Check that #[expect] works
|
||||||
|
#[expect(clippy::zombie_processes)]
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
x.kill();
|
||||||
|
x.id();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
x.wait().unwrap(); // OK
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let x = Command::new("").spawn().unwrap();
|
||||||
|
x.wait_with_output().unwrap(); // OK
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
x.try_wait().unwrap(); // OK
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
let mut r = &mut x;
|
||||||
|
r.wait().unwrap(); // OK, not calling `.wait()` directly on `x` but through `r` -> `x`
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
process_child(x); // OK, other function might call `.wait()` so assume it does
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
let v = &x;
|
||||||
|
// (allow shared refs is fine because one cannot call `.wait()` through that)
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/rust-lang/rust-clippy/pull/11476#issuecomment-1718456033
|
||||||
|
// Unconditionally exiting the process in various ways (should not lint)
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
std::process::abort(); // same as above, but abort instead of exit
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
if true { /* nothing */ }
|
||||||
|
std::process::abort(); // still unconditionally exits
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conditionally exiting
|
||||||
|
// It should assume that it might not exit and still lint
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
if true {
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
if true {
|
||||||
|
while false {}
|
||||||
|
// Calling `exit()` after leaving a while loop should still be linted.
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut x = { Command::new("").spawn().unwrap() };
|
||||||
|
x.wait().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
struct S {
|
||||||
|
c: Child,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut s = S {
|
||||||
|
c: Command::new("").spawn().unwrap(),
|
||||||
|
};
|
||||||
|
s.c.wait().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
if true {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
x.wait().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
if true {
|
||||||
|
x.wait().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
if true {
|
||||||
|
x.wait().unwrap();
|
||||||
|
} else if true {
|
||||||
|
x.wait().unwrap();
|
||||||
|
} else {
|
||||||
|
x.wait().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut x = Command::new("").spawn().unwrap();
|
||||||
|
if true {
|
||||||
|
x.wait().unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
x.wait().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_child(c: Child) {
|
||||||
|
todo!()
|
||||||
|
}
|
64
tests/ui/zombie_processes.stderr
Normal file
64
tests/ui/zombie_processes.stderr
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes.rs:14:21
|
||||||
|
|
|
||||||
|
LL | let mut x = Command::new("").spawn().unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: consider calling `.wait()`
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
= note: `-D clippy::zombie-processes` implied by `-D warnings`
|
||||||
|
= help: to override `-D warnings` add `#[allow(clippy::zombie_processes)]`
|
||||||
|
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes.rs:41:21
|
||||||
|
|
|
||||||
|
LL | let mut x = Command::new("").spawn().unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: consider calling `.wait()`
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes.rs:66:21
|
||||||
|
|
|
||||||
|
LL | let mut x = Command::new("").spawn().unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: consider calling `.wait()`
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes.rs:73:21
|
||||||
|
|
|
||||||
|
LL | let mut x = Command::new("").spawn().unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: consider calling `.wait()`
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes.rs:99:21
|
||||||
|
|
|
||||||
|
LL | let mut x = Command::new("").spawn().unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: consider calling `.wait()`
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes.rs:108:21
|
||||||
|
|
|
||||||
|
LL | let mut x = Command::new("").spawn().unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: consider calling `.wait()`
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
|
||||||
|
error: aborting due to 6 previous errors
|
||||||
|
|
26
tests/ui/zombie_processes_fixable.fixed
Normal file
26
tests/ui/zombie_processes_fixable.fixed
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
#![warn(clippy::zombie_processes)]
|
||||||
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
|
use std::process::{Child, Command};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let _ = Command::new("").spawn().unwrap().wait();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
Command::new("").spawn().unwrap().wait();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
spawn_proc().wait();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
spawn_proc().wait().unwrap(); // OK
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_main() {
|
||||||
|
Command::new("").spawn().unwrap().wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_proc() -> Child {
|
||||||
|
Command::new("").spawn().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_proc_2() -> Child {
|
||||||
|
return Command::new("").spawn().unwrap();
|
||||||
|
}
|
26
tests/ui/zombie_processes_fixable.rs
Normal file
26
tests/ui/zombie_processes_fixable.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
#![warn(clippy::zombie_processes)]
|
||||||
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
|
use std::process::{Child, Command};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let _ = Command::new("").spawn().unwrap();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
Command::new("").spawn().unwrap();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
spawn_proc();
|
||||||
|
//~^ ERROR: spawned process is never `wait()`ed on
|
||||||
|
spawn_proc().wait().unwrap(); // OK
|
||||||
|
}
|
||||||
|
|
||||||
|
fn not_main() {
|
||||||
|
Command::new("").spawn().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_proc() -> Child {
|
||||||
|
Command::new("").spawn().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_proc_2() -> Child {
|
||||||
|
return Command::new("").spawn().unwrap();
|
||||||
|
}
|
40
tests/ui/zombie_processes_fixable.stderr
Normal file
40
tests/ui/zombie_processes_fixable.stderr
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes_fixable.rs:7:13
|
||||||
|
|
|
||||||
|
LL | let _ = Command::new("").spawn().unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- help: try: `.wait()`
|
||||||
|
|
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
= note: `-D clippy::zombie-processes` implied by `-D warnings`
|
||||||
|
= help: to override `-D warnings` add `#[allow(clippy::zombie_processes)]`
|
||||||
|
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes_fixable.rs:9:5
|
||||||
|
|
|
||||||
|
LL | Command::new("").spawn().unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- help: try: `.wait()`
|
||||||
|
|
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes_fixable.rs:11:5
|
||||||
|
|
|
||||||
|
LL | spawn_proc();
|
||||||
|
| ^^^^^^^^^^^^- help: try: `.wait()`
|
||||||
|
|
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
|
||||||
|
error: spawned process is never `wait()`ed on
|
||||||
|
--> tests/ui/zombie_processes_fixable.rs:17:5
|
||||||
|
|
|
||||||
|
LL | Command::new("").spawn().unwrap();
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- help: try: `.wait()`
|
||||||
|
|
|
||||||
|
= note: not doing so might leave behind zombie processes
|
||||||
|
= note: see https://doc.rust-lang.org/stable/std/process/struct.Child.html#warning
|
||||||
|
|
||||||
|
error: aborting due to 4 previous errors
|
||||||
|
|
Loading…
Reference in a new issue