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