use ControlFlow::{Break, Continue};
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::res::{MaybeDef, MaybeResPath};
use clippy_utils::{fn_def_id, get_enclosing_block};
use rustc_ast::Mutability;
use rustc_ast::visit::visit_opt;
use rustc_errors::Applicability;
use rustc_hir::def_id::LocalDefId;
use rustc_hir::intravisit::{Visitor, walk_block, walk_expr};
use rustc_hir::{Expr, ExprKind, HirId, LetStmt, Node, PatKind, Stmt, StmtKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::hir::nested_filter;
use rustc_session::declare_lint_pass;
use rustc_span::{Span, sym};
use std::ops::ControlFlow;

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".
    ///
    /// 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
    /// conservatively checks that all uses of a variable definitely don't call `wait()` and only then emits a warning.
    /// For that reason, a seemingly unrelated use can get called out as calling `wait()` in help messages.
    ///
    /// ### Control flow
    /// If a `wait()` call exists in an if/then block but not in the else block (or there is no else block),
    /// then this still gets linted as not calling `wait()` in all code paths.
    /// Likewise, when early-returning from the function, `wait()` calls that appear after the return expression
    /// are also not accepted.
    /// In other words, the `wait()` call must be unconditionally reachable after the spawn expression.
    ///
    /// ### 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.83.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 child_ty = cx.typeck_results().expr_ty(expr)
            && child_ty.is_diag_item(cx, sym::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 {
                        cx,
                        local_id,
                        create_id: expr.hir_id,
                        body_id: cx.tcx.hir_enclosing_body_owner(expr.hir_id),
                        state: VisitorState::WalkUpToCreate,
                        early_return: None,
                        missing_wait_branch: None,
                    };

                    let res = (
                        walk_block(&mut vis, enclosing_block),
                        vis.missing_wait_branch,
                        vis.early_return,
                    );

                    let cause = match res {
                        (Break(MaybeWait(wait_span)), _, Some(return_span)) => {
                            Cause::EarlyReturn { wait_span, return_span }
                        },
                        (Break(MaybeWait(_)), _, None) => return,
                        (Continue(()), None, _) => Cause::NeverWait,
                        (Continue(()), Some(MissingWaitBranch::MissingElse { if_span, wait_span }), _) => {
                            Cause::MissingElse { wait_span, if_span }
                        },
                        (Continue(()), Some(MissingWaitBranch::MissingWaitInBranch { branch_span, wait_span }), _) => {
                            Cause::MissingWaitInBranch { wait_span, branch_span }
                        },
                    };

                    // Don't emit a suggestion since the binding is used later
                    check(cx, expr, cause, false);
                },
                Node::LetStmt(&LetStmt { pat, .. }) if let PatKind::Wild = pat.kind => {
                    // `let _ = child;`, also dropped immediately without `wait()`ing
                    check(cx, expr, Cause::NeverWait, true);
                },
                Node::Stmt(&Stmt {
                    kind: StmtKind::Semi(_),
                    ..
                }) => {
                    // Immediately dropped. E.g. `std::process::Command::new("echo").spawn().unwrap();`
                    check(cx, expr, Cause::NeverWait, true);
                },
                _ => {},
            }
        }
    }
}

struct MaybeWait(Span);

/// A visitor responsible for finding a `wait()` call on a local variable.
///
/// 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.
struct WaitFinder<'a, 'tcx> {
    cx: &'a LateContext<'tcx>,
    local_id: HirId,
    create_id: HirId,
    body_id: LocalDefId,
    state: VisitorState,
    early_return: Option<Span>,
    // When joining two if branches where one of them doesn't call `wait()`, stores its span for more targeted help
    // messages
    missing_wait_branch: Option<MissingWaitBranch>,
}

#[derive(PartialEq)]
enum VisitorState {
    WalkUpToCreate,
    CreateFound,
}

#[derive(Copy, Clone)]
enum MissingWaitBranch {
    MissingElse { if_span: Span, wait_span: Span },
    MissingWaitInBranch { branch_span: Span, wait_span: Span },
}

impl<'tcx> Visitor<'tcx> for WaitFinder<'_, 'tcx> {
    type NestedFilter = nested_filter::OnlyBodies;
    type Result = ControlFlow<MaybeWait>;

    fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) -> Self::Result {
        if ex.hir_id == self.create_id {
            self.state = VisitorState::CreateFound;
            return Continue(());
        }

        if self.state != VisitorState::CreateFound {
            return walk_expr(self, ex);
        }

        if ex.res_local_id() == Some(self.local_id) {
            match self.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(self.cx, expr)
                        && let Some(fn_name) = self.cx.tcx.get_diagnostic_name(fn_did)
                        && matches!(fn_name, sym::child_id | sym::child_kill) => {},

                // Conservatively assume that all other kinds of nodes call `.wait()` somehow.
                _ => return Break(MaybeWait(ex.span)),
            }
        } else {
            match ex.kind {
                ExprKind::Ret(e) if self.cx.tcx.hir_enclosing_body_owner(ex.hir_id) == self.body_id => {
                    visit_opt!(self, visit_expr, e);
                    if self.early_return.is_none() {
                        self.early_return = Some(ex.span);
                    }

                    return Continue(());
                },
                ExprKind::If(cond, then, None) => {
                    walk_expr(self, cond)?;

                    if let Break(MaybeWait(wait_span)) = walk_expr(self, then)
                        && self.missing_wait_branch.is_none()
                    {
                        self.missing_wait_branch = Some(MissingWaitBranch::MissingElse {
                            if_span: ex.span,
                            wait_span,
                        });
                    }

                    return Continue(());
                },

                ExprKind::If(cond, then, Some(else_)) => {
                    walk_expr(self, cond)?;

                    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(MaybeWait(wait_span))) => {
                            if self.missing_wait_branch.is_none() {
                                self.missing_wait_branch = Some(MissingWaitBranch::MissingWaitInBranch {
                                    branch_span: ex.span.shrink_to_lo().to(then.span),
                                    wait_span,
                                });
                            }
                        },
                        (Break(MaybeWait(wait_span)), Continue(())) => {
                            if self.missing_wait_branch.is_none() {
                                self.missing_wait_branch = Some(MissingWaitBranch::MissingWaitInBranch {
                                    branch_span: then.span.shrink_to_hi().to(else_.span),
                                    wait_span,
                                });
                            }
                        },

                        // `wait()` in both branches is ok
                        (Break(MaybeWait(wait_span)), Break(MaybeWait(_))) => {
                            self.missing_wait_branch = None;
                            return Break(MaybeWait(wait_span));
                        },
                    }

                    return Continue(());
                },
                _ => {},
            }
        }

        walk_expr(self, ex)
    }

    fn maybe_tcx(&mut self) -> Self::MaybeTyCtxt {
        self.cx.tcx
    }
}

#[derive(Copy, Clone)]
enum Cause {
    /// No call to `wait()` at all
    NeverWait,
    /// `wait()` call exists, but not all code paths definitely lead to one due to
    /// an early return
    EarlyReturn { wait_span: Span, return_span: Span },
    /// `wait()` call exists in some if branches but not this one
    MissingWaitInBranch { wait_span: Span, branch_span: Span },
    /// `wait()` call exists in an if/then branch but it is missing an else block
    MissingElse { wait_span: Span, if_span: Span },
}

impl Cause {
    fn message(self) -> &'static str {
        match self {
            Cause::NeverWait => "spawned process is never `wait()`ed on",
            Cause::EarlyReturn { .. } | Cause::MissingWaitInBranch { .. } | Cause::MissingElse { .. } => {
                "spawned process is not `wait()`ed on in all code paths"
            },
        }
    }

    fn fallback_help(self) -> &'static str {
        match self {
            Cause::NeverWait => "consider calling `.wait()`",
            Cause::EarlyReturn { .. } | Cause::MissingWaitInBranch { .. } | Cause::MissingElse { .. } => {
                "consider calling `.wait()` in all code paths"
            },
        }
    }
}

/// 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>, cause: Cause, emit_suggestion: bool) {
    let Some(block) = get_enclosing_block(cx, spawn_expr.hir_id) else {
        return;
    };

    let mut vis = ExitPointFinder {
        state: ExitPointState::WalkUpTo(spawn_expr.hir_id),
        cx,
    };
    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, cause.message(), |diag| {
        match cause {
            Cause::EarlyReturn { wait_span, return_span } => {
                diag.span_note(
                    return_span,
                    "no `wait()` call exists on the code path to this early return",
                );
                diag.span_note(
                    wait_span,
                    "`wait()` call exists, but it is unreachable due to the early return",
                );
            },
            Cause::MissingWaitInBranch { wait_span, branch_span } => {
                diag.span_note(branch_span, "`wait()` is not called in this if branch");
                diag.span_note(wait_span, "`wait()` is called in the other branch");
            },
            Cause::MissingElse { if_span, wait_span } => {
                diag.span_note(
                    if_span,
                    "this if expression has a `wait()` call, but it is missing an else block",
                );
                diag.span_note(wait_span, "`wait()` called here");
            },
            Cause::NeverWait => {},
        }

        if emit_suggestion {
            diag.span_suggestion(
                spawn_expr.span.shrink_to_hi(),
                "try",
                ".wait()",
                Applicability::MaybeIncorrect,
            );
        } else {
            diag.help(cause.fallback_help());
        }

        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 {
    if let Some(fn_did) = fn_def_id(cx, expr)
        && let Some(fn_name) = cx.tcx.get_diagnostic_name(fn_did)
        && matches!(fn_name, sym::process_exit | sym::process_abort)
    {
        true
    } else {
        false
    }
}

#[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<'tcx> Visitor<'tcx> for ExitPointFinder<'_, '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(()),
        }
    }
}
