//! 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 std::sync::Arc; use hir_def::{path::path, resolver::HasResolver, AdtId, AssocItemId, DefWithBodyId, HasModule}; use hir_expand::name; use itertools::Either; use itertools::Itertools; use rustc_hash::FxHashSet; use typed_arena::Arena; use crate::{ db::HirDatabase, diagnostics::match_check::{ self, deconstruct_pat::DeconstructedPat, usefulness::{compute_match_usefulness, MatchCheckCtx}, }, display::HirDisplay, InferenceResult, Ty, TyExt, }; pub(crate) use hir_def::{ body::Body, expr::{Expr, ExprId, MatchArm, Pat, PatId}, LocalFieldId, VariantId, }; pub enum BodyValidationDiagnostic { RecordMissingFields { record: Either, variant: VariantId, missed_fields: Vec, }, ReplaceFilterMapNextWithFindMap { method_call_expr: ExprId, }, MissingMatchArms { match_expr: ExprId, uncovered_patterns: String, }, } impl BodyValidationDiagnostic { pub fn collect(db: &dyn HirDatabase, owner: DefWithBodyId) -> Vec { let _p = profile::span("BodyValidationDiagnostic::collect"); let infer = db.infer(owner); let mut validator = ExprValidator::new(owner, infer); validator.validate_body(db); validator.diagnostics } } struct ExprValidator { owner: DefWithBodyId, infer: Arc, pub(super) diagnostics: Vec, } impl ExprValidator { fn new(owner: DefWithBodyId, infer: Arc) -> ExprValidator { ExprValidator { owner, infer, diagnostics: Vec::new() } } fn validate_body(&mut self, db: &dyn HirDatabase) { let body = db.body(self.owner); let mut filter_map_next_checker = None; for (id, expr) in body.exprs.iter() { if let Some((variant, missed_fields, true)) = 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, db, self.infer.clone()); } Expr::Call { .. } | Expr::MethodCall { .. } => { self.validate_call(db, id, expr, &mut filter_map_next_checker); } _ => {} } } for (id, pat) in body.pats.iter() { if let Some((variant, missed_fields, true)) = 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, db: &dyn HirDatabase, call_id: ExprId, expr: &Expr, filter_map_next_checker: &mut Option, ) { // Check that the number of arguments matches the number of parameters. // FIXME: Due to shortcomings in the current type system implementation, only emit this // diagnostic if there are no type mismatches in the containing function. if self.infer.expr_type_mismatches().next().is_some() { return; } match expr { Expr::MethodCall { receiver, .. } => { let (callee, _) = match self.infer.method_resolution(call_id) { Some(it) => it, None => return, }; if filter_map_next_checker .get_or_insert_with(|| { FilterMapNextChecker::new(&self.owner.resolver(db.upcast()), db) }) .check(call_id, receiver, &callee) .is_some() { self.diagnostics.push( BodyValidationDiagnostic::ReplaceFilterMapNextWithFindMap { method_call_expr: call_id, }, ); } } _ => return, }; } fn validate_match( &mut self, id: ExprId, match_expr: ExprId, arms: &[MatchArm], db: &dyn HirDatabase, infer: Arc, ) { let body = db.body(self.owner); let match_expr_ty = &infer[match_expr]; if match_expr_ty.is_unknown() { return; } let pattern_arena = Arena::new(); let cx = MatchCheckCtx::new(self.owner.module(db.upcast()), self.owner, db, &pattern_arena); let mut m_arms = Vec::with_capacity(arms.len()); let mut has_lowering_errors = false; for arm in arms { if let Some(pat_ty) = infer.type_of_pat.get(arm.pat) { // We only include patterns whose type matches the type // of the match 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 match expression type if // necessary. // // FIXME we should use the type checker for this. if (pat_ty == match_expr_ty || match_expr_ty .as_reference() .map(|(match_expr_ty, ..)| match_expr_ty == pat_ty) .unwrap_or(false)) && types_of_subpatterns_do_match(arm.pat, &body, &infer) { // If we had a NotUsefulMatchArm diagnostic, we could // check the usefulness of each pattern as we added it // to the matrix here. let m_arm = match_check::MatchArm { pat: self.lower_pattern(&cx, arm.pat, db, &body, &mut has_lowering_errors), has_guard: arm.guard.is_some(), }; m_arms.push(m_arm); if !has_lowering_errors { continue; } } } // If we can't resolve the type of a pattern, or the pattern type doesn't // fit the match expression, we skip this diagnostic. Skipping the entire // diagnostic rather than just not including this match arm is preferred // to avoid the chance of false positives. cov_mark::hit!(validate_match_bailed_out); return; } let report = compute_match_usefulness(&cx, &m_arms, match_expr_ty); // FIXME Report unreacheble 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: id, uncovered_patterns: missing_match_arms(&cx, match_expr_ty, witnesses, arms), }); } } fn lower_pattern<'p>( &self, cx: &MatchCheckCtx<'_, 'p>, pat: PatId, db: &dyn HirDatabase, body: &Body, have_errors: &mut bool, ) -> &'p DeconstructedPat<'p> { let mut patcx = match_check::PatCtxt::new(db, &self.infer, body); let pattern = patcx.lower_pattern(pat); let pattern = cx.pattern_arena.alloc(DeconstructedPat::from_pat(cx, &pattern)); if !patcx.errors.is_empty() { *have_errors = true; } pattern } } struct FilterMapNextChecker { filter_map_function_id: Option, next_function_id: Option, prev_filter_map_expr_id: Option, } impl FilterMapNextChecker { fn new(resolver: &hir_def::resolver::Resolver, db: &dyn HirDatabase) -> Self { // Find and store the FunctionIds for Iterator::filter_map and Iterator::next let iterator_path = path![core::iter::Iterator]; let mut filter_map_function_id = None; let mut next_function_id = None; if let Some(iterator_trait_id) = resolver.resolve_known_trait(db.upcast(), &iterator_path) { let iterator_trait_items = &db.trait_data(iterator_trait_id).items; for item in iterator_trait_items.iter() { if let (name, AssocItemId::FunctionId(id)) = item { if *name == name![filter_map] { filter_map_function_id = Some(*id); } if *name == name![next] { next_function_id = Some(*id); } } if filter_map_function_id.is_some() && next_function_id.is_some() { break; } } } Self { filter_map_function_id, next_function_id, prev_filter_map_expr_id: 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? { if let Some(prev_filter_map_expr_id) = self.prev_filter_map_expr_id { if *receiver_expr_id == prev_filter_map_expr_id { 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, /*exhaustive*/ bool)> { let (fields, exhaustive) = match expr { Expr::RecordLit { fields, spread, ellipsis, is_assignee_expr, .. } => { let exhaustive = if *is_assignee_expr { !*ellipsis } else { spread.is_none() }; (fields, exhaustive) } _ => return None, }; let variant_def = infer.variant_resolution_for_expr(id)?; if let VariantId::UnionId(_) = variant_def { return None; } let variant_data = variant_def.variant_data(db.upcast()); let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect(); let missed_fields: Vec = variant_data .fields() .iter() .filter_map(|(f, d)| if specified_fields.contains(&d.name) { None } else { Some(f) }) .collect(); if missed_fields.is_empty() { return None; } Some((variant_def, missed_fields, exhaustive)) } pub fn record_pattern_missing_fields( db: &dyn HirDatabase, infer: &InferenceResult, id: PatId, pat: &Pat, ) -> Option<(VariantId, Vec, /*exhaustive*/ bool)> { let (fields, exhaustive) = 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.variant_data(db.upcast()); let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect(); let missed_fields: Vec = variant_data .fields() .iter() .filter_map(|(f, d)| if specified_fields.contains(&d.name) { None } else { Some(f) }) .collect(); if missed_fields.is_empty() { return None; } Some((variant_def, missed_fields, exhaustive)) } 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.type_mismatch_for_pat(pat) { Some(_) => *has_type_mismatches = true, None => { body[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<'p>( cx: &MatchCheckCtx<'_, 'p>, scrut_ty: &Ty, witnesses: Vec>, arms: &[MatchArm], ) -> String { struct DisplayWitness<'a, 'p>(&'a DeconstructedPat<'p>, &'a MatchCheckCtx<'a, 'p>); impl<'a, 'p> fmt::Display for DisplayWitness<'a, 'p> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let DisplayWitness(witness, cx) = *self; let pat = witness.to_pat(cx); write!(f, "{}", pat.display(cx.db)) } } let non_empty_enum = match scrut_ty.as_adt() { Some((AdtId::EnumId(e), _)) => !cx.db.enum_data(e).variants.is_empty(), _ => false, }; if arms.is_empty() && !non_empty_enum { format!("type `{}` is non-empty", scrut_ty.display(cx.db)) } else { let pat_display = |witness| DisplayWitness(witness, cx); 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()) } } } }