//! Various diagnostics for expressions that are collected together in one pass
//! through the body using inference results: mismatched arg counts, missing
//! fields, etc.

use std::fmt;

use base_db::Crate;
use either::Either;
use hir_def::{
    AdtId, AssocItemId, CallableDefId, DefWithBodyId, HasModule, ItemContainerId, Lookup,
    attrs::AttrFlags,
    lang_item::LangItems,
    resolver::{HasResolver, ValueNs},
};
use intern::sym;
use itertools::Itertools;
use rustc_hash::FxHashSet;
use rustc_pattern_analysis::constructor::Constructor;
use rustc_type_ir::inherent::IntoKind;
use syntax::{
    AstNode,
    ast::{self, UnaryOp},
};
use tracing::debug;

use typed_arena::Arena;

use crate::{
    Adjust, InferenceResult,
    db::HirDatabase,
    diagnostics::match_check::{
        self,
        pat_analysis::{self, DeconstructedPat, MatchCheckCtx, WitnessPat},
    },
    display::{DisplayTarget, HirDisplay},
    next_solver::{
        CallableIdWrapper, DbInterner, ParamEnv, Ty, TyKind, TypingMode,
        infer::{DbInternerInferExt, InferCtxt},
    },
};

pub(crate) use hir_def::{
    LocalFieldId, VariantId,
    expr_store::Body,
    hir::{Expr, ExprId, MatchArm, Pat, PatId, RecordSpread, Statement},
};

pub enum BodyValidationDiagnostic<'db> {
    RecordMissingFields {
        record: Either<ExprId, PatId>,
        variant: VariantId,
        missed_fields: Vec<LocalFieldId>,
    },
    ReplaceFilterMapNextWithFindMap {
        method_call_expr: ExprId,
    },
    MissingMatchArms {
        match_expr: ExprId,
        uncovered_patterns: String,
    },
    NonExhaustiveLet {
        pat: PatId,
        uncovered_patterns: String,
    },
    RemoveTrailingReturn {
        return_expr: ExprId,
    },
    RemoveUnnecessaryElse {
        if_expr: ExprId,
    },
    UnusedMustUse {
        expr: ExprId,
        message: Option<&'db str>,
    },
}

impl<'db> BodyValidationDiagnostic<'db> {
    pub fn collect(
        db: &'db dyn HirDatabase,
        owner: DefWithBodyId,
        validate_lints: bool,
    ) -> Vec<BodyValidationDiagnostic<'db>> {
        let _p = tracing::info_span!("BodyValidationDiagnostic::collect").entered();
        let infer = InferenceResult::of(db, owner);
        let body = Body::of(db, owner);
        let env = db.trait_environment(owner.into());
        let interner = DbInterner::new_with(db, owner.krate(db));
        let infcx =
            interner.infer_ctxt().build(TypingMode::typeck_for_body(interner, owner.into()));
        let mut validator = ExprValidator {
            owner,
            body,
            infer,
            diagnostics: Vec::new(),
            validate_lints,
            env,
            infcx,
        };
        validator.validate_body();
        validator.diagnostics
    }
}

struct ExprValidator<'db> {
    owner: DefWithBodyId,
    body: &'db Body,
    infer: &'db InferenceResult,
    env: ParamEnv<'db>,
    diagnostics: Vec<BodyValidationDiagnostic<'db>>,
    validate_lints: bool,
    infcx: InferCtxt<'db>,
}

impl<'db> ExprValidator<'db> {
    #[inline]
    fn db(&self) -> &'db dyn HirDatabase {
        self.infcx.interner.db
    }

    fn validate_body(&mut self) {
        let db = self.db();
        let mut filter_map_next_checker = None;
        // we'll pass &mut self while iterating over body.exprs, so they need to be disjoint
        let body = self.body;

        if matches!(self.owner, DefWithBodyId::FunctionId(_)) {
            self.check_for_trailing_return(body.root_expr(), body);
        }

        for (id, expr) in body.exprs() {
            if let Some((variant, missed_fields)) =
                record_literal_missing_fields(db, self.infer, id, expr)
            {
                self.diagnostics.push(BodyValidationDiagnostic::RecordMissingFields {
                    record: Either::Left(id),
                    variant,
                    missed_fields,
                });
            }

            match expr {
                Expr::Match { expr, arms } => {
                    self.validate_match(id, *expr, arms);
                }
                Expr::Call { .. } | Expr::MethodCall { .. } => {
                    self.validate_call(id, expr, &mut filter_map_next_checker);
                }
                Expr::Closure { body: body_expr, .. } => {
                    self.check_for_trailing_return(*body_expr, body);
                }
                Expr::If { .. } => {
                    self.check_for_unnecessary_else(id, expr);
                }
                Expr::Block { .. } | Expr::Unsafe { .. } => {
                    self.validate_block(expr);
                }
                _ => {}
            }
        }

        for (id, pat) in body.pats() {
            if let Some((variant, missed_fields)) =
                record_pattern_missing_fields(db, self.infer, id, pat)
            {
                self.diagnostics.push(BodyValidationDiagnostic::RecordMissingFields {
                    record: Either::Right(id),
                    variant,
                    missed_fields,
                });
            }
        }
    }

    fn validate_call(
        &mut self,
        call_id: ExprId,
        expr: &Expr,
        filter_map_next_checker: &mut Option<FilterMapNextChecker<'db>>,
    ) {
        if !self.validate_lints {
            return;
        }
        // Check that the number of arguments matches the number of parameters.

        if self.infer.exprs_have_type_mismatches() {
            // FIXME: Due to shortcomings in the current type system implementation, only emit
            // this diagnostic if there are no type mismatches in the containing function.
        } else if let Expr::MethodCall { receiver, .. } = expr {
            let (callee, _) = match self.infer.method_resolution(call_id) {
                Some(it) => it,
                None => return,
            };

            let checker = filter_map_next_checker.get_or_insert_with(|| {
                FilterMapNextChecker::new(self.infcx.interner.lang_items(), self.db())
            });

            if checker.check(call_id, receiver, &callee).is_some() {
                self.diagnostics.push(BodyValidationDiagnostic::ReplaceFilterMapNextWithFindMap {
                    method_call_expr: call_id,
                });
            }

            if let Some(receiver_ty) = self.infer.type_of_expr_with_adjust(*receiver) {
                checker.prev_receiver_ty = Some(receiver_ty);
            }
        }
    }

    fn validate_match(&mut self, match_expr: ExprId, scrutinee_expr: ExprId, arms: &[MatchArm]) {
        let Some(scrut_ty) = self.infer.type_of_expr_with_adjust(scrutinee_expr) else {
            return;
        };
        if scrut_ty.references_non_lt_error() {
            return;
        }

        let cx = MatchCheckCtx::new(self.owner.module(self.db()), &self.infcx, self.env);

        let pattern_arena = Arena::new();
        let mut m_arms = Vec::with_capacity(arms.len());
        let mut has_lowering_errors = false;
        // Note: Skipping the entire diagnostic rather than just not including a faulty match arm is
        // preferred to avoid the chance of false positives.
        for arm in arms {
            let pat_ty = self.infer.type_of_pat_with_adjust(arm.pat);
            if pat_ty.references_non_lt_error() {
                return;
            }

            // We only include patterns whose type matches the type
            // of the scrutinee expression. If we had an InvalidMatchArmPattern
            // diagnostic or similar we could raise that in an else
            // block here.
            //
            // When comparing the types, we also have to consider that rustc
            // will automatically de-reference the scrutinee expression type if
            // necessary.
            //
            // FIXME we should use the type checker for this.
            if (pat_ty == scrut_ty
                || scrut_ty
                    .as_reference()
                    .is_none_or(|(match_expr_ty, ..)| match_expr_ty == pat_ty))
                && types_of_subpatterns_do_match(arm.pat, self.body, self.infer)
            {
                // If we had a NotUsefulMatchArm diagnostic, we could
                // check the usefulness of each pattern as we added it
                // to the matrix here.
                let pat = self.lower_pattern(&cx, arm.pat, &mut has_lowering_errors);
                let m_arm = pat_analysis::MatchArm {
                    pat: pattern_arena.alloc(pat),
                    has_guard: arm.guard.is_some(),
                    arm_data: (),
                };
                m_arms.push(m_arm);
                if !has_lowering_errors {
                    continue;
                }
            }
            // If the pattern type doesn't fit the match expression, we skip this diagnostic.
            cov_mark::hit!(validate_match_bailed_out);
            return;
        }

        let known_valid_scrutinee = Some(self.is_known_valid_scrutinee(scrutinee_expr));
        let report =
            match cx.compute_match_usefulness(m_arms.as_slice(), scrut_ty, known_valid_scrutinee) {
                Ok(report) => report,
                Err(()) => return,
            };

        // FIXME Report unreachable arms
        // https://github.com/rust-lang/rust/blob/f31622a50/compiler/rustc_mir_build/src/thir/pattern/check_match.rs#L200

        let witnesses = report.non_exhaustiveness_witnesses;
        if !witnesses.is_empty() {
            self.diagnostics.push(BodyValidationDiagnostic::MissingMatchArms {
                match_expr,
                uncovered_patterns: missing_match_arms(
                    &cx,
                    scrut_ty,
                    witnesses,
                    m_arms.is_empty(),
                    self.owner.krate(self.db()),
                ),
            });
        }
    }

    // [rustc's `is_known_valid_scrutinee`](https://github.com/rust-lang/rust/blob/c9bd03cb724e13cca96ad320733046cbdb16fbbe/compiler/rustc_mir_build/src/thir/pattern/check_match.rs#L288)
    //
    // While the above function in rustc uses thir exprs, r-a doesn't have them.
    // So, the logic here is getting same result as "hir lowering + match with lowered thir"
    // with "hir only"
    fn is_known_valid_scrutinee(&self, scrutinee_expr: ExprId) -> bool {
        let db = self.db();

        if self
            .infer
            .expr_adjustments
            .get(&scrutinee_expr)
            .is_some_and(|adjusts| adjusts.iter().any(|a| matches!(a.kind, Adjust::Deref(..))))
        {
            return false;
        }

        match &self.body[scrutinee_expr] {
            Expr::UnaryOp { op: UnaryOp::Deref, .. } => false,
            Expr::Path(path) => {
                let value_or_partial = self.owner.resolver(db).resolve_path_in_value_ns_fully(
                    db,
                    path,
                    self.body.expr_path_hygiene(scrutinee_expr),
                );
                value_or_partial.is_none_or(|v| !matches!(v, ValueNs::StaticId(_)))
            }
            Expr::Field { expr, .. } => match self.infer.expr_ty(*expr).kind() {
                TyKind::Adt(adt, ..) if matches!(adt.def_id(), AdtId::UnionId(_)) => false,
                _ => self.is_known_valid_scrutinee(*expr),
            },
            Expr::Index { base, .. } => self.is_known_valid_scrutinee(*base),
            Expr::Cast { expr, .. } => self.is_known_valid_scrutinee(*expr),
            Expr::Missing => false,
            _ => true,
        }
    }

    fn validate_block(&mut self, expr: &Expr) {
        let (Expr::Block { statements, .. } | Expr::Unsafe { statements, .. }) = expr else {
            return;
        };
        let pattern_arena = Arena::new();
        let cx = MatchCheckCtx::new(self.owner.module(self.db()), &self.infcx, self.env);
        for stmt in &**statements {
            let diag = match *stmt {
                Statement::Expr { expr: stmt_expr, has_semi: true } if self.validate_lints => {
                    self.check_unused_must_use(stmt_expr)
                }
                Statement::Let { pat, initializer, else_branch: None, .. } => {
                    self.check_non_exhaustive_let(&cx, &pattern_arena, pat, initializer)
                }
                _ => None,
            };
            if let Some(diag) = diag {
                self.diagnostics.push(diag);
            }
        }
    }

    fn check_non_exhaustive_let<'a>(
        &self,
        cx: &MatchCheckCtx<'a, 'db>,
        pattern_arena: &'a Arena<DeconstructedPat<'a, 'db>>,
        pat: PatId,
        initializer: Option<ExprId>,
    ) -> Option<BodyValidationDiagnostic<'db>> {
        if self.infer.pat_has_type_mismatch(pat) {
            return None;
        }
        let initializer = initializer?;
        let ty = self.infer.type_of_expr_with_adjust(initializer)?;
        if ty.references_non_lt_error() {
            return None;
        }

        let mut have_errors = false;
        let deconstructed_pat = self.lower_pattern(cx, pat, &mut have_errors);

        // optimization, wildcard trivially hold
        if have_errors || matches!(deconstructed_pat.ctor(), Constructor::Wildcard) {
            return None;
        }

        let match_arm = rustc_pattern_analysis::MatchArm {
            pat: pattern_arena.alloc(deconstructed_pat),
            has_guard: false,
            arm_data: (),
        };
        let report = match cx.compute_match_usefulness(&[match_arm], ty, None) {
            Ok(v) => v,
            Err(e) => {
                debug!(?e, "match usefulness error");
                return None;
            }
        };
        let witnesses = report.non_exhaustiveness_witnesses;
        if witnesses.is_empty() {
            return None;
        }
        Some(BodyValidationDiagnostic::NonExhaustiveLet {
            pat,
            uncovered_patterns: missing_match_arms(
                cx,
                ty,
                witnesses,
                false,
                self.owner.krate(self.db()),
            ),
        })
    }

    fn lower_pattern<'a>(
        &self,
        cx: &MatchCheckCtx<'a, 'db>,
        pat: PatId,
        have_errors: &mut bool,
    ) -> DeconstructedPat<'a, 'db> {
        let mut patcx = match_check::PatCtxt::new(self.db(), self.infer, self.body);
        let pattern = patcx.lower_pattern(pat);
        let pattern = cx.lower_pat(&pattern);
        if !patcx.errors.is_empty() {
            *have_errors = true;
        }
        pattern
    }

    fn check_unused_must_use(&self, expr: ExprId) -> Option<BodyValidationDiagnostic<'db>> {
        let fn_def = match &self.body[expr] {
            Expr::Call { callee, .. } => {
                let callee_ty = self.infer.expr_ty(*callee);
                if let TyKind::FnDef(CallableIdWrapper(CallableDefId::FunctionId(func)), _) =
                    callee_ty.kind()
                {
                    Some(func.into())
                } else {
                    None
                }
            }
            Expr::MethodCall { .. } => {
                self.infer.method_resolution(expr).map(|(func, _)| func.into())
            }
            _ => return None,
        };
        let ty_def = self.infer.type_of_expr_with_adjust(expr).and_then(|ty| match ty.kind() {
            TyKind::Adt(adt, _) => Some(adt.def_id().into()),
            _ => None,
        });
        let must_use_diag = |owner| {
            AttrFlags::must_use_message(self.db(), owner?)
                .map(|message| BodyValidationDiagnostic::UnusedMustUse { expr, message })
        };
        must_use_diag(fn_def).or_else(|| must_use_diag(ty_def))
    }

    fn check_for_trailing_return(&mut self, body_expr: ExprId, body: &Body) {
        if !self.validate_lints {
            return;
        }
        match &body[body_expr] {
            Expr::Block { statements, tail, .. } => {
                let last_stmt = tail.or_else(|| match statements.last()? {
                    Statement::Expr { expr, .. } => Some(*expr),
                    _ => None,
                });
                if let Some(last_stmt) = last_stmt {
                    self.check_for_trailing_return(last_stmt, body);
                }
            }
            Expr::If { then_branch, else_branch, .. } => {
                self.check_for_trailing_return(*then_branch, body);
                if let Some(else_branch) = else_branch {
                    self.check_for_trailing_return(*else_branch, body);
                }
            }
            Expr::Match { arms, .. } => {
                for arm in arms.iter() {
                    let MatchArm { expr, .. } = arm;
                    self.check_for_trailing_return(*expr, body);
                }
            }
            Expr::Return { .. } => {
                self.diagnostics.push(BodyValidationDiagnostic::RemoveTrailingReturn {
                    return_expr: body_expr,
                });
            }
            _ => (),
        }
    }

    fn check_for_unnecessary_else(&mut self, id: ExprId, expr: &Expr) {
        if !self.validate_lints {
            return;
        }
        if let Expr::If { condition: _, then_branch, else_branch } = expr {
            if else_branch.is_none() {
                return;
            }
            if let Expr::Block { statements, tail, .. } = &self.body[*then_branch] {
                let last_then_expr = tail.or_else(|| match statements.last()? {
                    Statement::Expr { expr, .. } => Some(*expr),
                    _ => None,
                });
                if let Some(last_then_expr) = last_then_expr
                    && let Some(last_then_expr_ty) =
                        self.infer.type_of_expr_with_adjust(last_then_expr)
                    && last_then_expr_ty.is_never()
                {
                    // Only look at sources if the then branch diverges and we have an else branch.
                    let source_map = &Body::with_source_map(self.db(), self.owner).1;
                    let Ok(source_ptr) = source_map.expr_syntax(id) else {
                        return;
                    };
                    let root = source_ptr.file_syntax(self.db());
                    let either::Left(ast::Expr::IfExpr(if_expr)) = source_ptr.value.to_node(&root)
                    else {
                        return;
                    };
                    let mut top_if_expr = if_expr;
                    loop {
                        let parent = top_if_expr.syntax().parent();
                        let has_parent_expr_stmt_or_stmt_list =
                            parent.as_ref().is_some_and(|node| {
                                ast::ExprStmt::can_cast(node.kind())
                                    | ast::StmtList::can_cast(node.kind())
                            });
                        if has_parent_expr_stmt_or_stmt_list {
                            // Only emit diagnostic if parent or direct ancestor is either
                            // an expr stmt or a stmt list.
                            break;
                        }
                        let Some(parent_if_expr) = parent.and_then(ast::IfExpr::cast) else {
                            // Bail if parent is neither an if expr, an expr stmt nor a stmt list.
                            return;
                        };
                        // Check parent if expr.
                        top_if_expr = parent_if_expr;
                    }

                    self.diagnostics
                        .push(BodyValidationDiagnostic::RemoveUnnecessaryElse { if_expr: id })
                }
            }
        }
    }
}

struct FilterMapNextChecker<'db> {
    filter_map_function_id: Option<hir_def::FunctionId>,
    next_function_id: Option<hir_def::FunctionId>,
    prev_filter_map_expr_id: Option<ExprId>,
    prev_receiver_ty: Option<Ty<'db>>,
}

impl<'db> FilterMapNextChecker<'db> {
    fn new(lang_items: &'db LangItems, db: &'db dyn HirDatabase) -> Self {
        // Find and store the FunctionIds for Iterator::filter_map and Iterator::next
        let (next_function_id, filter_map_function_id) = match lang_items.IteratorNext {
            Some(next_function_id) => (
                Some(next_function_id),
                match next_function_id.lookup(db).container {
                    ItemContainerId::TraitId(iterator_trait_id) => {
                        let iterator_trait_items = &iterator_trait_id.trait_items(db).items;
                        iterator_trait_items.iter().find_map(|(name, it)| match it {
                            &AssocItemId::FunctionId(id) if *name == sym::filter_map => Some(id),
                            _ => None,
                        })
                    }
                    _ => None,
                },
            ),
            None => (None, None),
        };
        Self {
            filter_map_function_id,
            next_function_id,
            prev_filter_map_expr_id: None,
            prev_receiver_ty: None,
        }
    }

    // check for instances of .filter_map(..).next()
    fn check(
        &mut self,
        current_expr_id: ExprId,
        receiver_expr_id: &ExprId,
        function_id: &hir_def::FunctionId,
    ) -> Option<()> {
        if *function_id == self.filter_map_function_id? {
            self.prev_filter_map_expr_id = Some(current_expr_id);
            return None;
        }

        if *function_id == self.next_function_id?
            && let Some(prev_filter_map_expr_id) = self.prev_filter_map_expr_id
        {
            let is_dyn_trait = self
                .prev_receiver_ty
                .as_ref()
                .is_some_and(|it| it.strip_references().dyn_trait().is_some());
            if *receiver_expr_id == prev_filter_map_expr_id && !is_dyn_trait {
                return Some(());
            }
        }

        self.prev_filter_map_expr_id = None;
        None
    }
}

pub fn record_literal_missing_fields(
    db: &dyn HirDatabase,
    infer: &InferenceResult,
    id: ExprId,
    expr: &Expr,
) -> Option<(VariantId, Vec<LocalFieldId>)> {
    let (fields, spread) = match expr {
        Expr::RecordLit { fields, spread, .. } => (fields, spread),
        _ => return None,
    };

    let variant_def = infer.variant_resolution_for_expr(id)?;
    if let VariantId::UnionId(_) = variant_def {
        return None;
    }

    let variant_data = variant_def.fields(db);

    let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
    // don't show missing fields if:
    // - has ..expr
    // - or has default value + ..
    // - or already in code
    let missed_fields: Vec<LocalFieldId> = variant_data
        .fields()
        .iter()
        .filter_map(|(f, d)| {
            if specified_fields.contains(&d.name)
                || matches!(spread, RecordSpread::Expr(_))
                || (d.default_value.is_some() && matches!(spread, RecordSpread::FieldDefaults))
            {
                None
            } else {
                Some(f)
            }
        })
        .collect();
    if missed_fields.is_empty() {
        return None;
    }
    Some((variant_def, missed_fields))
}

pub fn record_pattern_missing_fields(
    db: &dyn HirDatabase,
    infer: &InferenceResult,
    id: PatId,
    pat: &Pat,
) -> Option<(VariantId, Vec<LocalFieldId>)> {
    let (fields, ellipsis) = match pat {
        Pat::Record { path: _, args, ellipsis } => (args, *ellipsis),
        _ => return None,
    };

    let variant_def = infer.variant_resolution_for_pat(id)?;
    if let VariantId::UnionId(_) = variant_def {
        return None;
    }

    let variant_data = variant_def.fields(db);

    let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
    // don't show missing fields if:
    // - in code
    // - or has ..
    let missed_fields: Vec<LocalFieldId> = variant_data
        .fields()
        .iter()
        .filter_map(
            |(f, d)| {
                if specified_fields.contains(&d.name) || ellipsis { None } else { Some(f) }
            },
        )
        .collect();
    if missed_fields.is_empty() {
        return None;
    }
    Some((variant_def, missed_fields))
}

fn types_of_subpatterns_do_match(pat: PatId, body: &Body, infer: &InferenceResult) -> bool {
    fn walk(pat: PatId, body: &Body, infer: &InferenceResult, has_type_mismatches: &mut bool) {
        match infer.pat_has_type_mismatch(pat) {
            true => *has_type_mismatches = true,
            false if *has_type_mismatches => (),
            false => {
                let pat = &body[pat];
                if let Pat::ConstBlock(expr) | Pat::Lit(expr) = *pat {
                    *has_type_mismatches |= infer.expr_has_type_mismatch(expr);
                    if *has_type_mismatches {
                        return;
                    }
                }
                pat.walk_child_pats(|subpat| walk(subpat, body, infer, has_type_mismatches))
            }
        }
    }

    let mut has_type_mismatches = false;
    walk(pat, body, infer, &mut has_type_mismatches);
    !has_type_mismatches
}

fn missing_match_arms<'a, 'db>(
    cx: &MatchCheckCtx<'a, 'db>,
    scrut_ty: Ty<'a>,
    witnesses: Vec<WitnessPat<'a, 'db>>,
    arms_is_empty: bool,
    krate: Crate,
) -> String {
    struct DisplayWitness<'a, 'b, 'db>(
        &'a WitnessPat<'b, 'db>,
        &'a MatchCheckCtx<'b, 'db>,
        DisplayTarget,
    );
    impl fmt::Display for DisplayWitness<'_, '_, '_> {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            let DisplayWitness(witness, cx, display_target) = *self;
            let pat = cx.hoist_witness_pat(witness);
            write!(f, "{}", pat.display(cx.db, display_target))
        }
    }

    let non_empty_enum = match scrut_ty.as_adt() {
        Some((AdtId::EnumId(e), _)) => !e.enum_variants(cx.db).variants.is_empty(),
        _ => false,
    };
    let display_target = DisplayTarget::from_crate(cx.db, krate);
    if arms_is_empty && !non_empty_enum {
        format!("type `{}` is non-empty", scrut_ty.display(cx.db, display_target))
    } else {
        let pat_display = |witness| DisplayWitness(witness, cx, display_target);
        const LIMIT: usize = 3;
        match &*witnesses {
            [witness] => format!("`{}` not covered", pat_display(witness)),
            [head @ .., tail] if head.len() < LIMIT => {
                let head = head.iter().map(pat_display);
                format!("`{}` and `{}` not covered", head.format("`, `"), pat_display(tail))
            }
            _ => {
                let (head, tail) = witnesses.split_at(LIMIT);
                let head = head.iter().map(pat_display);
                format!("`{}` and {} more not covered", head.format("`, `"), tail.len())
            }
        }
    }
}
