| //! 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 base_db::Crate; | |
| use either::Either; | |
| use hir_def::{ | |
| AdtId, AssocItemId, CallableDefId, DefWithBodyId, HasModule, ItemContainerId, Lookup, | |
| attrs::AttrFlags, | |
| lang_item::LangItems, | |
| resolver::{HasResolver, ValueNs}, | |
| }; | |
| use intern::sym; | |
| use itertools::Itertools; | |
| use rustc_hash::FxHashSet; | |
| use rustc_pattern_analysis::constructor::Constructor; | |
| use rustc_type_ir::inherent::IntoKind; | |
| use syntax::{ | |
| AstNode, | |
| ast::{self, UnaryOp}, | |
| }; | |
| use tracing::debug; | |
| use typed_arena::Arena; | |
| use crate::{ | |
| Adjust, InferenceResult, | |
| db::HirDatabase, | |
| diagnostics::match_check::{ | |
| self, | |
| pat_analysis::{self, DeconstructedPat, MatchCheckCtx, WitnessPat}, | |
| }, | |
| display::{DisplayTarget, HirDisplay}, | |
| next_solver::{ | |
| CallableIdWrapper, DbInterner, ParamEnv, Ty, TyKind, TypingMode, | |
| infer::{DbInternerInferExt, InferCtxt}, | |
| }, | |
| }; | |
| pub(crate) use hir_def::{ | |
| LocalFieldId, VariantId, | |
| expr_store::Body, | |
| hir::{Expr, ExprId, MatchArm, Pat, PatId, RecordSpread, Statement}, | |
| }; | |
| pub enum BodyValidationDiagnostic<'db> { | |
| RecordMissingFields { | |
| record: Either<ExprId, PatId>, | |
| variant: VariantId, | |
| missed_fields: Vec<LocalFieldId>, | |
| }, | |
| ReplaceFilterMapNextWithFindMap { | |
| method_call_expr: ExprId, | |
| }, | |
| MissingMatchArms { | |
| match_expr: ExprId, | |
| uncovered_patterns: String, | |
| }, | |
| NonExhaustiveLet { | |
| pat: PatId, | |
| uncovered_patterns: String, | |
| }, | |
| RemoveTrailingReturn { | |
| return_expr: ExprId, | |
| }, | |
| RemoveUnnecessaryElse { | |
| if_expr: ExprId, | |
| }, | |
| UnusedMustUse { | |
| expr: ExprId, | |
| message: Option<&'db str>, | |
| }, | |
| } | |
| impl<'db> BodyValidationDiagnostic<'db> { | |
| pub fn collect( | |
| db: &'db dyn HirDatabase, | |
| owner: DefWithBodyId, | |
| validate_lints: bool, | |
| ) -> Vec<BodyValidationDiagnostic<'db>> { | |
| let _p = tracing::info_span!("BodyValidationDiagnostic::collect").entered(); | |
| let infer = InferenceResult::of(db, owner); | |
| let body = Body::of(db, owner); | |
| let env = db.trait_environment(owner.generic_def(db)); | |
| let interner = DbInterner::new_with(db, owner.krate(db)); | |
| let infcx = | |
| interner.infer_ctxt().build(TypingMode::typeck_for_body(interner, owner.into())); | |
| let mut validator = ExprValidator { | |
| owner, | |
| body, | |
| infer, | |
| diagnostics: Vec::new(), | |
| validate_lints, | |
| env, | |
| infcx, | |
| }; | |
| validator.validate_body(); | |
| validator.diagnostics | |
| } | |
| } | |
| struct ExprValidator<'db> { | |
| owner: DefWithBodyId, | |
| body: &'db Body, | |
| infer: &'db InferenceResult<'db>, | |
| env: ParamEnv<'db>, | |
| diagnostics: Vec<BodyValidationDiagnostic<'db>>, | |
| validate_lints: bool, | |
| infcx: InferCtxt<'db>, | |
| } | |
| impl<'db> ExprValidator<'db> { | |
| #[inline] | |
| fn db(&self) -> &'db dyn HirDatabase { | |
| self.infcx.interner.db | |
| } | |
| fn validate_body(&mut self) { | |
| let db = self.db(); | |
| let mut filter_map_next_checker = None; | |
| // we'll pass &mut self while iterating over body.exprs, so they need to be disjoint | |
| let body = self.body; | |
| if matches!(self.owner, DefWithBodyId::FunctionId(_)) { | |
| self.check_for_trailing_return(body.root_expr(), body); | |
| } | |
| for (id, expr) in body.exprs() { | |
| if let Some((variant, missed_fields)) = | |
| 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); | |
| } | |
| Expr::Call { .. } | Expr::MethodCall { .. } => { | |
| self.validate_call(id, expr, &mut filter_map_next_checker); | |
| } | |
| Expr::Closure { body: body_expr, .. } => { | |
| self.check_for_trailing_return(*body_expr, body); | |
| } | |
| Expr::If { .. } => { | |
| self.check_for_unnecessary_else(id, expr); | |
| } | |
| Expr::Block { statements, .. } => { | |
| self.validate_block(statements); | |
| } | |
| _ => {} | |
| } | |
| } | |
| } | |
| fn validate_call( | |
| &mut self, | |
| call_id: ExprId, | |
| expr: &Expr, | |
| filter_map_next_checker: &mut Option<FilterMapNextChecker<'db>>, | |
| ) { | |
| if !self.validate_lints { | |
| return; | |
| } | |
| // Check that the number of arguments matches the number of parameters. | |
| if self.infer.exprs_have_type_mismatches() { | |
| // FIXME: Due to shortcomings in the current type system implementation, only emit | |
| // this diagnostic if there are no type mismatches in the containing function. | |
| } else if let Expr::MethodCall { receiver, .. } = expr { | |
| let (callee, _) = match self.infer.method_resolution(call_id) { | |
| Some(it) => it, | |
| None => return, | |
| }; | |
| let checker = filter_map_next_checker.get_or_insert_with(|| { | |
| FilterMapNextChecker::new(self.infcx.interner.lang_items(), self.db()) | |
| }); | |
| if checker.check(call_id, receiver, &callee).is_some() { | |
| self.diagnostics.push(BodyValidationDiagnostic::ReplaceFilterMapNextWithFindMap { | |
| method_call_expr: call_id, | |
| }); | |
| } | |
| if let Some(receiver_ty) = self.infer.type_of_expr_with_adjust(*receiver) { | |
| checker.prev_receiver_ty = Some(receiver_ty); | |
| } | |
| } | |
| } | |
| fn validate_match(&mut self, match_expr: ExprId, scrutinee_expr: ExprId, arms: &[MatchArm]) { | |
| let Some(scrut_ty) = self.infer.type_of_expr_with_adjust(scrutinee_expr) else { | |
| return; | |
| }; | |
| if scrut_ty.references_non_lt_error() { | |
| return; | |
| } | |
| let cx = MatchCheckCtx::new(self.owner.module(self.db()), &self.infcx, self.env); | |
| let pattern_arena = Arena::new(); | |
| let mut m_arms = Vec::with_capacity(arms.len()); | |
| let mut has_lowering_errors = false; | |
| // Note: Skipping the entire diagnostic rather than just not including a faulty match arm is | |
| // preferred to avoid the chance of false positives. | |
| for arm in arms { | |
| let pat_ty = self.infer.type_of_pat_with_adjust(arm.pat); | |
| if pat_ty.references_non_lt_error() { | |
| return; | |
| } | |
| // We only include patterns whose type matches the type | |
| // of the scrutinee 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 scrutinee expression type if | |
| // necessary. | |
| // | |
| // FIXME we should use the type checker for this. | |
| if (pat_ty == scrut_ty | |
| || scrut_ty | |
| .as_reference() | |
| .is_none_or(|(match_expr_ty, ..)| match_expr_ty == pat_ty)) | |
| && types_of_subpatterns_do_match(arm.pat, self.body, self.infer) | |
| { | |
| // If we had a NotUsefulMatchArm diagnostic, we could | |
| // check the usefulness of each pattern as we added it | |
| // to the matrix here. | |
| let pat = self.lower_pattern(&cx, arm.pat, &mut has_lowering_errors); | |
| let m_arm = pat_analysis::MatchArm { | |
| pat: pattern_arena.alloc(pat), | |
| has_guard: arm.guard.is_some(), | |
| arm_data: (), | |
| }; | |
| m_arms.push(m_arm); | |
| if !has_lowering_errors { | |
| continue; | |
| } | |
| } | |
| // If the pattern type doesn't fit the match expression, we skip this diagnostic. | |
| cov_mark::hit!(validate_match_bailed_out); | |
| return; | |
| } | |
| let known_valid_scrutinee = Some(self.is_known_valid_scrutinee(scrutinee_expr)); | |
| let report = | |
| match cx.compute_match_usefulness(m_arms.as_slice(), scrut_ty, known_valid_scrutinee) { | |
| Ok(report) => report, | |
| Err(()) => return, | |
| }; | |
| // FIXME Report unreachable 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, | |
| uncovered_patterns: missing_match_arms( | |
| &cx, | |
| scrut_ty, | |
| witnesses, | |
| m_arms.is_empty(), | |
| self.owner.krate(self.db()), | |
| ), | |
| }); | |
| } | |
| } | |
| // [rustc's `is_known_valid_scrutinee`](https://github.com/rust-lang/rust/blob/c9bd03cb724e13cca96ad320733046cbdb16fbbe/compiler/rustc_mir_build/src/thir/pattern/check_match.rs#L288) | |
| // | |
| // While the above function in rustc uses thir exprs, r-a doesn't have them. | |
| // So, the logic here is getting same result as "hir lowering + match with lowered thir" | |
| // with "hir only" | |
| fn is_known_valid_scrutinee(&self, scrutinee_expr: ExprId) -> bool { | |
| let db = self.db(); | |
| if self | |
| .infer | |
| .expr_adjustments | |
| .get(&scrutinee_expr) | |
| .is_some_and(|adjusts| adjusts.iter().any(|a| matches!(a.kind, Adjust::Deref(..)))) | |
| { | |
| return false; | |
| } | |
| match &self.body[scrutinee_expr] { | |
| Expr::UnaryOp { op: UnaryOp::Deref, .. } => false, | |
| Expr::Path(path) => { | |
| let value_or_partial = self.owner.resolver(db).resolve_path_in_value_ns_fully( | |
| db, | |
| path, | |
| self.body.expr_path_hygiene(scrutinee_expr), | |
| ); | |
| value_or_partial.is_none_or(|v| !matches!(v, ValueNs::StaticId(_))) | |
| } | |
| Expr::Field { expr, .. } => match self.infer.expr_ty(*expr).kind() { | |
| TyKind::Adt(adt, ..) if matches!(adt.def_id(), AdtId::UnionId(_)) => false, | |
| _ => self.is_known_valid_scrutinee(*expr), | |
| }, | |
| Expr::Index { base, .. } => self.is_known_valid_scrutinee(*base), | |
| Expr::Cast { expr, .. } => self.is_known_valid_scrutinee(*expr), | |
| Expr::Missing => false, | |
| _ => true, | |
| } | |
| } | |
| fn validate_block(&mut self, statements: &[Statement]) { | |
| let pattern_arena = Arena::new(); | |
| let cx = MatchCheckCtx::new(self.owner.module(self.db()), &self.infcx, self.env); | |
| for stmt in statements { | |
| match *stmt { | |
| Statement::Expr { expr: stmt_expr, has_semi: true } if self.validate_lints => { | |
| let mut diags = Vec::new(); | |
| self.check_unused_must_use(stmt_expr, &mut diags); | |
| self.diagnostics.extend(diags); | |
| } | |
| Statement::Let { pat, initializer, else_branch: None, .. } => { | |
| if let Some(diag) = | |
| self.check_non_exhaustive_let(&cx, &pattern_arena, pat, initializer) | |
| { | |
| self.diagnostics.push(diag); | |
| } | |
| } | |
| _ => {} | |
| } | |
| } | |
| } | |
| fn check_non_exhaustive_let<'a>( | |
| &self, | |
| cx: &MatchCheckCtx<'a, 'db>, | |
| pattern_arena: &'a Arena<DeconstructedPat<'a, 'db>>, | |
| pat: PatId, | |
| initializer: Option<ExprId>, | |
| ) -> Option<BodyValidationDiagnostic<'db>> { | |
| // Bail out if any subpattern has a type mismatch: the pattern isn't well-formed then, and | |
| // deconstructing it can produce constructors with mismatching arities, which the | |
| // usefulness analysis doesn't expect. `check_match` does the same for its arms. | |
| if !types_of_subpatterns_do_match(pat, self.body, self.infer) { | |
| return None; | |
| } | |
| let initializer = initializer?; | |
| let ty = self.infer.type_of_expr_with_adjust(initializer)?; | |
| if ty.references_non_lt_error() { | |
| return None; | |
| } | |
| let mut have_errors = false; | |
| let deconstructed_pat = self.lower_pattern(cx, pat, &mut have_errors); | |
| // optimization, wildcard trivially hold | |
| if have_errors || matches!(deconstructed_pat.ctor(), Constructor::Wildcard) { | |
| return None; | |
| } | |
| let match_arm = rustc_pattern_analysis::MatchArm { | |
| pat: pattern_arena.alloc(deconstructed_pat), | |
| has_guard: false, | |
| arm_data: (), | |
| }; | |
| let report = match cx.compute_match_usefulness(&[match_arm], ty, None) { | |
| Ok(v) => v, | |
| Err(e) => { | |
| debug!(?e, "match usefulness error"); | |
| return None; | |
| } | |
| }; | |
| let witnesses = report.non_exhaustiveness_witnesses; | |
| if witnesses.is_empty() { | |
| return None; | |
| } | |
| Some(BodyValidationDiagnostic::NonExhaustiveLet { | |
| pat, | |
| uncovered_patterns: missing_match_arms( | |
| cx, | |
| ty, | |
| witnesses, | |
| false, | |
| self.owner.krate(self.db()), | |
| ), | |
| }) | |
| } | |
| fn lower_pattern<'a>( | |
| &self, | |
| cx: &MatchCheckCtx<'a, 'db>, | |
| pat: PatId, | |
| have_errors: &mut bool, | |
| ) -> DeconstructedPat<'a, 'db> { | |
| let mut patcx = match_check::PatCtxt::new(self.db(), self.infer, self.body); | |
| let pattern = patcx.lower_pattern(pat); | |
| let pattern = cx.lower_pat(&pattern); | |
| if !patcx.errors.is_empty() { | |
| *have_errors = true; | |
| } | |
| pattern | |
| } | |
| fn check_unused_must_use( | |
| &self, | |
| mut expr: ExprId, | |
| acc: &mut Vec<BodyValidationDiagnostic<'db>>, | |
| ) { | |
| // Walk through container expressions so that the value-producing leaf is | |
| // checked even when wrapped in a block, `unsafe { .. }`, `if`/`match`, or | |
| // a `const { .. }` block. Single-tail chains are followed by reassigning | |
| // `expr`; branching containers (`if`/`match`) recurse on each arm. | |
| loop { | |
| match &self.body[expr] { | |
| Expr::Block { tail: Some(tail), .. } | Expr::Const(tail) => expr = *tail, | |
| Expr::If { then_branch, else_branch, .. } => { | |
| self.check_unused_must_use(*then_branch, acc); | |
| if let Some(else_branch) = else_branch { | |
| self.check_unused_must_use(*else_branch, acc); | |
| } | |
| return; | |
| } | |
| Expr::Match { arms, .. } => { | |
| for arm in arms.iter() { | |
| self.check_unused_must_use(arm.expr, acc); | |
| } | |
| return; | |
| } | |
| _ => break, | |
| } | |
| } | |
| let fn_def = match &self.body[expr] { | |
| Expr::Call { callee, .. } => { | |
| let callee_ty = self.infer.expr_ty(*callee); | |
| if let TyKind::FnDef(CallableIdWrapper(CallableDefId::FunctionId(func)), _) = | |
| callee_ty.kind() | |
| { | |
| Some(func.into()) | |
| } else { | |
| None | |
| } | |
| } | |
| Expr::MethodCall { .. } => { | |
| self.infer.method_resolution(expr).map(|(func, _)| func.into()) | |
| } | |
| _ => None, | |
| }; | |
| let ty_def = self.infer.type_of_expr_with_adjust(expr).and_then(|ty| match ty.kind() { | |
| TyKind::Adt(adt, _) => Some(adt.def_id().into()), | |
| _ => None, | |
| }); | |
| let must_use_diag = |owner| { | |
| AttrFlags::must_use_message(self.db(), owner?) | |
| .map(|message| BodyValidationDiagnostic::UnusedMustUse { expr, message }) | |
| }; | |
| if let Some(diag) = must_use_diag(fn_def).or_else(|| must_use_diag(ty_def)) { | |
| acc.push(diag); | |
| } | |
| } | |
| fn check_for_trailing_return(&mut self, body_expr: ExprId, body: &Body) { | |
| if !self.validate_lints { | |
| return; | |
| } | |
| match &body[body_expr] { | |
| Expr::Block { statements, tail, .. } => { | |
| let last_stmt = tail.or_else(|| match statements.last()? { | |
| Statement::Expr { expr, .. } => Some(*expr), | |
| _ => None, | |
| }); | |
| if let Some(last_stmt) = last_stmt { | |
| self.check_for_trailing_return(last_stmt, body); | |
| } | |
| } | |
| Expr::If { then_branch, else_branch, .. } => { | |
| self.check_for_trailing_return(*then_branch, body); | |
| if let Some(else_branch) = else_branch { | |
| self.check_for_trailing_return(*else_branch, body); | |
| } | |
| } | |
| Expr::Match { arms, .. } => { | |
| for arm in arms.iter() { | |
| let MatchArm { expr, .. } = arm; | |
| self.check_for_trailing_return(*expr, body); | |
| } | |
| } | |
| Expr::Return { .. } => { | |
| self.diagnostics.push(BodyValidationDiagnostic::RemoveTrailingReturn { | |
| return_expr: body_expr, | |
| }); | |
| } | |
| _ => (), | |
| } | |
| } | |
| fn check_for_unnecessary_else(&mut self, id: ExprId, expr: &Expr) { | |
| if !self.validate_lints { | |
| return; | |
| } | |
| if let Expr::If { condition: _, then_branch, else_branch } = expr { | |
| if else_branch.is_none() { | |
| return; | |
| } | |
| if let Expr::Block { statements, tail, .. } = &self.body[*then_branch] { | |
| let last_then_expr = tail.or_else(|| match statements.last()? { | |
| Statement::Expr { expr, .. } => Some(*expr), | |
| _ => None, | |
| }); | |
| if let Some(last_then_expr) = last_then_expr | |
| && let Some(last_then_expr_ty) = | |
| self.infer.type_of_expr_with_adjust(last_then_expr) | |
| && last_then_expr_ty.is_never() | |
| { | |
| // Only look at sources if the then branch diverges and we have an else branch. | |
| let source_map = &Body::with_source_map(self.db(), self.owner).1; | |
| let Ok(source_ptr) = source_map.expr_syntax(id) else { | |
| return; | |
| }; | |
| let root = source_ptr.file_syntax(self.db()); | |
| let either::Left(ast::Expr::IfExpr(if_expr)) = source_ptr.value.to_node(&root) | |
| else { | |
| return; | |
| }; | |
| let mut top_if_expr = if_expr; | |
| loop { | |
| let parent = top_if_expr.syntax().parent(); | |
| let has_parent_expr_stmt_or_stmt_list = | |
| parent.as_ref().is_some_and(|node| { | |
| ast::ExprStmt::can_cast(node.kind()) | |
| | ast::StmtList::can_cast(node.kind()) | |
| }); | |
| if has_parent_expr_stmt_or_stmt_list { | |
| // Only emit diagnostic if parent or direct ancestor is either | |
| // an expr stmt or a stmt list. | |
| break; | |
| } | |
| let Some(parent_if_expr) = parent.and_then(ast::IfExpr::cast) else { | |
| // Bail if parent is neither an if expr, an expr stmt nor a stmt list. | |
| return; | |
| }; | |
| // Check parent if expr. | |
| top_if_expr = parent_if_expr; | |
| } | |
| self.diagnostics | |
| .push(BodyValidationDiagnostic::RemoveUnnecessaryElse { if_expr: id }) | |
| } | |
| } | |
| } | |
| } | |
| } | |
| struct FilterMapNextChecker<'db> { | |
| filter_map_function_id: Option<hir_def::FunctionId>, | |
| next_function_id: Option<hir_def::FunctionId>, | |
| prev_filter_map_expr_id: Option<ExprId>, | |
| prev_receiver_ty: Option<Ty<'db>>, | |
| } | |
| impl<'db> FilterMapNextChecker<'db> { | |
| fn new(lang_items: &'db LangItems, db: &'db dyn HirDatabase) -> Self { | |
| // Find and store the FunctionIds for Iterator::filter_map and Iterator::next | |
| let (next_function_id, filter_map_function_id) = match lang_items.IteratorNext { | |
| Some(next_function_id) => ( | |
| Some(next_function_id), | |
| match next_function_id.lookup(db).container { | |
| ItemContainerId::TraitId(iterator_trait_id) => { | |
| let iterator_trait_items = &iterator_trait_id.trait_items(db).items; | |
| iterator_trait_items.iter().find_map(|(name, it)| match it { | |
| &AssocItemId::FunctionId(id) if *name == sym::filter_map => Some(id), | |
| _ => None, | |
| }) | |
| } | |
| _ => None, | |
| }, | |
| ), | |
| None => (None, None), | |
| }; | |
| Self { | |
| filter_map_function_id, | |
| next_function_id, | |
| prev_filter_map_expr_id: None, | |
| prev_receiver_ty: 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? | |
| && let Some(prev_filter_map_expr_id) = self.prev_filter_map_expr_id | |
| { | |
| let is_dyn_trait = self | |
| .prev_receiver_ty | |
| .as_ref() | |
| .is_some_and(|it| it.strip_references().dyn_trait().is_some()); | |
| if *receiver_expr_id == prev_filter_map_expr_id && !is_dyn_trait { | |
| return Some(()); | |
| } | |
| } | |
| self.prev_filter_map_expr_id = None; | |
| None | |
| } | |
| } | |
| pub fn record_literal_missing_fields<'db>( | |
| db: &'db dyn HirDatabase, | |
| infer: &InferenceResult<'db>, | |
| id: ExprId, | |
| expr: &Expr, | |
| ) -> Option<(VariantId, Vec<LocalFieldId>)> { | |
| let (fields, spread) = match expr { | |
| Expr::RecordLit { fields, spread, .. } => (fields, spread), | |
| _ => return None, | |
| }; | |
| let variant_def = infer.variant_resolution_for_expr(id)?; | |
| if let VariantId::UnionId(_) = variant_def { | |
| return None; | |
| } | |
| let variant_data = variant_def.fields(db); | |
| let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect(); | |
| // don't show missing fields if: | |
| // - has ..expr | |
| // - or has default value + .. | |
| // - or already in code | |
| let missed_fields: Vec<LocalFieldId> = variant_data | |
| .fields() | |
| .iter() | |
| .filter_map(|(f, d)| { | |
| if specified_fields.contains(&d.name) | |
| || matches!(spread, RecordSpread::Expr(_)) | |
| || (d.default_value.is_some() && matches!(spread, RecordSpread::FieldDefaults)) | |
| { | |
| None | |
| } else { | |
| Some(f) | |
| } | |
| }) | |
| .collect(); | |
| if missed_fields.is_empty() { | |
| return None; | |
| } | |
| Some((variant_def, missed_fields)) | |
| } | |
| pub fn record_pattern_missing_fields<'db>( | |
| db: &'db dyn HirDatabase, | |
| infer: &InferenceResult<'db>, | |
| id: PatId, | |
| pat: &Pat, | |
| ) -> Option<(VariantId, Vec<LocalFieldId>)> { | |
| let (fields, ellipsis) = 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.fields(db); | |
| let specified_fields: FxHashSet<_> = fields.iter().map(|f| &f.name).collect(); | |
| // don't show missing fields if: | |
| // - in code | |
| // - or has .. | |
| let missed_fields: Vec<LocalFieldId> = variant_data | |
| .fields() | |
| .iter() | |
| .filter_map( | |
| |(f, d)| { | |
| if specified_fields.contains(&d.name) || ellipsis { None } else { Some(f) } | |
| }, | |
| ) | |
| .collect(); | |
| if missed_fields.is_empty() { | |
| return None; | |
| } | |
| Some((variant_def, missed_fields)) | |
| } | |
| 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.pat_has_type_mismatch(pat) { | |
| true => *has_type_mismatches = true, | |
| false if *has_type_mismatches => (), | |
| false => { | |
| let pat = &body[pat]; | |
| if let Pat::Lit(expr) = *pat { | |
| *has_type_mismatches |= infer.expr_has_type_mismatch(expr); | |
| if *has_type_mismatches { | |
| return; | |
| } | |
| } | |
| 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<'a, 'db>( | |
| cx: &MatchCheckCtx<'a, 'db>, | |
| scrut_ty: Ty<'a>, | |
| witnesses: Vec<WitnessPat<'a, 'db>>, | |
| arms_is_empty: bool, | |
| krate: Crate, | |
| ) -> String { | |
| struct DisplayWitness<'a, 'b, 'db>( | |
| &'a WitnessPat<'b, 'db>, | |
| &'a MatchCheckCtx<'b, 'db>, | |
| DisplayTarget, | |
| ); | |
| impl fmt::Display for DisplayWitness<'_, '_, '_> { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| let DisplayWitness(witness, cx, display_target) = *self; | |
| let pat = cx.hoist_witness_pat(witness); | |
| write!(f, "{}", pat.display(cx.db, display_target)) | |
| } | |
| } | |
| let non_empty_enum = match scrut_ty.as_adt() { | |
| Some((AdtId::EnumId(e), _)) => !e.enum_variants(cx.db).variants.is_empty(), | |
| _ => false, | |
| }; | |
| let display_target = DisplayTarget::from_crate(cx.db, krate); | |
| if arms_is_empty && !non_empty_enum { | |
| format!("type `{}` is non-empty", scrut_ty.display(cx.db, display_target)) | |
| } else { | |
| let pat_display = |witness| DisplayWitness(witness, cx, display_target); | |
| 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()) | |
| } | |
| } | |
| } | |
| } |