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 self.infer.pat_has_type_mismatch(pat) {
347 return None;
348 }
349 let initializer = initializer?;
350 let ty = self.infer.type_of_expr_with_adjust(initializer)?;
351 if ty.references_non_lt_error() {
352 return None;
353 }
354
355 let mut have_errors = false;
356 let deconstructed_pat = self.lower_pattern(cx, pat, &mut have_errors);
357
358 if have_errors || matches!(deconstructed_pat.ctor(), Constructor::Wildcard) {
360 return None;
361 }
362
363 let match_arm = rustc_pattern_analysis::MatchArm {
364 pat: pattern_arena.alloc(deconstructed_pat),
365 has_guard: false,
366 arm_data: (),
367 };
368 let report = match cx.compute_match_usefulness(&[match_arm], ty, None) {
369 Ok(v) => v,
370 Err(e) => {
371 debug!(?e, "match usefulness error");
372 return None;
373 }
374 };
375 let witnesses = report.non_exhaustiveness_witnesses;
376 if witnesses.is_empty() {
377 return None;
378 }
379 Some(BodyValidationDiagnostic::NonExhaustiveLet {
380 pat,
381 uncovered_patterns: missing_match_arms(
382 cx,
383 ty,
384 witnesses,
385 false,
386 self.owner.krate(self.db()),
387 ),
388 })
389 }
390
391 fn lower_pattern<'a>(
392 &self,
393 cx: &MatchCheckCtx<'a, 'db>,
394 pat: PatId,
395 have_errors: &mut bool,
396 ) -> DeconstructedPat<'a, 'db> {
397 let mut patcx = match_check::PatCtxt::new(self.db(), self.infer, self.body);
398 let pattern = patcx.lower_pattern(pat);
399 let pattern = cx.lower_pat(&pattern);
400 if !patcx.errors.is_empty() {
401 *have_errors = true;
402 }
403 pattern
404 }
405
406 fn check_unused_must_use(
407 &self,
408 mut expr: ExprId,
409 acc: &mut Vec<BodyValidationDiagnostic<'db>>,
410 ) {
411 loop {
416 match &self.body[expr] {
417 Expr::Block { tail: Some(tail), .. } | Expr::Const(tail) => expr = *tail,
418 Expr::If { then_branch, else_branch, .. } => {
419 self.check_unused_must_use(*then_branch, acc);
420 if let Some(else_branch) = else_branch {
421 self.check_unused_must_use(*else_branch, acc);
422 }
423 return;
424 }
425 Expr::Match { arms, .. } => {
426 for arm in arms.iter() {
427 self.check_unused_must_use(arm.expr, acc);
428 }
429 return;
430 }
431 _ => break,
432 }
433 }
434
435 let fn_def = match &self.body[expr] {
436 Expr::Call { callee, .. } => {
437 let callee_ty = self.infer.expr_ty(*callee);
438 if let TyKind::FnDef(CallableIdWrapper(CallableDefId::FunctionId(func)), _) =
439 callee_ty.kind()
440 {
441 Some(func.into())
442 } else {
443 None
444 }
445 }
446 Expr::MethodCall { .. } => {
447 self.infer.method_resolution(expr).map(|(func, _)| func.into())
448 }
449 _ => None,
450 };
451 let ty_def = self.infer.type_of_expr_with_adjust(expr).and_then(|ty| match ty.kind() {
452 TyKind::Adt(adt, _) => Some(adt.def_id().into()),
453 _ => None,
454 });
455 let must_use_diag = |owner| {
456 AttrFlags::must_use_message(self.db(), owner?)
457 .map(|message| BodyValidationDiagnostic::UnusedMustUse { expr, message })
458 };
459 if let Some(diag) = must_use_diag(fn_def).or_else(|| must_use_diag(ty_def)) {
460 acc.push(diag);
461 }
462 }
463
464 fn check_for_trailing_return(&mut self, body_expr: ExprId, body: &Body) {
465 if !self.validate_lints {
466 return;
467 }
468 match &body[body_expr] {
469 Expr::Block { statements, tail, .. } => {
470 let last_stmt = tail.or_else(|| match statements.last()? {
471 Statement::Expr { expr, .. } => Some(*expr),
472 _ => None,
473 });
474 if let Some(last_stmt) = last_stmt {
475 self.check_for_trailing_return(last_stmt, body);
476 }
477 }
478 Expr::If { then_branch, else_branch, .. } => {
479 self.check_for_trailing_return(*then_branch, body);
480 if let Some(else_branch) = else_branch {
481 self.check_for_trailing_return(*else_branch, body);
482 }
483 }
484 Expr::Match { arms, .. } => {
485 for arm in arms.iter() {
486 let MatchArm { expr, .. } = arm;
487 self.check_for_trailing_return(*expr, body);
488 }
489 }
490 Expr::Return { .. } => {
491 self.diagnostics.push(BodyValidationDiagnostic::RemoveTrailingReturn {
492 return_expr: body_expr,
493 });
494 }
495 _ => (),
496 }
497 }
498
499 fn check_for_unnecessary_else(&mut self, id: ExprId, expr: &Expr) {
500 if !self.validate_lints {
501 return;
502 }
503 if let Expr::If { condition: _, then_branch, else_branch } = expr {
504 if else_branch.is_none() {
505 return;
506 }
507 if let Expr::Block { statements, tail, .. } = &self.body[*then_branch] {
508 let last_then_expr = tail.or_else(|| match statements.last()? {
509 Statement::Expr { expr, .. } => Some(*expr),
510 _ => None,
511 });
512 if let Some(last_then_expr) = last_then_expr
513 && let Some(last_then_expr_ty) =
514 self.infer.type_of_expr_with_adjust(last_then_expr)
515 && last_then_expr_ty.is_never()
516 {
517 let source_map = &Body::with_source_map(self.db(), self.owner).1;
519 let Ok(source_ptr) = source_map.expr_syntax(id) else {
520 return;
521 };
522 let root = source_ptr.file_syntax(self.db());
523 let either::Left(ast::Expr::IfExpr(if_expr)) = source_ptr.value.to_node(&root)
524 else {
525 return;
526 };
527 let mut top_if_expr = if_expr;
528 loop {
529 let parent = top_if_expr.syntax().parent();
530 let has_parent_expr_stmt_or_stmt_list =
531 parent.as_ref().is_some_and(|node| {
532 ast::ExprStmt::can_cast(node.kind())
533 | ast::StmtList::can_cast(node.kind())
534 });
535 if has_parent_expr_stmt_or_stmt_list {
536 break;
539 }
540 let Some(parent_if_expr) = parent.and_then(ast::IfExpr::cast) else {
541 return;
543 };
544 top_if_expr = parent_if_expr;
546 }
547
548 self.diagnostics
549 .push(BodyValidationDiagnostic::RemoveUnnecessaryElse { if_expr: id })
550 }
551 }
552 }
553 }
554}
555
556struct FilterMapNextChecker<'db> {
557 filter_map_function_id: Option<hir_def::FunctionId>,
558 next_function_id: Option<hir_def::FunctionId>,
559 prev_filter_map_expr_id: Option<ExprId>,
560 prev_receiver_ty: Option<Ty<'db>>,
561}
562
563impl<'db> FilterMapNextChecker<'db> {
564 fn new(lang_items: &'db LangItems, db: &'db dyn HirDatabase) -> Self {
565 let (next_function_id, filter_map_function_id) = match lang_items.IteratorNext {
567 Some(next_function_id) => (
568 Some(next_function_id),
569 match next_function_id.lookup(db).container {
570 ItemContainerId::TraitId(iterator_trait_id) => {
571 let iterator_trait_items = &iterator_trait_id.trait_items(db).items;
572 iterator_trait_items.iter().find_map(|(name, it)| match it {
573 &AssocItemId::FunctionId(id) if *name == sym::filter_map => Some(id),
574 _ => None,
575 })
576 }
577 _ => None,
578 },
579 ),
580 None => (None, None),
581 };
582 Self {
583 filter_map_function_id,
584 next_function_id,
585 prev_filter_map_expr_id: None,
586 prev_receiver_ty: None,
587 }
588 }
589
590 fn check(
592 &mut self,
593 current_expr_id: ExprId,
594 receiver_expr_id: &ExprId,
595 function_id: &hir_def::FunctionId,
596 ) -> Option<()> {
597 if *function_id == self.filter_map_function_id? {
598 self.prev_filter_map_expr_id = Some(current_expr_id);
599 return None;
600 }
601
602 if *function_id == self.next_function_id?
603 && let Some(prev_filter_map_expr_id) = self.prev_filter_map_expr_id
604 {
605 let is_dyn_trait = self
606 .prev_receiver_ty
607 .as_ref()
608 .is_some_and(|it| it.strip_references().dyn_trait().is_some());
609 if *receiver_expr_id == prev_filter_map_expr_id && !is_dyn_trait {
610 return Some(());
611 }
612 }
613
614 self.prev_filter_map_expr_id = None;
615 None
616 }
617}
618
619pub fn record_literal_missing_fields<'db>(
620 db: &'db dyn HirDatabase,
621 infer: &InferenceResult<'db>,
622 id: ExprId,
623 expr: &Expr,
624) -> Option<(VariantId, Vec<LocalFieldId>)> {
625 let (fields, spread) = match expr {
626 Expr::RecordLit { fields, spread, .. } => (fields, spread),
627 _ => return None,
628 };
629
630 let variant_def = infer.variant_resolution_for_expr(id)?;
631 if let VariantId::UnionId(_) = variant_def {
632 return None;
633 }
634
635 let variant_data = variant_def.fields(db);
636
637 let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
638 let missed_fields: Vec<LocalFieldId> = variant_data
643 .fields()
644 .iter()
645 .filter_map(|(f, d)| {
646 if specified_fields.contains(&d.name)
647 || matches!(spread, RecordSpread::Expr(_))
648 || (d.default_value.is_some() && matches!(spread, RecordSpread::FieldDefaults))
649 {
650 None
651 } else {
652 Some(f)
653 }
654 })
655 .collect();
656 if missed_fields.is_empty() {
657 return None;
658 }
659 Some((variant_def, missed_fields))
660}
661
662pub fn record_pattern_missing_fields<'db>(
663 db: &'db dyn HirDatabase,
664 infer: &InferenceResult<'db>,
665 id: PatId,
666 pat: &Pat,
667) -> Option<(VariantId, Vec<LocalFieldId>)> {
668 let (fields, ellipsis) = match pat {
669 Pat::Record { path: _, args, ellipsis } => (args, *ellipsis),
670 _ => return None,
671 };
672
673 let variant_def = infer.variant_resolution_for_pat(id)?;
674 if let VariantId::UnionId(_) = variant_def {
675 return None;
676 }
677
678 let variant_data = variant_def.fields(db);
679
680 let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect();
681 let missed_fields: Vec<LocalFieldId> = variant_data
685 .fields()
686 .iter()
687 .filter_map(
688 |(f, d)| {
689 if specified_fields.contains(&d.name) || ellipsis { None } else { Some(f) }
690 },
691 )
692 .collect();
693 if missed_fields.is_empty() {
694 return None;
695 }
696 Some((variant_def, missed_fields))
697}
698
699fn types_of_subpatterns_do_match(pat: PatId, body: &Body, infer: &InferenceResult<'_>) -> bool {
700 fn walk(pat: PatId, body: &Body, infer: &InferenceResult<'_>, has_type_mismatches: &mut bool) {
701 match infer.pat_has_type_mismatch(pat) {
702 true => *has_type_mismatches = true,
703 false if *has_type_mismatches => (),
704 false => {
705 let pat = &body[pat];
706 if let Pat::Lit(expr) = *pat {
707 *has_type_mismatches |= infer.expr_has_type_mismatch(expr);
708 if *has_type_mismatches {
709 return;
710 }
711 }
712 pat.walk_child_pats(|subpat| walk(subpat, body, infer, has_type_mismatches))
713 }
714 }
715 }
716
717 let mut has_type_mismatches = false;
718 walk(pat, body, infer, &mut has_type_mismatches);
719 !has_type_mismatches
720}
721
722fn missing_match_arms<'a, 'db>(
723 cx: &MatchCheckCtx<'a, 'db>,
724 scrut_ty: Ty<'a>,
725 witnesses: Vec<WitnessPat<'a, 'db>>,
726 arms_is_empty: bool,
727 krate: Crate,
728) -> String {
729 struct DisplayWitness<'a, 'b, 'db>(
730 &'a WitnessPat<'b, 'db>,
731 &'a MatchCheckCtx<'b, 'db>,
732 DisplayTarget,
733 );
734 impl fmt::Display for DisplayWitness<'_, '_, '_> {
735 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
736 let DisplayWitness(witness, cx, display_target) = *self;
737 let pat = cx.hoist_witness_pat(witness);
738 write!(f, "{}", pat.display(cx.db, display_target))
739 }
740 }
741
742 let non_empty_enum = match scrut_ty.as_adt() {
743 Some((AdtId::EnumId(e), _)) => !e.enum_variants(cx.db).variants.is_empty(),
744 _ => false,
745 };
746 let display_target = DisplayTarget::from_crate(cx.db, krate);
747 if arms_is_empty && !non_empty_enum {
748 format!("type `{}` is non-empty", scrut_ty.display(cx.db, display_target))
749 } else {
750 let pat_display = |witness| DisplayWitness(witness, cx, display_target);
751 const LIMIT: usize = 3;
752 match &*witnesses {
753 [witness] => format!("`{}` not covered", pat_display(witness)),
754 [head @ .., tail] if head.len() < LIMIT => {
755 let head = head.iter().map(pat_display);
756 format!("`{}` and `{}` not covered", head.format("`, `"), pat_display(tail))
757 }
758 _ => {
759 let (head, tail) = witnesses.split_at(LIMIT);
760 let head = head.iter().map(pat_display);
761 format!("`{}` and {} more not covered", head.format("`, `"), tail.len())
762 }
763 }
764 }
765}