1use std::fmt;
6
7use base_db::Crate;
8use either::Either;
9use hir_def::{
10 AdtId, AssocItemId, CallableDefId, DefWithBodyId, HasModule, ItemContainerId, Lookup,
11 attrs::AttrFlags,
12 lang_item::LangItems,
13 resolver::{HasResolver, ValueNs},
14};
15use intern::sym;
16use itertools::Itertools;
17use rustc_hash::FxHashSet;
18use rustc_pattern_analysis::constructor::Constructor;
19use rustc_type_ir::inherent::IntoKind;
20use syntax::{
21 AstNode,
22 ast::{self, UnaryOp},
23};
24use tracing::debug;
25
26use typed_arena::Arena;
27
28use crate::{
29 Adjust, InferenceResult,
30 db::HirDatabase,
31 diagnostics::match_check::{
32 self,
33 pat_analysis::{self, DeconstructedPat, MatchCheckCtx, WitnessPat},
34 },
35 display::{DisplayTarget, HirDisplay},
36 next_solver::{
37 CallableIdWrapper, DbInterner, ParamEnv, Ty, TyKind, TypingMode,
38 infer::{DbInternerInferExt, InferCtxt},
39 },
40};
41
42pub(crate) use hir_def::{
43 LocalFieldId, VariantId,
44 expr_store::Body,
45 hir::{Expr, ExprId, MatchArm, Pat, PatId, RecordSpread, Statement},
46};
47
48pub enum BodyValidationDiagnostic<'db> {
49 RecordMissingFields {
50 record: Either<ExprId, PatId>,
51 variant: VariantId,
52 missed_fields: Vec<LocalFieldId>,
53 },
54 ReplaceFilterMapNextWithFindMap {
55 method_call_expr: ExprId,
56 },
57 MissingMatchArms {
58 match_expr: ExprId,
59 uncovered_patterns: String,
60 },
61 NonExhaustiveLet {
62 pat: PatId,
63 uncovered_patterns: String,
64 },
65 RemoveTrailingReturn {
66 return_expr: ExprId,
67 },
68 RemoveUnnecessaryElse {
69 if_expr: ExprId,
70 },
71 UnusedMustUse {
72 expr: ExprId,
73 message: Option<&'db str>,
74 },
75}
76
77impl<'db> BodyValidationDiagnostic<'db> {
78 pub fn collect(
79 db: &'db dyn HirDatabase,
80 owner: DefWithBodyId,
81 validate_lints: bool,
82 ) -> Vec<BodyValidationDiagnostic<'db>> {
83 let _p = tracing::info_span!("BodyValidationDiagnostic::collect").entered();
84 let infer = InferenceResult::of(db, owner);
85 let body = Body::of(db, owner);
86 let env = db.trait_environment(owner.generic_def(db));
87 let interner = DbInterner::new_with(db, owner.krate(db));
88 let infcx =
89 interner.infer_ctxt().build(TypingMode::typeck_for_body(interner, owner.into()));
90 let mut validator = ExprValidator {
91 owner,
92 body,
93 infer,
94 diagnostics: Vec::new(),
95 validate_lints,
96 env,
97 infcx,
98 };
99 validator.validate_body();
100 validator.diagnostics
101 }
102}
103
104struct ExprValidator<'db> {
105 owner: DefWithBodyId,
106 body: &'db Body,
107 infer: &'db InferenceResult<'db>,
108 env: ParamEnv<'db>,
109 diagnostics: Vec<BodyValidationDiagnostic<'db>>,
110 validate_lints: bool,
111 infcx: InferCtxt<'db>,
112}
113
114impl<'db> ExprValidator<'db> {
115 #[inline]
116 fn db(&self) -> &'db dyn HirDatabase {
117 self.infcx.interner.db
118 }
119
120 fn validate_body(&mut self) {
121 let db = self.db();
122 let mut filter_map_next_checker = None;
123 let body = self.body;
125
126 if matches!(self.owner, DefWithBodyId::FunctionId(_)) {
127 self.check_for_trailing_return(body.root_expr(), body);
128 }
129
130 for (id, expr) in body.exprs() {
131 if let Some((variant, missed_fields)) =
132 record_literal_missing_fields(db, self.infer, id, expr)
133 {
134 self.diagnostics.push(BodyValidationDiagnostic::RecordMissingFields {
135 record: Either::Left(id),
136 variant,
137 missed_fields,
138 });
139 }
140
141 match expr {
142 Expr::Match { expr, arms } => {
143 self.validate_match(id, *expr, arms);
144 }
145 Expr::Call { .. } | Expr::MethodCall { .. } => {
146 self.validate_call(id, expr, &mut filter_map_next_checker);
147 }
148 Expr::Closure { body: body_expr, .. } => {
149 self.check_for_trailing_return(*body_expr, body);
150 }
151 Expr::If { .. } => {
152 self.check_for_unnecessary_else(id, expr);
153 }
154 Expr::Block { .. } | Expr::Unsafe { .. } => {
155 self.validate_block(expr);
156 }
157 _ => {}
158 }
159 }
160 }
161
162 fn validate_call(
163 &mut self,
164 call_id: ExprId,
165 expr: &Expr,
166 filter_map_next_checker: &mut Option<FilterMapNextChecker<'db>>,
167 ) {
168 if !self.validate_lints {
169 return;
170 }
171 if self.infer.exprs_have_type_mismatches() {
174 } else if let Expr::MethodCall { receiver, .. } = expr {
177 let (callee, _) = match self.infer.method_resolution(call_id) {
178 Some(it) => it,
179 None => return,
180 };
181
182 let checker = filter_map_next_checker.get_or_insert_with(|| {
183 FilterMapNextChecker::new(self.infcx.interner.lang_items(), self.db())
184 });
185
186 if checker.check(call_id, receiver, &callee).is_some() {
187 self.diagnostics.push(BodyValidationDiagnostic::ReplaceFilterMapNextWithFindMap {
188 method_call_expr: call_id,
189 });
190 }
191
192 if let Some(receiver_ty) = self.infer.type_of_expr_with_adjust(*receiver) {
193 checker.prev_receiver_ty = Some(receiver_ty);
194 }
195 }
196 }
197
198 fn validate_match(&mut self, match_expr: ExprId, scrutinee_expr: ExprId, arms: &[MatchArm]) {
199 let Some(scrut_ty) = self.infer.type_of_expr_with_adjust(scrutinee_expr) else {
200 return;
201 };
202 if scrut_ty.references_non_lt_error() {
203 return;
204 }
205
206 let cx = MatchCheckCtx::new(self.owner.module(self.db()), &self.infcx, self.env);
207
208 let pattern_arena = Arena::new();
209 let mut m_arms = Vec::with_capacity(arms.len());
210 let mut has_lowering_errors = false;
211 for arm in arms {
214 let pat_ty = self.infer.type_of_pat_with_adjust(arm.pat);
215 if pat_ty.references_non_lt_error() {
216 return;
217 }
218
219 if (pat_ty == scrut_ty
230 || scrut_ty
231 .as_reference()
232 .is_none_or(|(match_expr_ty, ..)| match_expr_ty == pat_ty))
233 && types_of_subpatterns_do_match(arm.pat, self.body, self.infer)
234 {
235 let pat = self.lower_pattern(&cx, arm.pat, &mut has_lowering_errors);
239 let m_arm = pat_analysis::MatchArm {
240 pat: pattern_arena.alloc(pat),
241 has_guard: arm.guard.is_some(),
242 arm_data: (),
243 };
244 m_arms.push(m_arm);
245 if !has_lowering_errors {
246 continue;
247 }
248 }
249 cov_mark::hit!(validate_match_bailed_out);
251 return;
252 }
253
254 let known_valid_scrutinee = Some(self.is_known_valid_scrutinee(scrutinee_expr));
255 let report =
256 match cx.compute_match_usefulness(m_arms.as_slice(), scrut_ty, known_valid_scrutinee) {
257 Ok(report) => report,
258 Err(()) => return,
259 };
260
261 let witnesses = report.non_exhaustiveness_witnesses;
265 if !witnesses.is_empty() {
266 self.diagnostics.push(BodyValidationDiagnostic::MissingMatchArms {
267 match_expr,
268 uncovered_patterns: missing_match_arms(
269 &cx,
270 scrut_ty,
271 witnesses,
272 m_arms.is_empty(),
273 self.owner.krate(self.db()),
274 ),
275 });
276 }
277 }
278
279 fn is_known_valid_scrutinee(&self, scrutinee_expr: ExprId) -> bool {
285 let db = self.db();
286
287 if self
288 .infer
289 .expr_adjustments
290 .get(&scrutinee_expr)
291 .is_some_and(|adjusts| adjusts.iter().any(|a| matches!(a.kind, Adjust::Deref(..))))
292 {
293 return false;
294 }
295
296 match &self.body[scrutinee_expr] {
297 Expr::UnaryOp { op: UnaryOp::Deref, .. } => false,
298 Expr::Path(path) => {
299 let value_or_partial = self.owner.resolver(db).resolve_path_in_value_ns_fully(
300 db,
301 path,
302 self.body.expr_path_hygiene(scrutinee_expr),
303 );
304 value_or_partial.is_none_or(|v| !matches!(v, ValueNs::StaticId(_)))
305 }
306 Expr::Field { expr, .. } => match self.infer.expr_ty(*expr).kind() {
307 TyKind::Adt(adt, ..) if matches!(adt.def_id(), AdtId::UnionId(_)) => false,
308 _ => self.is_known_valid_scrutinee(*expr),
309 },
310 Expr::Index { base, .. } => self.is_known_valid_scrutinee(*base),
311 Expr::Cast { expr, .. } => self.is_known_valid_scrutinee(*expr),
312 Expr::Missing => false,
313 _ => true,
314 }
315 }
316
317 fn validate_block(&mut self, expr: &Expr) {
318 let (Expr::Block { statements, .. } | Expr::Unsafe { statements, .. }) = expr else {
319 return;
320 };
321 let pattern_arena = Arena::new();
322 let cx = MatchCheckCtx::new(self.owner.module(self.db()), &self.infcx, self.env);
323 for stmt in &**statements {
324 match *stmt {
325 Statement::Expr { expr: stmt_expr, has_semi: true } if self.validate_lints => {
326 let mut diags = Vec::new();
327 self.check_unused_must_use(stmt_expr, &mut diags);
328 self.diagnostics.extend(diags);
329 }
330 Statement::Let { pat, initializer, else_branch: None, .. } => {
331 if let Some(diag) =
332 self.check_non_exhaustive_let(&cx, &pattern_arena, pat, initializer)
333 {
334 self.diagnostics.push(diag);
335 }
336 }
337 _ => {}
338 }
339 }
340 }
341
342 fn check_non_exhaustive_let<'a>(
343 &self,
344 cx: &MatchCheckCtx<'a, 'db>,
345 pattern_arena: &'a Arena<DeconstructedPat<'a, 'db>>,
346 pat: PatId,
347 initializer: Option<ExprId>,
348 ) -> Option<BodyValidationDiagnostic<'db>> {
349 if self.infer.pat_has_type_mismatch(pat) {
350 return None;
351 }
352 let initializer = initializer?;
353 let ty = self.infer.type_of_expr_with_adjust(initializer)?;
354 if ty.references_non_lt_error() {
355 return None;
356 }
357
358 let mut have_errors = false;
359 let deconstructed_pat = self.lower_pattern(cx, pat, &mut have_errors);
360
361 if have_errors || matches!(deconstructed_pat.ctor(), Constructor::Wildcard) {
363 return None;
364 }
365
366 let match_arm = rustc_pattern_analysis::MatchArm {
367 pat: pattern_arena.alloc(deconstructed_pat),
368 has_guard: false,
369 arm_data: (),
370 };
371 let report = match cx.compute_match_usefulness(&[match_arm], ty, None) {
372 Ok(v) => v,
373 Err(e) => {
374 debug!(?e, "match usefulness error");
375 return None;
376 }
377 };
378 let witnesses = report.non_exhaustiveness_witnesses;
379 if witnesses.is_empty() {
380 return None;
381 }
382 Some(BodyValidationDiagnostic::NonExhaustiveLet {
383 pat,
384 uncovered_patterns: missing_match_arms(
385 cx,
386 ty,
387 witnesses,
388 false,
389 self.owner.krate(self.db()),
390 ),
391 })
392 }
393
394 fn lower_pattern<'a>(
395 &self,
396 cx: &MatchCheckCtx<'a, 'db>,
397 pat: PatId,
398 have_errors: &mut bool,
399 ) -> DeconstructedPat<'a, 'db> {
400 let mut patcx = match_check::PatCtxt::new(self.db(), self.infer, self.body);
401 let pattern = patcx.lower_pattern(pat);
402 let pattern = cx.lower_pat(&pattern);
403 if !patcx.errors.is_empty() {
404 *have_errors = true;
405 }
406 pattern
407 }
408
409 fn check_unused_must_use(
410 &self,
411 mut expr: ExprId,
412 acc: &mut Vec<BodyValidationDiagnostic<'db>>,
413 ) {
414 loop {
419 match &self.body[expr] {
420 Expr::Block { tail: Some(tail), .. }
421 | Expr::Unsafe { tail: Some(tail), .. }
422 | Expr::Const(tail) => expr = *tail,
423 Expr::If { then_branch, else_branch, .. } => {
424 self.check_unused_must_use(*then_branch, acc);
425 if let Some(else_branch) = else_branch {
426 self.check_unused_must_use(*else_branch, acc);
427 }
428 return;
429 }
430 Expr::Match { arms, .. } => {
431 for arm in arms.iter() {
432 self.check_unused_must_use(arm.expr, acc);
433 }
434 return;
435 }
436 _ => break,
437 }
438 }
439
440 let fn_def = match &self.body[expr] {
441 Expr::Call { callee, .. } => {
442 let callee_ty = self.infer.expr_ty(*callee);
443 if let TyKind::FnDef(CallableIdWrapper(CallableDefId::FunctionId(func)), _) =
444 callee_ty.kind()
445 {
446 Some(func.into())
447 } else {
448 None
449 }
450 }
451 Expr::MethodCall { .. } => {
452 self.infer.method_resolution(expr).map(|(func, _)| func.into())
453 }
454 _ => None,
455 };
456 let ty_def = self.infer.type_of_expr_with_adjust(expr).and_then(|ty| match ty.kind() {
457 TyKind::Adt(adt, _) => Some(adt.def_id().into()),
458 _ => None,
459 });
460 let must_use_diag = |owner| {
461 AttrFlags::must_use_message(self.db(), owner?)
462 .map(|message| BodyValidationDiagnostic::UnusedMustUse { expr, message })
463 };
464 if let Some(diag) = must_use_diag(fn_def).or_else(|| must_use_diag(ty_def)) {
465 acc.push(diag);
466 }
467 }
468
469 fn check_for_trailing_return(&mut self, body_expr: ExprId, body: &Body) {
470 if !self.validate_lints {
471 return;
472 }
473 match &body[body_expr] {
474 Expr::Block { statements, tail, .. } => {
475 let last_stmt = tail.or_else(|| match statements.last()? {
476 Statement::Expr { expr, .. } => Some(*expr),
477 _ => None,
478 });
479 if let Some(last_stmt) = last_stmt {
480 self.check_for_trailing_return(last_stmt, body);
481 }
482 }
483 Expr::If { then_branch, else_branch, .. } => {
484 self.check_for_trailing_return(*then_branch, body);
485 if let Some(else_branch) = else_branch {
486 self.check_for_trailing_return(*else_branch, body);
487 }
488 }
489 Expr::Match { arms, .. } => {
490 for arm in arms.iter() {
491 let MatchArm { expr, .. } = arm;
492 self.check_for_trailing_return(*expr, body);
493 }
494 }
495 Expr::Return { .. } => {
496 self.diagnostics.push(BodyValidationDiagnostic::RemoveTrailingReturn {
497 return_expr: body_expr,
498 });
499 }
500 _ => (),
501 }
502 }
503
504 fn check_for_unnecessary_else(&mut self, id: ExprId, expr: &Expr) {
505 if !self.validate_lints {
506 return;
507 }
508 if let Expr::If { condition: _, then_branch, else_branch } = expr {
509 if else_branch.is_none() {
510 return;
511 }
512 if let Expr::Block { statements, tail, .. } = &self.body[*then_branch] {
513 let last_then_expr = tail.or_else(|| match statements.last()? {
514 Statement::Expr { expr, .. } => Some(*expr),
515 _ => None,
516 });
517 if let Some(last_then_expr) = last_then_expr
518 && let Some(last_then_expr_ty) =
519 self.infer.type_of_expr_with_adjust(last_then_expr)
520 && last_then_expr_ty.is_never()
521 {
522 let source_map = &Body::with_source_map(self.db(), self.owner).1;
524 let Ok(source_ptr) = source_map.expr_syntax(id) else {
525 return;
526 };
527 let root = source_ptr.file_syntax(self.db());
528 let either::Left(ast::Expr::IfExpr(if_expr)) = source_ptr.value.to_node(&root)
529 else {
530 return;
531 };
532 let mut top_if_expr = if_expr;
533 loop {
534 let parent = top_if_expr.syntax().parent();
535 let has_parent_expr_stmt_or_stmt_list =
536 parent.as_ref().is_some_and(|node| {
537 ast::ExprStmt::can_cast(node.kind())
538 | ast::StmtList::can_cast(node.kind())
539 });
540 if has_parent_expr_stmt_or_stmt_list {
541 break;
544 }
545 let Some(parent_if_expr) = parent.and_then(ast::IfExpr::cast) else {
546 return;
548 };
549 top_if_expr = parent_if_expr;
551 }
552
553 self.diagnostics
554 .push(BodyValidationDiagnostic::RemoveUnnecessaryElse { if_expr: id })
555 }
556 }
557 }
558 }
559}
560
561struct FilterMapNextChecker<'db> {
562 filter_map_function_id: Option<hir_def::FunctionId>,
563 next_function_id: Option<hir_def::FunctionId>,
564 prev_filter_map_expr_id: Option<ExprId>,
565 prev_receiver_ty: Option<Ty<'db>>,
566}
567
568impl<'db> FilterMapNextChecker<'db> {
569 fn new(lang_items: &'db LangItems, db: &'db dyn HirDatabase) -> Self {
570 let (next_function_id, filter_map_function_id) = match lang_items.IteratorNext {
572 Some(next_function_id) => (
573 Some(next_function_id),
574 match next_function_id.lookup(db).container {
575 ItemContainerId::TraitId(iterator_trait_id) => {
576 let iterator_trait_items = &iterator_trait_id.trait_items(db).items;
577 iterator_trait_items.iter().find_map(|(name, it)| match it {
578 &AssocItemId::FunctionId(id) if *name == sym::filter_map => Some(id),
579 _ => None,
580 })
581 }
582 _ => None,
583 },
584 ),
585 None => (None, None),
586 };
587 Self {
588 filter_map_function_id,
589 next_function_id,
590 prev_filter_map_expr_id: None,
591 prev_receiver_ty: None,
592 }
593 }
594
595 fn check(
597 &mut self,
598 current_expr_id: ExprId,
599 receiver_expr_id: &ExprId,
600 function_id: &hir_def::FunctionId,
601 ) -> Option<()> {
602 if *function_id == self.filter_map_function_id? {
603 self.prev_filter_map_expr_id = Some(current_expr_id);
604 return None;
605 }
606
607 if *function_id == self.next_function_id?
608 && let Some(prev_filter_map_expr_id) = self.prev_filter_map_expr_id
609 {
610 let is_dyn_trait = self
611 .prev_receiver_ty
612 .as_ref()
613 .is_some_and(|it| it.strip_references().dyn_trait().is_some());
614 if *receiver_expr_id == prev_filter_map_expr_id && !is_dyn_trait {
615 return Some(());
616 }
617 }
618
619 self.prev_filter_map_expr_id = None;
620 None
621 }
622}
623
624pub fn record_literal_missing_fields<'db>(
625 db: &'db dyn HirDatabase,
626 infer: &InferenceResult<'db>,
627 id: ExprId,
628 expr: &Expr,
629) -> Option<(VariantId, Vec<LocalFieldId>)> {
630 let (fields, spread) = match expr {
631 Expr::RecordLit { fields, spread, .. } => (fields, spread),
632 _ => return None,
633 };
634
635 let variant_def = infer.variant_resolution_for_expr(id)?;
636 if let VariantId::UnionId(_) = variant_def {
637 return None;
638 }
639
640 let variant_data = variant_def.fields(db);
641
642 let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
643 let missed_fields: Vec<LocalFieldId> = variant_data
648 .fields()
649 .iter()
650 .filter_map(|(f, d)| {
651 if specified_fields.contains(&d.name)
652 || matches!(spread, RecordSpread::Expr(_))
653 || (d.default_value.is_some() && matches!(spread, RecordSpread::FieldDefaults))
654 {
655 None
656 } else {
657 Some(f)
658 }
659 })
660 .collect();
661 if missed_fields.is_empty() {
662 return None;
663 }
664 Some((variant_def, missed_fields))
665}
666
667pub fn record_pattern_missing_fields<'db>(
668 db: &'db dyn HirDatabase,
669 infer: &InferenceResult<'db>,
670 id: PatId,
671 pat: &Pat,
672) -> Option<(VariantId, Vec<LocalFieldId>)> {
673 let (fields, ellipsis) = match pat {
674 Pat::Record { path: _, args, ellipsis } => (args, *ellipsis),
675 _ => return None,
676 };
677
678 let variant_def = infer.variant_resolution_for_pat(id)?;
679 if let VariantId::UnionId(_) = variant_def {
680 return None;
681 }
682
683 let variant_data = variant_def.fields(db);
684
685 let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
686 let missed_fields: Vec<LocalFieldId> = variant_data
690 .fields()
691 .iter()
692 .filter_map(
693 |(f, d)| {
694 if specified_fields.contains(&d.name) || ellipsis { None } else { Some(f) }
695 },
696 )
697 .collect();
698 if missed_fields.is_empty() {
699 return None;
700 }
701 Some((variant_def, missed_fields))
702}
703
704fn types_of_subpatterns_do_match(pat: PatId, body: &Body, infer: &InferenceResult<'_>) -> bool {
705 fn walk(pat: PatId, body: &Body, infer: &InferenceResult<'_>, has_type_mismatches: &mut bool) {
706 match infer.pat_has_type_mismatch(pat) {
707 true => *has_type_mismatches = true,
708 false if *has_type_mismatches => (),
709 false => {
710 let pat = &body[pat];
711 if let Pat::ConstBlock(expr) | Pat::Lit(expr) = *pat {
712 *has_type_mismatches |= infer.expr_has_type_mismatch(expr);
713 if *has_type_mismatches {
714 return;
715 }
716 }
717 pat.walk_child_pats(|subpat| walk(subpat, body, infer, has_type_mismatches))
718 }
719 }
720 }
721
722 let mut has_type_mismatches = false;
723 walk(pat, body, infer, &mut has_type_mismatches);
724 !has_type_mismatches
725}
726
727fn missing_match_arms<'a, 'db>(
728 cx: &MatchCheckCtx<'a, 'db>,
729 scrut_ty: Ty<'a>,
730 witnesses: Vec<WitnessPat<'a, 'db>>,
731 arms_is_empty: bool,
732 krate: Crate,
733) -> String {
734 struct DisplayWitness<'a, 'b, 'db>(
735 &'a WitnessPat<'b, 'db>,
736 &'a MatchCheckCtx<'b, 'db>,
737 DisplayTarget,
738 );
739 impl fmt::Display for DisplayWitness<'_, '_, '_> {
740 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
741 let DisplayWitness(witness, cx, display_target) = *self;
742 let pat = cx.hoist_witness_pat(witness);
743 write!(f, "{}", pat.display(cx.db, display_target))
744 }
745 }
746
747 let non_empty_enum = match scrut_ty.as_adt() {
748 Some((AdtId::EnumId(e), _)) => !e.enum_variants(cx.db).variants.is_empty(),
749 _ => false,
750 };
751 let display_target = DisplayTarget::from_crate(cx.db, krate);
752 if arms_is_empty && !non_empty_enum {
753 format!("type `{}` is non-empty", scrut_ty.display(cx.db, display_target))
754 } else {
755 let pat_display = |witness| DisplayWitness(witness, cx, display_target);
756 const LIMIT: usize = 3;
757 match &*witnesses {
758 [witness] => format!("`{}` not covered", pat_display(witness)),
759 [head @ .., tail] if head.len() < LIMIT => {
760 let head = head.iter().map(pat_display);
761 format!("`{}` and `{}` not covered", head.format("`, `"), pat_display(tail))
762 }
763 _ => {
764 let (head, tail) = witnesses.split_at(LIMIT);
765 let head = head.iter().map(pat_display);
766 format!("`{}` and {} more not covered", head.format("`, `"), tail.len())
767 }
768 }
769 }
770}