use rustc_arena::{DroplessArena, TypedArena};
use rustc_ast::Mutability;
use rustc_data_structures::fx::FxIndexSet;
use rustc_data_structures::stack::ensure_sufficient_stack;
use rustc_errors::codes::*;
use rustc_errors::{Applicability, ErrorGuaranteed, MultiSpan, msg, struct_span_code_err};
use rustc_hir::def::*;
use rustc_hir::def_id::{DefId, LocalDefId};
use rustc_hir::{self as hir, BindingMode, ByRef, HirId, MatchSource};
use rustc_infer::infer::TyCtxtInferExt;
use rustc_middle::bug;
use rustc_middle::thir::visit::Visitor;
use rustc_middle::thir::*;
use rustc_middle::ty::print::with_no_trimmed_paths;
use rustc_middle::ty::{self, AdtDef, Ty, TyCtxt};
use rustc_pattern_analysis::errors::Uncovered;
use rustc_pattern_analysis::rustc::{
    Constructor, DeconstructedPat, MatchArm, RedundancyExplanation, RevealedTy,
    RustcPatCtxt as PatCtxt, Usefulness, UsefulnessReport, WitnessPat,
};
use rustc_session::lint::builtin::{
    BINDINGS_WITH_VARIANT_NAME, IRREFUTABLE_LET_PATTERNS, UNREACHABLE_PATTERNS,
};
use rustc_span::edit_distance::find_best_match_for_name;
use rustc_span::hygiene::DesugaringKind;
use rustc_span::{Ident, Span};
use rustc_trait_selection::infer::InferCtxtExt;
use tracing::instrument;

use crate::diagnostics::*;

pub(crate) fn check_match(tcx: TyCtxt<'_>, def_id: LocalDefId) -> Result<(), ErrorGuaranteed> {
    let typeck_results = tcx.typeck(def_id);
    let (thir, expr) = tcx.thir_body(def_id)?;
    let thir = thir.borrow();
    let pattern_arena = TypedArena::default();
    let dropless_arena = DroplessArena::default();
    let mut visitor = MatchVisitor {
        tcx,
        thir: &*thir,
        typeck_results,
        // FIXME(#132279): We're in a body, should handle opaques.
        typing_env: ty::TypingEnv::non_body_analysis(tcx, def_id),
        hir_source: tcx.local_def_id_to_hir_id(def_id),
        let_source: LetSource::None,
        pattern_arena: &pattern_arena,
        dropless_arena: &dropless_arena,
        error: Ok(()),
    };
    visitor.visit_expr(&thir[expr]);

    let origin = match tcx.def_kind(def_id) {
        DefKind::AssocFn | DefKind::Fn => "function argument",
        DefKind::Closure => "closure argument",
        // other types of MIR don't have function parameters, and we don't need to
        // categorize those for the irrefutable check.
        _ if thir.params.is_empty() => "",
        kind => bug!("unexpected function parameters in THIR: {kind:?} {def_id:?}"),
    };

    for param in thir.params.iter() {
        if let Some(ref pattern) = param.pat {
            visitor.check_binding_is_irrefutable(pattern, origin, None, None, None);
        }
    }
    visitor.error
}

#[derive(Debug, Copy, Clone, PartialEq)]
enum RefutableFlag {
    Irrefutable,
    Refutable,
}
use RefutableFlag::*;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LetSource {
    None,
    PlainLet,
    IfLet,
    IfLetGuard,
    LetElse,
    WhileLet,
    Else,
    ElseIfLet,
}

struct MatchVisitor<'p, 'tcx> {
    tcx: TyCtxt<'tcx>,
    typing_env: ty::TypingEnv<'tcx>,
    typeck_results: &'tcx ty::TypeckResults<'tcx>,
    thir: &'p Thir<'tcx>,
    hir_source: HirId,
    let_source: LetSource,
    pattern_arena: &'p TypedArena<DeconstructedPat<'p, 'tcx>>,
    dropless_arena: &'p DroplessArena,
    /// Tracks if we encountered an error while checking this body. That the first function to
    /// report it stores it here. Some functions return `Result` to allow callers to short-circuit
    /// on error, but callers don't need to store it here again.
    error: Result<(), ErrorGuaranteed>,
}

// Visitor for a thir body. This calls `check_match` and `check_let` as appropriate.
impl<'p, 'tcx> Visitor<'p, 'tcx> for MatchVisitor<'p, 'tcx> {
    fn thir(&self) -> &'p Thir<'tcx> {
        self.thir
    }

    #[instrument(level = "trace", skip(self))]
    fn visit_arm(&mut self, arm: &'p Arm<'tcx>) {
        self.with_hir_source(arm.hir_id, |this| {
            if let Some(expr) = arm.guard {
                this.with_let_source(LetSource::IfLetGuard, |this| {
                    this.visit_expr(&this.thir[expr])
                });
            }
            this.visit_pat(&arm.pattern);
            this.visit_expr(&self.thir[arm.body]);
        });
    }

    #[instrument(level = "trace", skip(self))]
    fn visit_expr(&mut self, ex: &'p Expr<'tcx>) {
        match ex.kind {
            ExprKind::Scope { value, hir_id, .. } => {
                self.with_hir_source(hir_id, |this| {
                    this.visit_expr(&this.thir[value]);
                });
                return;
            }
            ExprKind::If { cond, then, else_opt, if_then_scope: _ } => {
                // Give a specific `let_source` for the condition.
                let let_source = match ex.span.desugaring_kind() {
                    Some(DesugaringKind::WhileLoop) => LetSource::WhileLet,
                    _ => match self.let_source {
                        LetSource::Else => LetSource::ElseIfLet,
                        _ => LetSource::IfLet,
                    },
                };
                self.with_let_source(let_source, |this| this.visit_expr(&self.thir[cond]));
                self.with_let_source(LetSource::None, |this| {
                    this.visit_expr(&this.thir[then]);
                });
                if let Some(else_) = else_opt {
                    self.with_let_source(LetSource::Else, |this| {
                        this.visit_expr(&this.thir[else_])
                    });
                }
                return;
            }
            ExprKind::Match { scrutinee, ref arms, match_source } => {
                self.check_match(scrutinee, arms, match_source, ex.span);
            }
            ExprKind::LoopMatch {
                match_data: LoopMatchMatchData { scrutinee, ref arms, span },
                ..
            } => {
                self.check_match(scrutinee, arms, MatchSource::Normal, span);
            }
            ExprKind::Let { ref pat, expr } => {
                self.check_let(pat, Some(expr), ex.span, None);
            }
            ExprKind::LogicalOp { op: LogicalOp::And, .. }
                if !matches!(self.let_source, LetSource::None) =>
            {
                let mut chain_refutabilities = Vec::new();
                let Ok(()) = self.visit_land(ex, &mut chain_refutabilities) else { return };
                // Lint only single irrefutable let binding.
                if let [Some((_, Irrefutable))] = chain_refutabilities[..] {
                    self.lint_single_let(ex.span, None, None);
                }
                return;
            }
            _ => {}
        };
        self.with_let_source(LetSource::None, |this| visit::walk_expr(this, ex));
    }

    fn visit_stmt(&mut self, stmt: &'p Stmt<'tcx>) {
        match stmt.kind {
            StmtKind::Let { ref pattern, initializer, else_block, hir_id, span, .. } => {
                self.with_hir_source(hir_id, |this| {
                    let let_source =
                        if else_block.is_some() { LetSource::LetElse } else { LetSource::PlainLet };
                    let else_span = else_block.map(|bid| this.thir.blocks[bid].span);
                    this.with_let_source(let_source, |this| {
                        this.check_let(pattern, initializer, span, else_span)
                    });
                    visit::walk_stmt(this, stmt);
                });
            }
            StmtKind::Expr { .. } => {
                visit::walk_stmt(self, stmt);
            }
        }
    }
}

impl<'p, 'tcx> MatchVisitor<'p, 'tcx> {
    #[instrument(level = "trace", skip(self, f))]
    fn with_let_source(&mut self, let_source: LetSource, f: impl FnOnce(&mut Self)) {
        let old_let_source = self.let_source;
        self.let_source = let_source;
        ensure_sufficient_stack(|| f(self));
        self.let_source = old_let_source;
    }

    fn with_hir_source<T>(&mut self, new_hir_source: HirId, f: impl FnOnce(&mut Self) -> T) -> T {
        let old_hir_source = self.hir_source;
        self.hir_source = new_hir_source;
        let ret = f(self);
        self.hir_source = old_hir_source;
        ret
    }

    /// Visit a nested chain of `&&`. Used for if-let chains. This must call `visit_expr` on the
    /// subexpressions we are not handling ourselves.
    fn visit_land(
        &mut self,
        ex: &'p Expr<'tcx>,
        accumulator: &mut Vec<Option<(Span, RefutableFlag)>>,
    ) -> Result<(), ErrorGuaranteed> {
        match ex.kind {
            ExprKind::Scope { value, hir_id, .. } => {
                self.with_hir_source(hir_id, |this| this.visit_land(&this.thir[value], accumulator))
            }
            ExprKind::LogicalOp { op: LogicalOp::And, lhs, rhs } => {
                // We recurse into the lhs only, because `&&` chains associate to the left.
                let res_lhs = self.visit_land(&self.thir[lhs], accumulator);
                let res_rhs = self.visit_land_rhs(&self.thir[rhs])?;
                accumulator.push(res_rhs);
                res_lhs
            }
            _ => {
                let res = self.visit_land_rhs(ex)?;
                accumulator.push(res);
                Ok(())
            }
        }
    }

    /// Visit the right-hand-side of a `&&`. Used for if-let chains. Returns `Some` if the
    /// expression was ultimately a `let ... = ...`, and `None` if it was a normal boolean
    /// expression. This must call `visit_expr` on the subexpressions we are not handling ourselves.
    fn visit_land_rhs(
        &mut self,
        ex: &'p Expr<'tcx>,
    ) -> Result<Option<(Span, RefutableFlag)>, ErrorGuaranteed> {
        match ex.kind {
            ExprKind::Scope { value, hir_id, .. } => {
                self.with_hir_source(hir_id, |this| this.visit_land_rhs(&this.thir[value]))
            }
            ExprKind::Let { ref pat, expr } => {
                let expr = &self.thir()[expr];
                self.with_let_source(LetSource::None, |this| {
                    this.visit_expr(expr);
                });
                Ok(Some((ex.span, self.is_let_irrefutable(pat, Some(expr))?)))
            }
            _ => {
                self.with_let_source(LetSource::None, |this| {
                    this.visit_expr(ex);
                });
                Ok(None)
            }
        }
    }

    fn lower_pattern(
        &mut self,
        cx: &PatCtxt<'p, 'tcx>,
        pat: &'p Pat<'tcx>,
    ) -> Result<&'p DeconstructedPat<'p, 'tcx>, ErrorGuaranteed> {
        if let Err(err) = pat.pat_error_reported() {
            self.error = Err(err);
            Err(err)
        } else {
            // Check the pattern for some things unrelated to exhaustiveness.
            let refutable = if cx.refutable { Refutable } else { Irrefutable };
            let mut err = Ok(());
            pat.walk_always(|pat| {
                check_borrow_conflicts_in_at_patterns(self, pat);
                check_for_bindings_named_same_as_variants(self, pat, refutable);
                err = err.and(check_never_pattern(cx, pat));
            });
            err?;
            Ok(self.pattern_arena.alloc(cx.lower_pat(pat)))
        }
    }

    /// Inspects the match scrutinee expression to determine whether the place it evaluates to may
    /// hold invalid data.
    fn is_known_valid_scrutinee(&self, scrutinee: &Expr<'tcx>) -> bool {
        use ExprKind::*;
        match &scrutinee.kind {
            // Pointers can validly point to a place with invalid data. It is undecided whether
            // references can too, so we conservatively assume they can.
            Deref { .. } => false,
            // Inherit validity of the parent place, unless the parent is an union.
            Field { lhs, .. } => {
                let lhs = &self.thir()[*lhs];
                match lhs.ty.kind() {
                    ty::Adt(def, _) if def.is_union() => false,
                    _ => self.is_known_valid_scrutinee(lhs),
                }
            }
            // Essentially a field access.
            Index { lhs, .. } => {
                let lhs = &self.thir()[*lhs];
                self.is_known_valid_scrutinee(lhs)
            }

            // No-op.
            Scope { value, .. } => self.is_known_valid_scrutinee(&self.thir()[*value]),

            // Casts don't cause a load.
            NeverToAny { source }
            | Cast { source }
            | Use { source }
            | PointerCoercion { source, .. }
            | PlaceTypeAscription { source, .. }
            | ValueTypeAscription { source, .. }
            | PlaceUnwrapUnsafeBinder { source }
            | ValueUnwrapUnsafeBinder { source }
            | WrapUnsafeBinder { source } => self.is_known_valid_scrutinee(&self.thir()[*source]),

            // These diverge.
            Become { .. }
            | Break { .. }
            | Continue { .. }
            | ConstContinue { .. }
            | Return { .. } => true,

            // These are statements that evaluate to `()`.
            Assign { .. } | AssignOp { .. } | InlineAsm { .. } | Let { .. } => true,

            // These evaluate to a value.
            RawBorrow { .. }
            | Adt { .. }
            | Array { .. }
            | Binary { .. }
            | Block { .. }
            | Borrow { .. }
            | Call { .. }
            | ByUse { .. }
            | Closure { .. }
            | ConstBlock { .. }
            | ConstParam { .. }
            | If { .. }
            | Literal { .. }
            | LogicalOp { .. }
            | Loop { .. }
            | LoopMatch { .. }
            | Match { .. }
            | NamedConst { .. }
            | NonHirLiteral { .. }
            | Repeat { .. }
            | StaticRef { .. }
            | ThreadLocalRef { .. }
            | Tuple { .. }
            | Unary { .. }
            | UpvarRef { .. }
            | VarRef { .. }
            | ZstLiteral { .. }
            | Yield { .. }
            | Reborrow { .. } => true,
        }
    }

    fn new_cx(
        &self,
        refutability: RefutableFlag,
        whole_match_span: Option<Span>,
        scrutinee: Option<&Expr<'tcx>>,
        scrut_span: Span,
    ) -> PatCtxt<'p, 'tcx> {
        let refutable = match refutability {
            Irrefutable => false,
            Refutable => true,
        };
        // If we don't have a scrutinee we're either a function parameter or a `let x;`. Both cases
        // require validity.
        let known_valid_scrutinee =
            scrutinee.map(|scrut| self.is_known_valid_scrutinee(scrut)).unwrap_or(true);
        PatCtxt {
            tcx: self.tcx,
            typeck_results: self.typeck_results,
            typing_env: self.typing_env,
            module: self.tcx.parent_module(self.hir_source).to_def_id(),
            dropless_arena: self.dropless_arena,
            match_lint_level: self.hir_source,
            whole_match_span,
            scrut_span,
            refutable,
            known_valid_scrutinee,
            internal_state: Default::default(),
        }
    }

    fn analyze_patterns(
        &mut self,
        cx: &PatCtxt<'p, 'tcx>,
        arms: &[MatchArm<'p, 'tcx>],
        scrut_ty: Ty<'tcx>,
    ) -> Result<UsefulnessReport<'p, 'tcx>, ErrorGuaranteed> {
        let report =
            rustc_pattern_analysis::rustc::analyze_match(&cx, &arms, scrut_ty).map_err(|err| {
                self.error = Err(err);
                err
            })?;

        // Warn unreachable subpatterns.
        for (arm, is_useful) in report.arm_usefulness.iter() {
            if let Usefulness::Useful(redundant_subpats) = is_useful
                && !redundant_subpats.is_empty()
            {
                let mut redundant_subpats = redundant_subpats.clone();
                // Emit lints in the order in which they occur in the file.
                redundant_subpats.sort_unstable_by_key(|(pat, _)| pat.data().span);
                for (pat, explanation) in redundant_subpats {
                    report_unreachable_pattern(cx, arm.arm_data, pat, &explanation, None)
                }
            }
        }
        Ok(report)
    }

    #[instrument(level = "trace", skip(self))]
    fn check_let(
        &mut self,
        pat: &'p Pat<'tcx>,
        scrutinee: Option<ExprId>,
        span: Span,
        else_span: Option<Span>,
    ) {
        assert!(self.let_source != LetSource::None);
        let scrut = scrutinee.map(|id| &self.thir[id]);
        if let LetSource::PlainLet = self.let_source {
            // `lhs = rhs` destructuring assignments are lowered to a `let` tagged
            // `AssignDesugar`; report them as assignments, not `let` bindings (#157553).
            if let hir::Node::LetStmt(&hir::LetStmt {
                source: hir::LocalSource::AssignDesugar,
                ..
            }) = self.tcx.hir_node(self.hir_source)
            {
                self.check_binding_is_irrefutable(
                    pat,
                    "assignment",
                    Some(Inform { descr: "destructuring assignments" }),
                    scrut,
                    None,
                );
            } else {
                self.check_binding_is_irrefutable(
                    pat,
                    "local binding",
                    Some(Inform { descr: "`let` bindings" }),
                    scrut,
                    Some(span),
                );
            }
        } else if let Ok(Irrefutable) = self.is_let_irrefutable(pat, scrut) {
            if span.from_expansion() {
                self.lint_single_let(span, None, None);
                return;
            }
            let let_else_span = self.check_irrefutable_option_some(pat, scrut, span);

            let sm = self.tcx.sess.source_map();
            let next_token_start = sm.span_extend_while_whitespace(span.clone()).hi();
            let line_span = sm.span_extend_to_line(span.clone()).with_lo(next_token_start);
            let else_keyword_span = sm.span_until_whitespace(line_span);
            self.lint_single_let(span, Some(else_keyword_span), let_else_span);
        }
    }

    /// Check case `let x = Some(y);`, user likely intended to destructure `Option`
    fn check_irrefutable_option_some(
        &self,
        pat: &'p Pat<'tcx>,
        initializer: Option<&Expr<'tcx>>,
        span: Span,
    ) -> Option<LetElseReplacementSuggestion> {
        if let sm = self.tcx.sess.source_map()
            && let Some(initializer) = initializer
            && let Some(s_ty) = initializer.ty.ty_adt_def()
            && self.tcx.is_diagnostic_item(rustc_span::sym::Option, s_ty.did())
            && let ExprKind::Scope { value, .. } = initializer.kind
            && let initializer_expr = &self.thir[value]
            && let ExprKind::Adt(AdtExpr { fields, .. }) = &initializer_expr.kind
            && let Some(field) = fields.first()
            && let inner = &self.thir[field.expr]
            && let Some(inner_ty) = inner.ty.ty_adt_def()
            && self.tcx.is_diagnostic_item(rustc_span::sym::Option, inner_ty.did())
            && let Ok(rhs) = sm.span_to_snippet(inner.span)
            && let Ok(lhs) = sm.span_to_snippet(pat.span)
        {
            let lhs = format!("Some({})", lhs);
            Some(LetElseReplacementSuggestion { span, lhs, rhs })
        } else {
            None
        }
    }

    fn check_match(
        &mut self,
        scrut: ExprId,
        arms: &[ArmId],
        source: hir::MatchSource,
        expr_span: Span,
    ) {
        let scrut = &self.thir[scrut];
        let cx = self.new_cx(Refutable, Some(expr_span), Some(scrut), scrut.span);

        let mut tarms = Vec::with_capacity(arms.len());
        for &arm in arms {
            let arm = &self.thir.arms[arm];
            let got_error = self.with_hir_source(arm.hir_id, |this| {
                let Ok(pat) = this.lower_pattern(&cx, &arm.pattern) else { return true };
                let arm =
                    MatchArm { pat, arm_data: this.hir_source, has_guard: arm.guard.is_some() };
                tarms.push(arm);
                false
            });
            if got_error {
                return;
            }
        }

        let Ok(report) = self.analyze_patterns(&cx, &tarms, scrut.ty) else { return };

        match source {
            // Don't report arm reachability of desugared `match $iter.into_iter() { iter => .. }`
            // when the iterator is an uninhabited type. unreachable_code will trigger instead.
            hir::MatchSource::ForLoopDesugar if arms.len() == 1 => {}
            hir::MatchSource::ForLoopDesugar
            | hir::MatchSource::Postfix
            | hir::MatchSource::Normal
            | hir::MatchSource::FormatArgs => {
                let is_match_arm =
                    matches!(source, hir::MatchSource::Postfix | hir::MatchSource::Normal);
                report_arm_reachability(&cx, &report, is_match_arm);
            }
            // Unreachable patterns in try and await expressions occur when one of
            // the arms are an uninhabited type. Which is OK.
            hir::MatchSource::AwaitDesugar | hir::MatchSource::TryDesugar(_) => {}
        }

        // Check if the match is exhaustive.
        let witnesses = report.non_exhaustiveness_witnesses;
        if !witnesses.is_empty() {
            if source == hir::MatchSource::ForLoopDesugar
                && let [_, snd_arm] = *arms
            {
                // the for loop pattern is not irrefutable
                let pat = &self.thir[snd_arm].pattern;
                // `pat` should be `Some(<pat_field>)` from a desugared for loop.
                debug_assert_eq!(pat.span.desugaring_kind(), Some(DesugaringKind::ForLoop));
                let PatKind::Variant { ref subpatterns, .. } = pat.kind else { bug!() };
                let [pat_field] = &subpatterns[..] else { bug!() };
                self.check_binding_is_irrefutable(
                    &pat_field.pattern,
                    "`for` loop binding",
                    None,
                    None,
                    None,
                );
            } else {
                // span after scrutinee, or after `.match`. That is, the braces, arms,
                // and any whitespace preceding the braces.
                let braces_span = match source {
                    hir::MatchSource::Normal => scrut
                        .span
                        .find_ancestor_in_same_ctxt(expr_span)
                        .map(|scrut_span| scrut_span.shrink_to_hi().with_hi(expr_span.hi())),
                    hir::MatchSource::Postfix => {
                        // This is horrendous, and we should deal with it by just
                        // stashing the span of the braces somewhere (like in the match source).
                        scrut.span.find_ancestor_in_same_ctxt(expr_span).and_then(|scrut_span| {
                            let sm = self.tcx.sess.source_map();
                            let brace_span = sm.span_extend_to_next_char(scrut_span, '{', true);
                            if sm.span_to_snippet(sm.next_point(brace_span)).as_deref() == Ok("{") {
                                let sp = brace_span.shrink_to_hi().with_hi(expr_span.hi());
                                // We also need to extend backwards for whitespace
                                sm.span_extend_prev_while(sp, |c| c.is_whitespace()).ok()
                            } else {
                                None
                            }
                        })
                    }
                    hir::MatchSource::ForLoopDesugar
                    | hir::MatchSource::TryDesugar(_)
                    | hir::MatchSource::AwaitDesugar
                    | hir::MatchSource::FormatArgs => None,
                };

                // Check if the match would be exhaustive if all guards were removed.
                // If so, we leave a note that guards don't count towards exhaustivity.
                let would_be_exhaustive_without_guards = {
                    let any_arm_has_guard = tarms.iter().any(|arm| arm.has_guard);
                    any_arm_has_guard && {
                        let guardless_arms: Vec<_> =
                            tarms.iter().map(|arm| MatchArm { has_guard: false, ..*arm }).collect();
                        rustc_pattern_analysis::rustc::analyze_match(&cx, &guardless_arms, scrut.ty)
                            .is_ok_and(|report| report.non_exhaustiveness_witnesses.is_empty())
                    }
                };
                self.error = Err(report_non_exhaustive_match(
                    &cx,
                    self.thir,
                    scrut.ty,
                    scrut.span,
                    witnesses,
                    arms,
                    braces_span,
                    would_be_exhaustive_without_guards,
                ));
            }
        }
    }

    #[instrument(level = "trace", skip(self))]
    fn lint_single_let(
        &mut self,
        let_span: Span,
        else_keyword_span: Option<Span>,
        let_else_span: Option<LetElseReplacementSuggestion>,
    ) {
        report_irrefutable_let_patterns(
            self.tcx,
            self.hir_source,
            self.let_source,
            1,
            let_span,
            else_keyword_span,
            let_else_span,
        );
    }

    fn analyze_binding(
        &mut self,
        pat: &'p Pat<'tcx>,
        refutability: RefutableFlag,
        scrut: Option<&Expr<'tcx>>,
    ) -> Result<(PatCtxt<'p, 'tcx>, UsefulnessReport<'p, 'tcx>), ErrorGuaranteed> {
        let cx = self.new_cx(refutability, None, scrut, pat.span);
        let pat = self.lower_pattern(&cx, pat)?;
        let arms = [MatchArm { pat, arm_data: self.hir_source, has_guard: false }];
        let report = self.analyze_patterns(&cx, &arms, pat.ty().inner())?;
        Ok((cx, report))
    }

    fn is_let_irrefutable(
        &mut self,
        pat: &'p Pat<'tcx>,
        scrut: Option<&Expr<'tcx>>,
    ) -> Result<RefutableFlag, ErrorGuaranteed> {
        let (cx, report) = self.analyze_binding(pat, Refutable, scrut)?;
        // Report if the pattern is unreachable, which can only occur when the type is uninhabited.
        report_arm_reachability(&cx, &report, false);
        // If the list of witnesses is empty, the match is exhaustive, i.e. the `if let` pattern is
        // irrefutable.
        Ok(if report.non_exhaustiveness_witnesses.is_empty() { Irrefutable } else { Refutable })
    }

    #[instrument(level = "trace", skip(self))]
    fn check_binding_is_irrefutable(
        &mut self,
        pat: &'p Pat<'tcx>,
        origin: &str,
        inform: Option<Inform>,
        scrut: Option<&Expr<'tcx>>,
        sp: Option<Span>,
    ) {
        let pattern_ty = pat.ty;

        let Ok((cx, report)) = self.analyze_binding(pat, Irrefutable, scrut) else { return };
        let witnesses = report.non_exhaustiveness_witnesses;
        if witnesses.is_empty() {
            // The pattern is irrefutable.
            return;
        }

        let mut let_suggestion = None;
        let mut misc_suggestion = None;
        let mut interpreted_as_const = None;
        let mut interpreted_as_const_sugg = None;

        if let Some(def_id) = is_const_pat_that_looks_like_binding(self.tcx, pat) {
            let span = self.tcx.def_span(def_id);
            let variable = self.tcx.item_name(def_id).to_string();
            // When we encounter a constant as the binding name, point at the `const` definition.
            interpreted_as_const = Some(InterpretedAsConst { span, variable: variable.clone() });
            interpreted_as_const_sugg = Some(InterpretedAsConstSugg { span: pat.span, variable });
        } else if let PatKind::Constant { .. } = pat.kind
            && let Ok(snippet) = self.tcx.sess.source_map().span_to_snippet(pat.span)
        {
            // If the pattern to match is an integer literal:
            if snippet.chars().all(|c| c.is_digit(10)) {
                // Then give a suggestion, the user might've meant to create a binding instead.
                misc_suggestion = Some(MiscPatternSuggestion::AttemptedIntegerLiteral {
                    start_span: pat.span.shrink_to_lo(),
                });
            }
        }

        if let Some(span) = sp
            && self.tcx.sess.source_map().is_span_accessible(span)
            && interpreted_as_const.is_none()
            && scrut.is_some()
        {
            let mut bindings = vec![];
            pat.each_binding(|name, _, _, _| bindings.push(name));

            let semi_span = span.shrink_to_hi();
            let start_span = span.shrink_to_lo();
            let end_span = semi_span.shrink_to_lo();
            let count = witnesses.len();

            let_suggestion = Some(if bindings.is_empty() {
                SuggestLet::If { start_span, semi_span, count }
            } else {
                SuggestLet::Else { end_span, count }
            });
        };

        let adt_defined_here = report_adt_defined_here(self.tcx, pattern_ty, &witnesses, false);

        // Emit an extra note if the first uncovered witness would be uninhabited
        // if we disregard visibility.
        let witness_1_is_privately_uninhabited = if let Some(witness_1) = witnesses.get(0)
            && let ty::Adt(adt, args) = witness_1.ty().kind()
            && adt.is_enum()
            && let Constructor::Variant(variant_index) = witness_1.ctor()
        {
            let variant_inhabited = adt
                .variant(*variant_index)
                .inhabited_predicate(self.tcx, *adt)
                .instantiate(self.tcx, args);
            variant_inhabited.apply(self.tcx, cx.typing_env, cx.module)
                && !variant_inhabited.apply_ignore_module(self.tcx, cx.typing_env)
        } else {
            false
        };

        let witness_1 = cx.print_witness_pat(witnesses.get(0).unwrap());

        self.error = Err(self.tcx.dcx().emit_err(PatternNotCovered {
            span: pat.span,
            origin,
            uncovered: Uncovered::new(pat.span, &cx, witnesses),
            inform,
            interpreted_as_const,
            interpreted_as_const_sugg,
            witness_1_is_privately_uninhabited,
            witness_1,
            _p: (),
            pattern_ty,
            let_suggestion,
            misc_suggestion,
            adt_defined_here,
        }));
    }
}

/// Check if a by-value binding is by-value. That is, check if the binding's type is not `Copy`.
/// Check that there are no borrow or move conflicts in `binding @ subpat` patterns.
///
/// For example, this would reject:
/// - `ref x @ Some(ref mut y)`,
/// - `ref mut x @ Some(ref y)`,
/// - `ref mut x @ Some(ref mut y)`,
/// - `ref mut? x @ Some(y)`, and
/// - `x @ Some(ref mut? y)`.
///
/// This analysis is *not* subsumed by NLL.
fn check_borrow_conflicts_in_at_patterns<'tcx>(cx: &MatchVisitor<'_, 'tcx>, pat: &Pat<'tcx>) {
    // Extract `sub` in `binding @ sub`.
    let PatKind::Binding { name, mode, ty, subpattern: Some(ref sub), .. } = pat.kind else {
        return;
    };

    let is_binding_by_move = |ty: Ty<'tcx>| !cx.tcx.type_is_copy_modulo_regions(cx.typing_env, ty);

    let sess = cx.tcx.sess;

    // Get the binding move, extract the mutability if by-ref.
    let mut_outer = match mode.0 {
        ByRef::No if is_binding_by_move(ty) => {
            // We have `x @ pat` where `x` is by-move. Reject all borrows in `pat`.
            let mut conflicts_ref = Vec::new();
            sub.each_binding(|_, mode, _, span| {
                if matches!(mode, ByRef::Yes(..)) {
                    conflicts_ref.push(span)
                }
            });
            if !conflicts_ref.is_empty() {
                sess.dcx().emit_err(BorrowOfMovedValue {
                    binding_span: pat.span,
                    conflicts_ref,
                    name: Ident::new(name, pat.span),
                    ty,
                    suggest_borrowing: Some(pat.span.shrink_to_lo()),
                });
            }
            return;
        }
        ByRef::No => return,
        ByRef::Yes(_, m) => m,
    };

    // We now have `ref $mut_outer binding @ sub` (semantically).
    // Recurse into each binding in `sub` and find mutability or move conflicts.
    let mut conflicts_move = Vec::new();
    let mut conflicts_mut_mut = Vec::new();
    let mut conflicts_mut_ref = Vec::new();
    sub.each_binding(|name, mode, ty, span| {
        match mode {
            ByRef::Yes(_, mut_inner) => match (mut_outer, mut_inner) {
                // Both sides are `ref`.
                (Mutability::Not, Mutability::Not) => {}
                // 2x `ref mut`.
                (Mutability::Mut, Mutability::Mut) => {
                    conflicts_mut_mut.push(Conflict::Mut { span, name })
                }
                (Mutability::Not, Mutability::Mut) => {
                    conflicts_mut_ref.push(Conflict::Mut { span, name })
                }
                (Mutability::Mut, Mutability::Not) => {
                    conflicts_mut_ref.push(Conflict::Ref { span, name })
                }
            },
            ByRef::No if is_binding_by_move(ty) => {
                conflicts_move.push(Conflict::Moved { span, name }) // `ref mut?` + by-move conflict.
            }
            ByRef::No => {} // `ref mut?` + by-copy is fine.
        }
    });

    let report_mut_mut = !conflicts_mut_mut.is_empty();
    let report_mut_ref = !conflicts_mut_ref.is_empty();
    let report_move_conflict = !conflicts_move.is_empty();

    let mut occurrences = match mut_outer {
        Mutability::Mut => vec![Conflict::Mut { span: pat.span, name }],
        Mutability::Not => vec![Conflict::Ref { span: pat.span, name }],
    };
    occurrences.extend(conflicts_mut_mut);
    occurrences.extend(conflicts_mut_ref);
    occurrences.extend(conflicts_move);

    // Report errors if any.
    if report_mut_mut {
        // Report mutability conflicts for e.g. `ref mut x @ Some(ref mut y)`.
        sess.dcx().emit_err(MultipleMutBorrows { span: pat.span, occurrences });
    } else if report_mut_ref {
        // Report mutability conflicts for e.g. `ref x @ Some(ref mut y)` or the converse.
        match mut_outer {
            Mutability::Mut => {
                sess.dcx().emit_err(AlreadyMutBorrowed { span: pat.span, occurrences });
            }
            Mutability::Not => {
                sess.dcx().emit_err(AlreadyBorrowed { span: pat.span, occurrences });
            }
        };
    } else if report_move_conflict {
        // Report by-ref and by-move conflicts, e.g. `ref x @ y`.
        sess.dcx().emit_err(MovedWhileBorrowed { span: pat.span, occurrences });
    }
}

fn check_for_bindings_named_same_as_variants(
    cx: &MatchVisitor<'_, '_>,
    pat: &Pat<'_>,
    rf: RefutableFlag,
) {
    if let PatKind::Binding {
        name,
        mode: BindingMode(ByRef::No, Mutability::Not),
        subpattern: None,
        ty,
        ..
    } = pat.kind
        && let ty::Adt(edef, _) = ty.peel_refs().kind()
        && edef.is_enum()
        && edef
            .variants()
            .iter()
            .any(|variant| variant.name == name && variant.ctor_kind() == Some(CtorKind::Const))
    {
        let variant_count = edef.variants().len();
        let ty_path = with_no_trimmed_paths!(cx.tcx.def_path_str(edef.did()));
        cx.tcx.emit_node_span_lint(
            BINDINGS_WITH_VARIANT_NAME,
            cx.hir_source,
            pat.span,
            BindingsWithVariantName {
                // If this is an irrefutable pattern, and there's > 1 variant,
                // then we can't actually match on this. Applying the below
                // suggestion would produce code that breaks on `check_binding_is_irrefutable`.
                suggestion: if rf == Refutable || variant_count == 1 {
                    Some(pat.span)
                } else {
                    None
                },
                ty_path,
                name: Ident::new(name, pat.span),
            },
        )
    }
}

/// Check that never patterns are only used on inhabited types.
fn check_never_pattern<'tcx>(
    cx: &PatCtxt<'_, 'tcx>,
    pat: &Pat<'tcx>,
) -> Result<(), ErrorGuaranteed> {
    if let PatKind::Never = pat.kind {
        if !cx.is_uninhabited(pat.ty) {
            return Err(cx.tcx.dcx().emit_err(NonEmptyNeverPattern { span: pat.span, ty: pat.ty }));
        }
    }
    Ok(())
}

fn report_irrefutable_let_patterns(
    tcx: TyCtxt<'_>,
    id: HirId,
    source: LetSource,
    count: usize,
    span: Span,
    else_keyword_span: Option<Span>,
    let_else_span: Option<LetElseReplacementSuggestion>,
) {
    macro_rules! emit_diag {
        ($lint:tt) => {{
            tcx.emit_node_span_lint(IRREFUTABLE_LET_PATTERNS, id, span, $lint { count });
        }};
    }

    match source {
        LetSource::None | LetSource::PlainLet | LetSource::Else => bug!(),
        LetSource::IfLet | LetSource::ElseIfLet => emit_diag!(IrrefutableLetPatternsIfLet),
        LetSource::IfLetGuard => emit_diag!(IrrefutableLetPatternsIfLetGuard),
        LetSource::LetElse => {
            let spans = match else_keyword_span {
                Some(else_keyword_span) => {
                    let mut spans = MultiSpan::from_span(else_keyword_span);
                    spans.push_span_label(
                        span,
                        msg!("assigning to binding pattern will always succeed"),
                    );
                    spans
                }
                None => span.into(),
            };

            tcx.emit_node_span_lint(
                IRREFUTABLE_LET_PATTERNS,
                id,
                spans,
                IrrefutableLetPatternsLetElse { be_replaced: let_else_span },
            );
        }
        LetSource::WhileLet => emit_diag!(IrrefutableLetPatternsWhileLet),
    }
}

/// Report unreachable arms, if any.
fn report_unreachable_pattern<'p, 'tcx>(
    cx: &PatCtxt<'p, 'tcx>,
    hir_id: HirId,
    pat: &DeconstructedPat<'p, 'tcx>,
    explanation: &RedundancyExplanation<'p, 'tcx>,
    whole_arm_span: Option<Span>,
) {
    static CAP_COVERED_BY_MANY: usize = 4;
    let pat_span = pat.data().span;
    let mut lint = UnreachablePatternInner {
        span: Some(pat_span),
        matches_no_values: None,
        matches_no_values_ty: **pat.ty(),
        uninhabited_note: None,
        covered_by_catchall: None,
        covered_by_one: None,
        covered_by_many: None,
        wanted_constant: None,
        accessible_constant: None,
        inaccessible_constant: None,
        pattern_let_binding: None,
        suggest_remove: None,
    };
    let mut covered_by_many_n_more_count = None;
    match explanation.covered_by.as_slice() {
        [] => {
            // Empty pattern; we report the uninhabited type that caused the emptiness.
            lint.span = None; // Don't label the pattern itself
            lint.uninhabited_note = Some(()); // Give a link about empty types
            lint.matches_no_values = Some(pat_span);
            lint.suggest_remove = whole_arm_span; // Suggest to remove the match arm
            pat.walk(&mut |subpat| {
                let ty = **subpat.ty();
                if cx.is_uninhabited(ty) {
                    lint.matches_no_values_ty = ty;
                    false // No need to dig further.
                } else if matches!(subpat.ctor(), Constructor::Ref | Constructor::UnionField) {
                    false // Don't explore further since they are not by-value.
                } else {
                    true
                }
            });
        }
        [covering_pat] if pat_is_catchall(covering_pat) => {
            // A binding pattern that matches all, a single binding name.
            let pat = covering_pat.data();
            lint.covered_by_catchall = Some(pat.span);
            find_fallback_pattern_typo(cx, hir_id, pat, &mut lint);
        }
        [covering_pat] => {
            lint.covered_by_one = Some(covering_pat.data().span);
        }
        covering_pats => {
            let mut iter = covering_pats.iter();
            let mut multispan = MultiSpan::from_span(pat_span);
            for p in iter.by_ref().take(CAP_COVERED_BY_MANY) {
                multispan.push_span_label(p.data().span, msg!("matches some of the same values"));
            }
            let remain = iter.count();
            if remain == 0 {
                multispan.push_span_label(pat_span, msg!("collectively making this unreachable"));
            } else {
                covered_by_many_n_more_count = Some(remain);
                multispan.push_span_label(
                    pat_span,
                    msg!("...and {$covered_by_many_n_more_count} other patterns collectively make this unreachable"),
                );
            }
            lint.covered_by_many = Some(multispan);
        }
    }
    cx.tcx.emit_node_span_lint(
        UNREACHABLE_PATTERNS,
        hir_id,
        pat_span,
        UnreachablePattern { inner: lint, covered_by_many_n_more_count },
    );
}

/// Detect typos that were meant to be a `const` but were interpreted as a new pattern binding.
fn find_fallback_pattern_typo<'tcx>(
    cx: &PatCtxt<'_, 'tcx>,
    hir_id: HirId,
    pat: &Pat<'tcx>,
    lint: &mut UnreachablePatternInner<'_>,
) {
    if cx.tcx.lint_level_spec_at_node(UNREACHABLE_PATTERNS, hir_id).is_allow() {
        // This is because we use `with_no_trimmed_paths` later, so if we never emit the lint we'd
        // ICE. At the same time, we don't really need to do all of this if we won't emit anything.
        return;
    }
    if let PatKind::Binding { name, subpattern: None, ty, .. } = pat.kind {
        // See if the binding might have been a `const` that was mistyped or out of scope.
        let mut accessible = vec![];
        let mut accessible_path = vec![];
        let mut inaccessible = vec![];
        let mut imported = vec![];
        let mut imported_spans = vec![];
        let (infcx, param_env) = cx.tcx.infer_ctxt().build_with_typing_env(cx.typing_env);
        let parent = cx.tcx.hir_get_parent_item(hir_id);

        for item in cx.tcx.hir_crate_items(()).free_items() {
            if let DefKind::Use = cx.tcx.def_kind(item.owner_id) {
                // Look for consts being re-exported.
                let item = cx.tcx.hir_expect_item(item.owner_id.def_id);
                let hir::ItemKind::Use(path, _) = item.kind else {
                    continue;
                };
                if let Some(value_ns) = path.res.value_ns
                    && let Res::Def(DefKind::Const { .. }, id) = value_ns
                    && infcx.can_eq(
                        param_env,
                        ty,
                        cx.tcx.type_of(id).instantiate_identity().skip_norm_wip(),
                    )
                {
                    if cx.tcx.visibility(id).is_accessible_from(parent, cx.tcx) {
                        // The original const is accessible, suggest using it directly.
                        let item_name = cx.tcx.item_name(id);
                        accessible.push(item_name);
                        accessible_path.push(with_no_trimmed_paths!(cx.tcx.def_path_str(id)));
                    } else if cx.tcx.visibility(item.owner_id).is_accessible_from(parent, cx.tcx) {
                        // The const is accessible only through the re-export, point at
                        // the `use`.
                        let ident = item.kind.ident().unwrap();
                        imported.push(ident.name);
                        imported_spans.push(ident.span);
                    }
                }
            }
            if let DefKind::Const { .. } = cx.tcx.def_kind(item.owner_id)
                && infcx.can_eq(
                    param_env,
                    ty,
                    cx.tcx.type_of(item.owner_id).instantiate_identity().skip_norm_wip(),
                )
            {
                // Look for local consts.
                let item_name = cx.tcx.item_name(item.owner_id);
                let vis = cx.tcx.visibility(item.owner_id);
                if vis.is_accessible_from(parent, cx.tcx) {
                    accessible.push(item_name);
                    // FIXME: the line below from PR #135310 is a workaround for the ICE in issue
                    // #135289, where a macro in a dependency can create unreachable patterns in the
                    // current crate. Path trimming expects diagnostics for a typoed const, but no
                    // diagnostics are emitted and we ICE. See
                    // `tests/ui/resolve/const-with-typo-in-pattern-binding-ice-135289.rs` for a
                    // test that reproduces the ICE if we don't use `with_no_trimmed_paths!`.
                    let path = with_no_trimmed_paths!(cx.tcx.def_path_str(item.owner_id));
                    accessible_path.push(path);
                } else if name == item_name {
                    // The const exists somewhere in this crate, but it can't be imported
                    // from this pattern's scope. We'll just point at its definition.
                    inaccessible.push(cx.tcx.def_span(item.owner_id));
                }
            }
        }
        if let Some((i, &const_name)) =
            accessible.iter().enumerate().find(|&(_, &const_name)| const_name == name)
        {
            // The pattern name is an exact match, so the pattern needed to be imported.
            lint.wanted_constant = Some(WantedConstant {
                span: pat.span,
                is_typo: false,
                const_name: const_name.to_string(),
                const_path: accessible_path[i].clone(),
            });
        } else if let Some(name) = find_best_match_for_name(&accessible, name, None) {
            // The pattern name is likely a typo.
            lint.wanted_constant = Some(WantedConstant {
                span: pat.span,
                is_typo: true,
                const_name: name.to_string(),
                const_path: name.to_string(),
            });
        } else if let Some(i) =
            imported.iter().enumerate().find(|&(_, &const_name)| const_name == name).map(|(i, _)| i)
        {
            // The const with the exact name wasn't re-exported from an import in this
            // crate, we point at the import.
            lint.accessible_constant = Some(imported_spans[i]);
        } else if let Some(name) = find_best_match_for_name(&imported, name, None) {
            // The typoed const wasn't re-exported by an import in this crate, we suggest
            // the right name (which will likely require another follow up suggestion).
            lint.wanted_constant = Some(WantedConstant {
                span: pat.span,
                is_typo: true,
                const_path: name.to_string(),
                const_name: name.to_string(),
            });
        } else if !inaccessible.is_empty() {
            for span in inaccessible {
                // The const with the exact name match isn't accessible, we just point at it.
                lint.inaccessible_constant = Some(span);
            }
        } else {
            // Look for local bindings for people that might have gotten confused with how
            // `let` and `const` works.
            for (_, node) in cx.tcx.hir_parent_iter(hir_id) {
                match node {
                    hir::Node::Stmt(hir::Stmt { kind: hir::StmtKind::Let(let_stmt), .. }) => {
                        if let hir::PatKind::Binding(_, _, binding_name, _) = let_stmt.pat.kind {
                            if name == binding_name.name {
                                lint.pattern_let_binding = Some(binding_name.span);
                            }
                        }
                    }
                    hir::Node::Block(hir::Block { stmts, .. }) => {
                        for stmt in *stmts {
                            if let hir::StmtKind::Let(let_stmt) = stmt.kind
                                && let hir::PatKind::Binding(_, _, binding_name, _) =
                                    let_stmt.pat.kind
                                && name == binding_name.name
                            {
                                lint.pattern_let_binding = Some(binding_name.span);
                            }
                        }
                    }
                    hir::Node::Item(_) => break,
                    _ => {}
                }
            }
        }
    }
}

/// Report unreachable arms, if any.
fn report_arm_reachability<'p, 'tcx>(
    cx: &PatCtxt<'p, 'tcx>,
    report: &UsefulnessReport<'p, 'tcx>,
    is_match_arm: bool,
) {
    let sm = cx.tcx.sess.source_map();
    for (arm, is_useful) in report.arm_usefulness.iter() {
        if let Usefulness::Redundant(explanation) = is_useful {
            let hir_id = arm.arm_data;
            let arm_span = cx.tcx.hir_span(hir_id);
            let whole_arm_span = if is_match_arm {
                // If the arm is followed by a comma, extend the span to include it.
                if let Some(comma) = sm.span_followed_by(arm_span, ",") {
                    Some(arm_span.to(comma))
                } else {
                    Some(arm_span)
                }
            } else {
                None
            };
            report_unreachable_pattern(cx, hir_id, arm.pat, explanation, whole_arm_span)
        }
    }
}

/// Checks for common cases of "catchall" patterns that may not be intended as such.
fn pat_is_catchall(pat: &DeconstructedPat<'_, '_>) -> bool {
    match pat.ctor() {
        Constructor::Wildcard => true,
        Constructor::Struct | Constructor::Ref => {
            pat.iter_fields().all(|ipat| pat_is_catchall(&ipat.pat))
        }
        _ => false,
    }
}

/// If the given pattern is a named constant that looks like it could have been
/// intended to be a binding, returns the `DefId` of the named constant.
///
/// Diagnostics use this to give more detailed suggestions for non-exhaustive
/// matches.
fn is_const_pat_that_looks_like_binding<'tcx>(tcx: TyCtxt<'tcx>, pat: &Pat<'tcx>) -> Option<DefId> {
    // The pattern must be a named constant, and the name that appears in
    // the pattern's source text must resemble a plain identifier without any
    // `::` namespace separators or other non-identifier characters.
    if let Some(def_id) = try { pat.extra.as_deref()?.expanded_const? }
        && matches!(tcx.def_kind(def_id), DefKind::Const { .. })
        && let Ok(snippet) = tcx.sess.source_map().span_to_snippet(pat.span)
        && snippet.chars().all(|c| c.is_alphanumeric() || c == '_')
    {
        Some(def_id)
    } else {
        None
    }
}

/// Report that a match is not exhaustive.
fn report_non_exhaustive_match<'p, 'tcx>(
    cx: &PatCtxt<'p, 'tcx>,
    thir: &Thir<'tcx>,
    scrut_ty: Ty<'tcx>,
    sp: Span,
    witnesses: Vec<WitnessPat<'p, 'tcx>>,
    arms: &[ArmId],
    braces_span: Option<Span>,
    would_be_exhaustive_without_guards: bool,
) -> ErrorGuaranteed {
    let is_empty_match = arms.is_empty();
    let non_empty_enum = match scrut_ty.kind() {
        ty::Adt(def, _) => def.is_enum() && !def.variants().is_empty(),
        _ => false,
    };
    // In the case of an empty match, replace the '`_` not covered' diagnostic with something more
    // informative.
    if is_empty_match && !non_empty_enum {
        return cx.tcx.dcx().emit_err(NonExhaustivePatternsTypeNotEmpty {
            cx,
            scrut_span: sp,
            braces_span,
            ty: scrut_ty,
        });
    }

    // FIXME: migration of this diagnostic will require list support
    let joined_patterns = joined_uncovered_patterns(cx, &witnesses);
    let mut err = struct_span_code_err!(
        cx.tcx.dcx(),
        sp,
        E0004,
        "non-exhaustive patterns: {joined_patterns} not covered"
    );
    err.span_label(
        sp,
        format!(
            "pattern{} {} not covered",
            rustc_errors::pluralize!(witnesses.len()),
            joined_patterns
        ),
    );

    // Point at the definition of non-covered `enum` variants.
    if let Some(AdtDefinedHere { adt_def_span, ty, variants }) =
        report_adt_defined_here(cx.tcx, scrut_ty, &witnesses, true)
    {
        let mut multi_span = MultiSpan::from_span(adt_def_span);
        multi_span.push_span_label(adt_def_span, "");
        for Variant { span } in variants {
            multi_span.push_span_label(span, "not covered");
        }
        err.span_note(multi_span, format!("`{ty}` defined here"));
    }
    err.note(format!("the matched value is of type `{}`", scrut_ty));

    if !is_empty_match {
        let mut special_tys = FxIndexSet::default();
        // Look at the first witness.
        collect_special_tys(cx, &witnesses[0], &mut special_tys);

        for ty in special_tys {
            if ty.is_ptr_sized_integral() {
                if ty.inner() == cx.tcx.types.usize {
                    err.note(format!(
                        "`{ty}::MAX` is not treated as exhaustive, \
                        so half-open ranges are necessary to match exhaustively",
                    ));
                } else if ty.inner() == cx.tcx.types.isize {
                    err.note(format!(
                        "`{ty}::MIN` and `{ty}::MAX` are not treated as exhaustive, \
                        so half-open ranges are necessary to match exhaustively",
                    ));
                }
            } else if ty.inner() == cx.tcx.types.str_ {
                err.note("`&str` cannot be matched exhaustively, so a wildcard `_` is necessary");
            } else if cx.is_foreign_non_exhaustive_enum(ty) {
                err.note(format!("`{ty}` is marked as non-exhaustive, so a wildcard `_` is necessary to match exhaustively"));
            } else if cx.is_uninhabited(ty.inner()) {
                // The type is uninhabited yet there is a witness: we must be in the `MaybeInvalid`
                // case.
                err.note(format!("`{ty}` is uninhabited but is not being matched by value, so a wildcard `_` is required"));
            }
        }
    }

    if let ty::Ref(_, sub_ty, _) = scrut_ty.kind() {
        if !sub_ty.is_inhabited_from(cx.tcx, cx.module, cx.typing_env) {
            err.note("references are always considered inhabited");
        }
    }

    for &arm in arms {
        let arm = &thir.arms[arm];
        if let Some(def_id) = is_const_pat_that_looks_like_binding(cx.tcx, &arm.pattern) {
            let const_name = cx.tcx.item_name(def_id);
            err.span_label(
                arm.pattern.span,
                format!(
                    "this pattern doesn't introduce a new catch-all binding, but rather pattern \
                     matches against the value of constant `{const_name}`",
                ),
            );
            err.span_note(cx.tcx.def_span(def_id), format!("constant `{const_name}` defined here"));
            err.span_suggestion_verbose(
                arm.pattern.span.shrink_to_hi(),
                "if you meant to introduce a binding, use a different name",
                "_var".to_string(),
                Applicability::MaybeIncorrect,
            );
        }
    }

    // Whether we suggest the actual missing patterns or `_`.
    let suggest_the_witnesses = witnesses.len() < 4;
    let suggested_arm = if suggest_the_witnesses {
        let pattern = witnesses
            .iter()
            .map(|witness| cx.print_witness_pat(witness))
            .collect::<Vec<String>>()
            .join(" | ");
        if witnesses.iter().all(|p| p.is_never_pattern()) && cx.tcx.features().never_patterns() {
            // Arms with a never pattern don't take a body.
            pattern
        } else {
            format!("{pattern} => todo!()")
        }
    } else {
        format!("_ => todo!()")
    };
    let mut suggestion = None;
    let sm = cx.tcx.sess.source_map();
    match arms {
        [] if let Some(braces_span) = braces_span => {
            // Get the span for the empty match body `{}`.
            let (indentation, more) = if let Some(snippet) = sm.indentation_before(sp) {
                (format!("\n{snippet}"), "    ")
            } else {
                (" ".to_string(), "")
            };
            suggestion = Some((
                braces_span,
                format!(" {{{indentation}{more}{suggested_arm},{indentation}}}",),
            ));
        }
        [only] => {
            let only = &thir[*only];
            let (pre_indentation, is_multiline) = if let Some(snippet) =
                sm.indentation_before(only.span)
                && let Ok(with_trailing) =
                    sm.span_extend_while(only.span, |c| c.is_whitespace() || c == ',')
                && sm.is_multiline(with_trailing)
            {
                (format!("\n{snippet}"), true)
            } else {
                (" ".to_string(), false)
            };
            let only_body = &thir[only.body];
            let comma = if matches!(only_body.kind, ExprKind::Block { .. })
                && only.span.eq_ctxt(only_body.span)
                && is_multiline
            {
                ""
            } else {
                ","
            };
            suggestion = Some((
                only.span.shrink_to_hi(),
                format!("{comma}{pre_indentation}{suggested_arm}"),
            ));
        }
        [.., prev, last] => {
            let prev = &thir[*prev];
            let last = &thir[*last];
            if prev.span.eq_ctxt(last.span) {
                let last_body = &thir[last.body];
                let comma = if matches!(last_body.kind, ExprKind::Block { .. })
                    && last.span.eq_ctxt(last_body.span)
                {
                    ""
                } else {
                    ","
                };
                let spacing = if sm.is_multiline(prev.span.between(last.span)) {
                    sm.indentation_before(last.span).map(|indent| format!("\n{indent}"))
                } else {
                    Some(" ".to_string())
                };
                if let Some(spacing) = spacing {
                    suggestion = Some((
                        last.span.shrink_to_hi(),
                        format!("{comma}{spacing}{suggested_arm}"),
                    ));
                }
            }
        }
        _ => {}
    }

    let msg = format!(
        "ensure that all possible cases are being handled by adding a match arm with a wildcard \
         pattern{}{}",
        if witnesses.len() > 1 && suggest_the_witnesses && suggestion.is_some() {
            ", a match arm with multiple or-patterns"
        } else {
            // we are either not suggesting anything, or suggesting `_`
            ""
        },
        match witnesses.len() {
            // non-exhaustive enum case
            0 if suggestion.is_some() => " as shown",
            0 => "",
            1 if suggestion.is_some() => " or an explicit pattern as shown",
            1 => " or an explicit pattern",
            _ if suggestion.is_some() => " as shown, or multiple match arms",
            _ => " or multiple match arms",
        },
    );

    if would_be_exhaustive_without_guards {
        err.subdiagnostic(NonExhaustiveMatchAllArmsGuarded);
    }
    if let Some((span, sugg)) = suggestion {
        err.span_suggestion_verbose(span, msg, sugg, Applicability::HasPlaceholders);
    } else {
        err.help(msg);
    }
    err.emit()
}

fn joined_uncovered_patterns<'p, 'tcx>(
    cx: &PatCtxt<'p, 'tcx>,
    witnesses: &[WitnessPat<'p, 'tcx>],
) -> String {
    const LIMIT: usize = 3;
    let pat_to_str = |pat: &WitnessPat<'p, 'tcx>| cx.print_witness_pat(pat);
    match witnesses {
        [] => bug!(),
        [witness] => format!("`{}`", cx.print_witness_pat(witness)),
        [head @ .., tail] if head.len() < LIMIT => {
            let head: Vec<_> = head.iter().map(pat_to_str).collect();
            format!("`{}` and `{}`", head.join("`, `"), cx.print_witness_pat(tail))
        }
        _ => {
            let (head, tail) = witnesses.split_at(LIMIT);
            let head: Vec<_> = head.iter().map(pat_to_str).collect();
            format!("`{}` and {} more", head.join("`, `"), tail.len())
        }
    }
}

/// Collect types that require specific explanations when they show up in witnesses.
fn collect_special_tys<'tcx>(
    cx: &PatCtxt<'_, 'tcx>,
    pat: &WitnessPat<'_, 'tcx>,
    special_tys: &mut FxIndexSet<RevealedTy<'tcx>>,
) {
    if matches!(pat.ctor(), Constructor::NonExhaustive | Constructor::Never) {
        special_tys.insert(*pat.ty());
    }
    if let Constructor::IntRange(range) = pat.ctor() {
        if cx.is_range_beyond_boundaries(range, *pat.ty()) {
            // The range denotes the values before `isize::MIN` or the values after `usize::MAX`/`isize::MAX`.
            special_tys.insert(*pat.ty());
        }
    }
    pat.iter_fields().for_each(|field_pat| collect_special_tys(cx, field_pat, special_tys))
}

fn report_adt_defined_here<'tcx>(
    tcx: TyCtxt<'tcx>,
    ty: Ty<'tcx>,
    witnesses: &[WitnessPat<'_, 'tcx>],
    point_at_non_local_ty: bool,
) -> Option<AdtDefinedHere<'tcx>> {
    let ty = ty.peel_refs();
    let ty::Adt(def, _) = ty.kind() else {
        return None;
    };
    let adt_def_span =
        tcx.hir_get_if_local(def.did()).and_then(|node| node.ident()).map(|ident| ident.span);
    let adt_def_span = if point_at_non_local_ty {
        adt_def_span.unwrap_or_else(|| tcx.def_span(def.did()))
    } else {
        adt_def_span?
    };

    let mut variants = vec![];
    for span in maybe_point_at_variant(tcx, *def, witnesses.iter().take(5)) {
        variants.push(Variant { span });
    }
    Some(AdtDefinedHere { adt_def_span, ty, variants })
}

fn maybe_point_at_variant<'a, 'p: 'a, 'tcx: 'p>(
    tcx: TyCtxt<'tcx>,
    def: AdtDef<'tcx>,
    patterns: impl Iterator<Item = &'a WitnessPat<'p, 'tcx>>,
) -> Vec<Span> {
    let mut covered = vec![];
    for pattern in patterns {
        if let Constructor::Variant(variant_index) = pattern.ctor() {
            if let ty::Adt(this_def, _) = pattern.ty().kind()
                && this_def.did() != def.did()
            {
                continue;
            }
            let sp = def.variant(*variant_index).ident(tcx).span;
            if covered.contains(&sp) {
                // Don't point at variants that have already been covered due to other patterns to avoid
                // visual clutter.
                continue;
            }
            covered.push(sp);
        }
        covered.extend(maybe_point_at_variant(tcx, def, pattern.iter_fields()));
    }
    covered
}
