use rustc_hir::{Crate, Expr, ExprKind, QPath};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::{declare_tool_lint, impl_lint_pass};

use crate::utils::{is_entrypoint_fn, is_no_std_crate, snippet, span_lint_and_help};
use if_chain::if_chain;

declare_clippy_lint! {
    /// **What it does:** Checks for recursion using the entrypoint.
    ///
    /// **Why is this bad?** Apart from special setups (which we could detect following attributes like #![no_std]),
    /// recursing into main() seems like an unintuitive antipattern we should be able to detect.
    ///
    /// **Known problems:** None.
    ///
    /// **Example:**
    /// ```no_run
    /// fn main() {
    ///     main();
    /// }
    /// ```
    pub MAIN_RECURSION,
    style,
    "recursion using the entrypoint"
}

#[derive(Default)]
pub struct MainRecursion {
    has_no_std_attr: bool,
}

impl_lint_pass!(MainRecursion => [MAIN_RECURSION]);

impl LateLintPass<'_> for MainRecursion {
    fn check_crate(&mut self, _: &LateContext<'_>, krate: &Crate<'_>) {
        self.has_no_std_attr = is_no_std_crate(krate);
    }

    fn check_expr_post(&mut self, cx: &LateContext<'_>, expr: &Expr<'_>) {
        if self.has_no_std_attr {
            return;
        }

        if_chain! {
            if let ExprKind::Call(func, _) = &expr.kind;
            if let ExprKind::Path(path) = &func.kind;
            if let QPath::Resolved(_, path) = &path;
            if let Some(def_id) = path.res.opt_def_id();
            if is_entrypoint_fn(cx, def_id);
            then {
                span_lint_and_help(
                    cx,
                    MAIN_RECURSION,
                    func.span,
                    &format!("recursing into entrypoint `{}`", snippet(cx, func.span, "main")),
                    None,
                    "consider using another function for this recursion"
                )
            }
        }
    }
}
