Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/hir-ty/src/diagnostics/expr.rs')
| -rw-r--r-- | crates/hir-ty/src/diagnostics/expr.rs | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/crates/hir-ty/src/diagnostics/expr.rs b/crates/hir-ty/src/diagnostics/expr.rs new file mode 100644 index 0000000000..335d95c0cf --- /dev/null +++ b/crates/hir-ty/src/diagnostics/expr.rs @@ -0,0 +1,369 @@ +//! 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::sync::Arc; + +use hir_def::{path::path, resolver::HasResolver, AssocItemId, DefWithBodyId, HasModule}; +use hir_expand::name; +use itertools::Either; +use rustc_hash::FxHashSet; +use typed_arena::Arena; + +use crate::{ + db::HirDatabase, + diagnostics::match_check::{ + self, + deconstruct_pat::DeconstructedPat, + usefulness::{compute_match_usefulness, MatchCheckCtx}, + }, + InferenceResult, TyExt, +}; + +pub(crate) use hir_def::{ + body::Body, + expr::{Expr, ExprId, MatchArm, Pat, PatId}, + LocalFieldId, VariantId, +}; + +pub enum BodyValidationDiagnostic { + RecordMissingFields { + record: Either<ExprId, PatId>, + variant: VariantId, + missed_fields: Vec<LocalFieldId>, + }, + ReplaceFilterMapNextWithFindMap { + method_call_expr: ExprId, + }, + MissingMatchArms { + match_expr: ExprId, + }, +} + +impl BodyValidationDiagnostic { + pub fn collect(db: &dyn HirDatabase, owner: DefWithBodyId) -> Vec<BodyValidationDiagnostic> { + 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<InferenceResult>, + pub(super) diagnostics: Vec<BodyValidationDiagnostic>, +} + +impl ExprValidator { + fn new(owner: DefWithBodyId, infer: Arc<InferenceResult>) -> 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<FilterMapNextChecker>, + ) { + // 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<InferenceResult>, + ) { + 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 { + module: self.owner.module(db.upcast()), + body: self.owner, + db, + pattern_arena: &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; + // FIXME Report witnesses + // eprintln!("compute_match_usefulness(..) -> {:?}", &witnesses); + if !witnesses.is_empty() { + self.diagnostics.push(BodyValidationDiagnostic::MissingMatchArms { match_expr: id }); + } + } + + 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<hir_def::FunctionId>, + next_function_id: Option<hir_def::FunctionId>, + prev_filter_map_expr_id: Option<ExprId>, +} + +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<LocalFieldId>, /*exhaustive*/ bool)> { + let (fields, exhaustive) = match expr { + Expr::RecordLit { path: _, fields, spread } => (fields, spread.is_none()), + _ => 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<LocalFieldId> = 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<LocalFieldId>, /*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<LocalFieldId> = 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 +} |