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 { statements, .. } => {
155 self.validate_block(statements);
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, statements: &[Statement]) {
318 let pattern_arena = Arena::new();
319 let cx = MatchCheckCtx::new(self.owner.module(self.db()), &self.infcx, self.env);
320 for stmt in statements {
321 match *stmt {
322 Statement::Expr { expr: stmt_expr, has_semi: true } if self.validate_lints => {
323 let mut diags = Vec::new();
324 self.check_unused_must_use(stmt_expr, &mut diags);
325 self.diagnostics.extend(diags);
326 }
327 Statement::Let { pat, initializer, else_branch: None, .. } => {
328 if let Some(diag) =
329 self.check_non_exhaustive_let(&cx, &pattern_arena, pat, initializer)
330 {
331 self.diagnostics.push(diag);
332 }
333 }
334 _ => {}
335 }
336 }
337 }
338
339 fn check_non_exhaustive_let<'a>(
340 &self,
341 cx: &MatchCheckCtx<'a, 'db>,
342 pattern_arena: &'a Arena<DeconstructedPat<'a, 'db>>,
343 pat: PatId,
344 initializer: Option<ExprId>,
345 ) -> Option<BodyValidationDiagnostic<'db>> {
346 if !types_of_subpatterns_do_match(pat, self.body, self.infer) {
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), .. } | Expr::Const(tail) => expr = *tail,
421 Expr::If { then_branch, else_branch, .. } => {
422 self.check_unused_must_use(*then_branch, acc);
423 if let Some(else_branch) = else_branch {
424 self.check_unused_must_use(*else_branch, acc);
425 }
426 return;
427 }
428 Expr::Match { arms, .. } => {
429 for arm in arms.iter() {
430 self.check_unused_must_use(arm.expr, acc);
431 }
432 return;
433 }
434 _ => break,
435 }
436 }
437
438 let fn_def = match &self.body[expr] {
439 Expr::Call { callee, .. } => {
440 let callee_ty = self.infer.expr_ty(*callee);
441 if let TyKind::FnDef(CallableIdWrapper(CallableDefId::FunctionId(func)), _) =
442 callee_ty.kind()
443 {
444 Some(func.into())
445 } else {
446 None
447 }
448 }
449 Expr::MethodCall { .. } => {
450 self.infer.method_resolution(expr).map(|(func, _)| func.into())
451 }
452 _ => None,
453 };
454 let ty_def = self.infer.type_of_expr_with_adjust(expr).and_then(|ty| match ty.kind() {
455 TyKind::Adt(adt, _) => Some(adt.def_id().into()),
456 _ => None,
457 });
458 let must_use_diag = |owner| {
459 AttrFlags::must_use_message(self.db(), owner?)
460 .map(|message| BodyValidationDiagnostic::UnusedMustUse { expr, message })
461 };
462 if let Some(diag) = must_use_diag(fn_def).or_else(|| must_use_diag(ty_def)) {
463 acc.push(diag);
464 }
465 }
466
467 fn check_for_trailing_return(&mut self, body_expr: ExprId, body: &Body) {
468 if !self.validate_lints {
469 return;
470 }
471 match &body[body_expr] {
472 Expr::Block { statements, tail, .. } => {
473 let last_stmt = tail.or_else(|| match statements.last()? {
474 Statement::Expr { expr, .. } => Some(*expr),
475 _ => None,
476 });
477 if let Some(last_stmt) = last_stmt {
478 self.check_for_trailing_return(last_stmt, body);
479 }
480 }
481 Expr::If { then_branch, else_branch, .. } => {
482 self.check_for_trailing_return(*then_branch, body);
483 if let Some(else_branch) = else_branch {
484 self.check_for_trailing_return(*else_branch, body);
485 }
486 }
487 Expr::Match { arms, .. } => {
488 for arm in arms.iter() {
489 let MatchArm { expr, .. } = arm;
490 self.check_for_trailing_return(*expr, body);
491 }
492 }
493 Expr::Return { .. } => {
494 self.diagnostics.push(BodyValidationDiagnostic::RemoveTrailingReturn {
495 return_expr: body_expr,
496 });
497 }
498 _ => (),
499 }
500 }
501
502 fn check_for_unnecessary_else(&mut self, id: ExprId, expr: &Expr) {
503 if !self.validate_lints {
504 return;
505 }
506 if let Expr::If { condition: _, then_branch, else_branch } = expr {
507 if else_branch.is_none() {
508 return;
509 }
510 if let Expr::Block { statements, tail, .. } = &self.body[*then_branch] {
511 let last_then_expr = tail.or_else(|| match statements.last()? {
512 Statement::Expr { expr, .. } => Some(*expr),
513 _ => None,
514 });
515 if let Some(last_then_expr) = last_then_expr
516 && let Some(last_then_expr_ty) =
517 self.infer.type_of_expr_with_adjust(last_then_expr)
518 && last_then_expr_ty.is_never()
519 {
520 let source_map = &Body::with_source_map(self.db(), self.owner).1;
522 let Ok(source_ptr) = source_map.expr_syntax(id) else {
523 return;
524 };
525 let root = source_ptr.file_syntax(self.db());
526 let either::Left(ast::Expr::IfExpr(if_expr)) = source_ptr.value.to_node(&root)
527 else {
528 return;
529 };
530 let mut top_if_expr = if_expr;
531 loop {
532 let parent = top_if_expr.syntax().parent();
533 let has_parent_expr_stmt_or_stmt_list =
534 parent.as_ref().is_some_and(|node| {
535 ast::ExprStmt::can_cast(node.kind())
536 | ast::StmtList::can_cast(node.kind())
537 });
538 if has_parent_expr_stmt_or_stmt_list {
539 break;
542 }
543 let Some(parent_if_expr) = parent.and_then(ast::IfExpr::cast) else {
544 return;
546 };
547 top_if_expr = parent_if_expr;
549 }
550
551 self.diagnostics
552 .push(BodyValidationDiagnostic::RemoveUnnecessaryElse { if_expr: id })
553 }
554 }
555 }
556 }
557}
558
559struct FilterMapNextChecker<'db> {
560 filter_map_function_id: Option<hir_def::FunctionId>,
561 next_function_id: Option<hir_def::FunctionId>,
562 prev_filter_map_expr_id: Option<ExprId>,
563 prev_receiver_ty: Option<Ty<'db>>,
564}
565
566impl<'db> FilterMapNextChecker<'db> {
567 fn new(lang_items: &'db LangItems, db: &'db dyn HirDatabase) -> Self {
568 let (next_function_id, filter_map_function_id) = match lang_items.IteratorNext {
570 Some(next_function_id) => (
571 Some(next_function_id),
572 match next_function_id.lookup(db).container {
573 ItemContainerId::TraitId(iterator_trait_id) => {
574 let iterator_trait_items = &iterator_trait_id.trait_items(db).items;
575 iterator_trait_items.iter().find_map(|(name, it)| match it {
576 &AssocItemId::FunctionId(id) if *name == sym::filter_map => Some(id),
577 _ => None,
578 })
579 }
580 _ => None,
581 },
582 ),
583 None => (None, None),
584 };
585 Self {
586 filter_map_function_id,
587 next_function_id,
588 prev_filter_map_expr_id: None,
589 prev_receiver_ty: None,
590 }
591 }
592
593 fn check(
595 &mut self,
596 current_expr_id: ExprId,
597 receiver_expr_id: &ExprId,
598 function_id: &hir_def::FunctionId,
599 ) -> Option<()> {
600 if *function_id == self.filter_map_function_id? {
601 self.prev_filter_map_expr_id = Some(current_expr_id);
602 return None;
603 }
604
605 if *function_id == self.next_function_id?
606 && let Some(prev_filter_map_expr_id) = self.prev_filter_map_expr_id
607 {
608 let is_dyn_trait = self
609 .prev_receiver_ty
610 .as_ref()
611 .is_some_and(|it| it.strip_references().dyn_trait().is_some());
612 if *receiver_expr_id == prev_filter_map_expr_id && !is_dyn_trait {
613 return Some(());
614 }
615 }
616
617 self.prev_filter_map_expr_id = None;
618 None
619 }
620}
621
622pub fn record_literal_missing_fields<'db>(
623 db: &'db dyn HirDatabase,
624 infer: &InferenceResult<'db>,
625 id: ExprId,
626 expr: &Expr,
627) -> Option<(VariantId, Vec<LocalFieldId>)> {
628 let (fields, spread) = match expr {
629 Expr::RecordLit { fields, spread, .. } => (fields, spread),
630 _ => return None,
631 };
632
633 let variant_def = infer.variant_resolution_for_expr(id)?;
634 if let VariantId::UnionId(_) = variant_def {
635 return None;
636 }
637
638 let variant_data = variant_def.fields(db);
639
640 let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
641 let missed_fields: Vec<LocalFieldId> = variant_data
646 .fields()
647 .iter()
648 .filter_map(|(f, d)| {
649 if specified_fields.contains(&d.name)
650 || matches!(spread, RecordSpread::Expr(_))
651 || (d.default_value.is_some() && matches!(spread, RecordSpread::FieldDefaults))
652 {
653 None
654 } else {
655 Some(f)
656 }
657 })
658 .collect();
659 if missed_fields.is_empty() {
660 return None;
661 }
662 Some((variant_def, missed_fields))
663}
664
665pub fn record_pattern_missing_fields<'db>(
666 db: &'db dyn HirDatabase,
667 infer: &InferenceResult<'db>,
668 id: PatId,
669 pat: &Pat,
670) -> Option<(VariantId, Vec<LocalFieldId>)> {
671 let (fields, ellipsis) = match pat {
672 Pat::Record { path: _, args, ellipsis } => (args, *ellipsis),
673 _ => return None,
674 };
675
676 let variant_def = infer.variant_resolution_for_pat(id)?;
677 if let VariantId::UnionId(_) = variant_def {
678 return None;
679 }
680
681 let variant_data = variant_def.fields(db);
682
683 let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
684 let missed_fields: Vec<LocalFieldId> = variant_data
688 .fields()
689 .iter()
690 .filter_map(
691 |(f, d)| {
692 if specified_fields.contains(&d.name) || ellipsis { None } else { Some(f) }
693 },
694 )
695 .collect();
696 if missed_fields.is_empty() {
697 return None;
698 }
699 Some((variant_def, missed_fields))
700}
701
702fn types_of_subpatterns_do_match(pat: PatId, body: &Body, infer: &InferenceResult<'_>) -> bool {
703 fn walk(pat: PatId, body: &Body, infer: &InferenceResult<'_>, has_type_mismatches: &mut bool) {
704 match infer.pat_has_type_mismatch(pat) {
705 true => *has_type_mismatches = true,
706 false if *has_type_mismatches => (),
707 false => {
708 let pat = &body[pat];
709 if let Pat::Lit(expr) = *pat {
710 *has_type_mismatches |= infer.expr_has_type_mismatch(expr);
711 if *has_type_mismatches {
712 return;
713 }
714 }
715 pat.walk_child_pats(|subpat| walk(subpat, body, infer, has_type_mismatches))
716 }
717 }
718 }
719
720 let mut has_type_mismatches = false;
721 walk(pat, body, infer, &mut has_type_mismatches);
722 !has_type_mismatches
723}
724
725fn missing_match_arms<'a, 'db>(
726 cx: &MatchCheckCtx<'a, 'db>,
727 scrut_ty: Ty<'a>,
728 witnesses: Vec<WitnessPat<'a, 'db>>,
729 arms_is_empty: bool,
730 krate: Crate,
731) -> String {
732 struct DisplayWitness<'a, 'b, 'db>(
733 &'a WitnessPat<'b, 'db>,
734 &'a MatchCheckCtx<'b, 'db>,
735 DisplayTarget,
736 );
737 impl fmt::Display for DisplayWitness<'_, '_, '_> {
738 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
739 let DisplayWitness(witness, cx, display_target) = *self;
740 let pat = cx.hoist_witness_pat(witness);
741 write!(f, "{}", pat.display(cx.db, display_target))
742 }
743 }
744
745 let non_empty_enum = match scrut_ty.as_adt() {
746 Some((AdtId::EnumId(e), _)) => !e.enum_variants(cx.db).variants.is_empty(),
747 _ => false,
748 };
749 let display_target = DisplayTarget::from_crate(cx.db, krate);
750 if arms_is_empty && !non_empty_enum {
751 format!("type `{}` is non-empty", scrut_ty.display(cx.db, display_target))
752 } else {
753 let pat_display = |witness| DisplayWitness(witness, cx, display_target);
754 const LIMIT: usize = 3;
755 match &*witnesses {
756 [witness] => format!("`{}` not covered", pat_display(witness)),
757 [head @ .., tail] if head.len() < LIMIT => {
758 let head = head.iter().map(pat_display);
759 format!("`{}` and `{}` not covered", head.format("`, `"), pat_display(tail))
760 }
761 _ => {
762 let (head, tail) = witnesses.split_at(LIMIT);
763 let head = head.iter().map(pat_display);
764 format!("`{}` and {} more not covered", head.format("`, `"), tail.len())
765 }
766 }
767 }
768}