use clippy_utils::diagnostics::span_lint;
use clippy_utils::get_enclosing_block;
use clippy_utils::res::{MaybeDef, MaybeResPath};
use clippy_utils::visitors::{Visitable, for_each_expr};
use core::ops::ControlFlow;
use rustc_hir::{Body, ExprKind, HirId, LangItem, LetStmt, Node, PatKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::symbol::sym;

declare_clippy_lint! {
    /// ### What it does
    /// Checks for collections that are never queried.
    ///
    /// ### Why is this bad?
    /// Putting effort into constructing a collection but then never querying it might indicate that
    /// the author forgot to do whatever they intended to do with the collection. Example: Clone
    /// a vector, sort it for iteration, but then mistakenly iterate the original vector
    /// instead.
    ///
    /// ### Example
    /// ```no_run
    /// # let samples = vec![3, 1, 2];
    /// let mut sorted_samples = samples.clone();
    /// sorted_samples.sort();
    /// for sample in &samples { // Oops, meant to use `sorted_samples`.
    ///     println!("{sample}");
    /// }
    /// ```
    /// Use instead:
    /// ```no_run
    /// # let samples = vec![3, 1, 2];
    /// let mut sorted_samples = samples.clone();
    /// sorted_samples.sort();
    /// for sample in &sorted_samples {
    ///     println!("{sample}");
    /// }
    /// ```
    #[clippy::version = "1.70.0"]
    pub COLLECTION_IS_NEVER_READ,
    nursery,
    "a collection is never queried"
}
declare_lint_pass!(CollectionIsNeverRead => [COLLECTION_IS_NEVER_READ]);

impl<'tcx> LateLintPass<'tcx> for CollectionIsNeverRead {
    fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx LetStmt<'tcx>) {
        // Look for local variables whose type is a container. Search surrounding block for read access.
        if let PatKind::Binding(_, local_id, _, _) = local.pat.kind
            && match_acceptable_type(cx, local)
            && let Some(enclosing_block) = get_enclosing_block(cx, local.hir_id)
            && has_no_read_access(cx, local_id, enclosing_block)
        {
            span_lint(cx, COLLECTION_IS_NEVER_READ, local.span, "collection is never read");
        }
    }
}

fn match_acceptable_type(cx: &LateContext<'_>, local: &LetStmt<'_>) -> bool {
    let ty = cx.typeck_results().pat_ty(local.pat);
    matches!(
        ty.opt_diag_name(cx),
        Some(
            sym::BTreeMap
                | sym::BTreeSet
                | sym::BinaryHeap
                | sym::HashMap
                | sym::HashSet
                | sym::LinkedList
                | sym::Option
                | sym::Vec
                | sym::VecDeque
        )
    ) || ty.is_lang_item(cx, LangItem::String)
}

fn has_no_read_access<'tcx, T: Visitable<'tcx>>(cx: &LateContext<'tcx>, id: HirId, block: T) -> bool {
    let mut has_access = false;
    let mut has_read_access = false;

    // Inspect all expressions and sub-expressions in the block.
    for_each_expr(cx, block, |expr| {
        // Ignore expressions that are not simply `id`.
        if expr.res_local_id() != Some(id) {
            return ControlFlow::Continue(());
        }

        // `id` is being accessed. Investigate if it's a read access.
        has_access = true;

        // `id` appearing in the left-hand side of an assignment is not a read access:
        //
        // id = ...; // Not reading `id`.
        if let Node::Expr(parent) = cx.tcx.parent_hir_node(expr.hir_id)
            && let ExprKind::Assign(lhs, ..) = parent.kind
            && lhs.res_local_id() == Some(id)
        {
            return ControlFlow::Continue(());
        }

        // Look for method call with receiver `id`. It might be a non-read access:
        //
        // id.foo(args)
        //
        // Only assuming this for "official" methods defined on the type. For methods defined in extension
        // traits (identified as local, based on the orphan rule), pessimistically assume that they might
        // have side effects, so consider them a read.
        if let Node::Expr(parent) = cx.tcx.parent_hir_node(expr.hir_id)
            && let ExprKind::MethodCall(_, receiver, args, _) = parent.kind
            && receiver.res_local_id() == Some(id)
            && let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(parent.hir_id)
            && !method_def_id.is_local()
        {
            // If this "official" method takes closures,
            // it has read access if one of the closures has read access.
            //
            // items.retain(|item| send_item(item).is_ok());
            let is_read_in_closure_arg = args.iter().any(|arg| {
                if let ExprKind::Closure(closure) = arg.kind
                    // To keep things simple, we only check the first param to see if its read.
                    && let Body { params: [param, ..], value } = cx.tcx.hir_body(closure.body)
                {
                    !has_no_read_access(cx, param.hir_id, *value)
                } else {
                    false
                }
            });
            if is_read_in_closure_arg {
                has_read_access = true;
                return ControlFlow::Break(());
            }

            // The method call is a statement, so the return value is not used. That's not a read access:
            //
            // id.foo(args);
            if let Node::Stmt(..) = cx.tcx.parent_hir_node(parent.hir_id) {
                return ControlFlow::Continue(());
            }

            // The method call is not a statement, so its return value is used somehow but its type is the
            // unit type, so this is not a real read access. Examples:
            //
            // let y = x.clear();
            // println!("{:?}", x.clear());
            if cx.typeck_results().expr_ty(parent).is_unit() {
                return ControlFlow::Continue(());
            }
        }

        // Any other access to `id` is a read access. Stop searching.
        has_read_access = true;
        ControlFlow::Break(())
    });

    // Ignore collections that have no access at all. Other lints should catch them.
    has_access && !has_read_access
}
