//! Diagnostics related methods for `Ty`. use std::fmt::Write; use std::ops::ControlFlow; use rustc_data_structures::fx::FxIndexMap; use rustc_errors::{ Applicability, Diag, DiagArgValue, IntoDiagArg, into_diag_arg_using_display, listify, pluralize, }; use rustc_hir::def::{DefKind, Namespace}; use rustc_hir::def_id::DefId; use rustc_hir::{self as hir, AmbigArg, LangItem, PredicateOrigin, WherePredicateKind}; use rustc_span::{BytePos, Span}; use rustc_type_ir::TyKind::*; use crate::ty::{ self, AliasTy, Const, ConstKind, FallibleTypeFolder, InferConst, InferTy, Instance, Opaque, PolyTraitPredicate, Projection, Ty, TyCtxt, TypeFoldable, TypeSuperFoldable, TypeSuperVisitable, TypeVisitable, TypeVisitor, }; impl IntoDiagArg for Ty<'_> { fn into_diag_arg(self, path: &mut Option) -> rustc_errors::DiagArgValue { ty::tls::with(|tcx| { let ty = tcx.short_string(self, path); DiagArgValue::Str(std::borrow::Cow::Owned(ty)) }) } } impl IntoDiagArg for Instance<'_> { fn into_diag_arg(self, path: &mut Option) -> rustc_errors::DiagArgValue { ty::tls::with(|tcx| { let instance = tcx.short_string_namespace(self, path, Namespace::ValueNS); DiagArgValue::Str(std::borrow::Cow::Owned(instance)) }) } } into_diag_arg_using_display! { ty::Region<'_>, } impl<'tcx> Ty<'tcx> { /// Similar to `Ty::is_primitive`, but also considers inferred numeric values to be primitive. pub fn is_primitive_ty(self) -> bool { matches!( self.kind(), Bool | Char | Str | Int(_) | Uint(_) | Float(_) | Infer( InferTy::IntVar(_) | InferTy::FloatVar(_) | InferTy::FreshIntTy(_) | InferTy::FreshFloatTy(_) ) ) } /// Whether the type is succinctly representable as a type instead of just referred to with a /// description in error messages. This is used in the main error message. pub fn is_simple_ty(self) -> bool { match self.kind() { Bool | Char | Str | Int(_) | Uint(_) | Float(_) | Infer( InferTy::IntVar(_) | InferTy::FloatVar(_) | InferTy::FreshIntTy(_) | InferTy::FreshFloatTy(_), ) => true, Ref(_, x, _) | Array(x, _) | Slice(x) => x.peel_refs().is_simple_ty(), Tuple(tys) if tys.is_empty() => true, _ => false, } } /// Whether the type is succinctly representable as a type instead of just referred to with a /// description in error messages. This is used in the primary span label. Beyond what /// `is_simple_ty` includes, it also accepts ADTs with no type arguments and references to /// ADTs with no type arguments. pub fn is_simple_text(self) -> bool { match self.kind() { Adt(_, args) => args.non_erasable_generics().next().is_none(), Ref(_, ty, _) => ty.is_simple_text(), _ => self.is_simple_ty(), } } } pub trait IsSuggestable<'tcx>: Sized { /// Whether this makes sense to suggest in a diagnostic. /// /// We filter out certain types and constants since they don't provide /// meaningful rendered suggestions when pretty-printed. We leave some /// nonsense, such as region vars, since those render as `'_` and are /// usually okay to reinterpret as elided lifetimes. /// /// Only if `infer_suggestable` is true, we consider type and const /// inference variables to be suggestable. fn is_suggestable(self, tcx: TyCtxt<'tcx>, infer_suggestable: bool) -> bool; fn make_suggestable( self, tcx: TyCtxt<'tcx>, infer_suggestable: bool, placeholder: Option>, ) -> Option; } impl<'tcx, T> IsSuggestable<'tcx> for T where T: TypeVisitable> + TypeFoldable>, { #[tracing::instrument(level = "debug", skip(tcx))] fn is_suggestable(self, tcx: TyCtxt<'tcx>, infer_suggestable: bool) -> bool { self.visit_with(&mut IsSuggestableVisitor { tcx, infer_suggestable }).is_continue() } fn make_suggestable( self, tcx: TyCtxt<'tcx>, infer_suggestable: bool, placeholder: Option>, ) -> Option { self.try_fold_with(&mut MakeSuggestableFolder { tcx, infer_suggestable, placeholder }).ok() } } pub fn suggest_arbitrary_trait_bound<'tcx>( tcx: TyCtxt<'tcx>, generics: &hir::Generics<'_>, err: &mut Diag<'_>, trait_pred: PolyTraitPredicate<'tcx>, associated_ty: Option<(&'static str, Ty<'tcx>)>, ) -> bool { if !trait_pred.is_suggestable(tcx, false) { return false; } let param_name = trait_pred.skip_binder().self_ty().to_string(); let mut constraint = trait_pred.to_string(); if let Some((name, term)) = associated_ty { // FIXME: this case overlaps with code in TyCtxt::note_and_explain_type_err. // That should be extracted into a helper function. if let Some(stripped) = constraint.strip_suffix('>') { constraint = format!("{stripped}, {name} = {term}>"); } else { constraint.push_str(&format!("<{name} = {term}>")); } } let param = generics.params.iter().find(|p| p.name.ident().as_str() == param_name); // Skip, there is a param named Self if param.is_some() && param_name == "Self" { return false; } // Suggest a where clause bound for a non-type parameter. err.span_suggestion_verbose( generics.tail_span_for_predicate_suggestion(), format!( "consider {} `where` clause, but there might be an alternative better way to express \ this requirement", if generics.where_clause_span.is_empty() { "introducing a" } else { "extending the" }, ), format!("{} {constraint}", generics.add_where_or_trailing_comma()), Applicability::MaybeIncorrect, ); true } #[derive(Debug, Clone, Copy)] enum SuggestChangingConstraintsMessage<'a> { RestrictBoundFurther, RestrictType { ty: &'a str }, RestrictTypeFurther { ty: &'a str }, RemoveMaybeUnsized, ReplaceMaybeUnsizedWithSized, } fn suggest_changing_unsized_bound( generics: &hir::Generics<'_>, suggestions: &mut Vec<(Span, String, String, SuggestChangingConstraintsMessage<'_>)>, param: &hir::GenericParam<'_>, def_id: Option, ) { // See if there's a `?Sized` bound that can be removed to suggest that. // First look at the `where` clause because we can have `where T: ?Sized`, // then look at params. for (where_pos, predicate) in generics.predicates.iter().enumerate() { let WherePredicateKind::BoundPredicate(predicate) = predicate.kind else { continue; }; if !predicate.is_param_bound(param.def_id.to_def_id()) { continue; }; let unsized_bounds = predicate .bounds .iter() .enumerate() .filter(|(_, bound)| { if let hir::GenericBound::Trait(poly) = bound && let hir::BoundPolarity::Maybe(_) = poly.modifiers.polarity && poly.trait_ref.trait_def_id() == def_id { true } else { false } }) .collect::>(); if unsized_bounds.is_empty() { continue; } let mut push_suggestion = |sp, msg| suggestions.push((sp, "Sized".to_string(), String::new(), msg)); if predicate.bounds.len() == unsized_bounds.len() { // All the bounds are unsized bounds, e.g. // `T: ?Sized + ?Sized` or `_: impl ?Sized + ?Sized`, // so in this case: // - if it's an impl trait predicate suggest changing the // the first bound to sized and removing the rest // - Otherwise simply suggest removing the entire predicate if predicate.origin == PredicateOrigin::ImplTrait { let first_bound = unsized_bounds[0].1; let first_bound_span = first_bound.span(); if first_bound_span.can_be_used_for_suggestions() { let question_span = first_bound_span.with_hi(first_bound_span.lo() + BytePos(1)); push_suggestion( question_span, SuggestChangingConstraintsMessage::ReplaceMaybeUnsizedWithSized, ); for (pos, _) in unsized_bounds.iter().skip(1) { let sp = generics.span_for_bound_removal(where_pos, *pos); push_suggestion(sp, SuggestChangingConstraintsMessage::RemoveMaybeUnsized); } } } else { let sp = generics.span_for_predicate_removal(where_pos); push_suggestion(sp, SuggestChangingConstraintsMessage::RemoveMaybeUnsized); } } else { // Some of the bounds are other than unsized. // So push separate removal suggestion for each unsized bound for (pos, _) in unsized_bounds { let sp = generics.span_for_bound_removal(where_pos, pos); push_suggestion(sp, SuggestChangingConstraintsMessage::RemoveMaybeUnsized); } } } } /// Suggest restricting a type param with a new bound. /// /// If `span_to_replace` is provided, then that span will be replaced with the /// `constraint`. If one wasn't provided, then the full bound will be suggested. pub fn suggest_constraining_type_param( tcx: TyCtxt<'_>, generics: &hir::Generics<'_>, err: &mut Diag<'_>, param_name: &str, constraint: &str, def_id: Option, span_to_replace: Option, ) -> bool { suggest_constraining_type_params( tcx, generics, err, [(param_name, constraint, def_id)].into_iter(), span_to_replace, ) } /// Suggest restricting a type param with a new bound. pub fn suggest_constraining_type_params<'a>( tcx: TyCtxt<'_>, generics: &hir::Generics<'_>, err: &mut Diag<'_>, param_names_and_constraints: impl Iterator)>, span_to_replace: Option, ) -> bool { let mut grouped = FxIndexMap::default(); let mut unstable_suggestion = false; param_names_and_constraints.for_each(|(param_name, constraint, def_id)| { let stable = match def_id { Some(def_id) => match tcx.lookup_stability(def_id) { Some(s) => s.level.is_stable(), None => true, }, None => true, }; if stable || tcx.sess.is_nightly_build() { grouped.entry(param_name).or_insert(Vec::new()).push(( constraint, def_id, if stable { "" } else { "unstable " }, )); if !stable { unstable_suggestion = true; } } }); let mut applicability = Applicability::MachineApplicable; let mut suggestions = Vec::new(); for (param_name, mut constraints) in grouped { let param = generics.params.iter().find(|p| p.name.ident().as_str() == param_name); let Some(param) = param else { return false }; { let mut sized_constraints = constraints.extract_if(.., |(_, def_id, _)| { def_id.is_some_and(|def_id| tcx.is_lang_item(def_id, LangItem::Sized)) }); if let Some((_, def_id, _)) = sized_constraints.next() { applicability = Applicability::MaybeIncorrect; err.span_label(param.span, "this type parameter needs to be `Sized`"); suggest_changing_unsized_bound(generics, &mut suggestions, param, def_id); } } let bound_message = if constraints.iter().any(|(_, def_id, _)| def_id.is_none()) { SuggestChangingConstraintsMessage::RestrictBoundFurther } else { SuggestChangingConstraintsMessage::RestrictTypeFurther { ty: param_name } }; // in the scenario like impl has stricter requirements than trait, // we should not suggest restrict bound on the impl, here we double check // the whether the param already has the constraint by checking `def_id` let bound_trait_defs: Vec = generics .bounds_for_param(param.def_id) .flat_map(|bound| { bound.bounds.iter().flat_map(|b| b.trait_ref().and_then(|t| t.trait_def_id())) }) .collect(); constraints .retain(|(_, def_id, _)| def_id.is_none_or(|def| !bound_trait_defs.contains(&def))); if constraints.is_empty() { continue; } let mut constraint = constraints.iter().map(|&(c, _, _)| c).collect::>(); constraint.sort(); constraint.dedup(); let all_known = constraints.iter().all(|&(_, def_id, _)| def_id.is_some()); let all_stable = constraints.iter().all(|&(_, _, stable)| stable.is_empty()); let all_unstable = constraints.iter().all(|&(_, _, stable)| !stable.is_empty()); let post = if all_stable || all_unstable { // Don't redundantly say "trait `X`, trait `Y`", instead "traits `X` and `Y`" let mut trait_names = constraints .iter() .map(|&(c, def_id, _)| match def_id { None => format!("`{c}`"), Some(def_id) => format!("`{}`", tcx.item_name(def_id)), }) .collect::>(); trait_names.sort(); trait_names.dedup(); let n = trait_names.len(); let stable = if all_stable { "" } else { "unstable " }; let trait_ = if all_known { format!("trait{}", pluralize!(n)) } else { String::new() }; let Some(trait_names) = listify(&trait_names, |n| n.to_string()) else { return false }; format!("{stable}{trait_} {trait_names}") } else { // We're more explicit when there's a mix of stable and unstable traits. let mut trait_names = constraints .iter() .map(|&(c, def_id, stable)| match def_id { None => format!("`{c}`"), Some(def_id) => format!("{stable}trait `{}`", tcx.item_name(def_id)), }) .collect::>(); trait_names.sort(); trait_names.dedup(); match listify(&trait_names, |t| t.to_string()) { Some(names) => names, None => return false, } }; let constraint = constraint.join(" + "); let mut suggest_restrict = |span, bound_list_non_empty, open_paren_sp| { let suggestion = if span_to_replace.is_some() { constraint.clone() } else if constraint.starts_with('<') { constraint.clone() } else if bound_list_non_empty { format!(" + {constraint}") } else { format!(" {constraint}") }; if let Some(open_paren_sp) = open_paren_sp { suggestions.push((open_paren_sp, post.clone(), "(".to_string(), bound_message)); suggestions.push((span, post.clone(), format!("){suggestion}"), bound_message)); } else { suggestions.push((span, post.clone(), suggestion, bound_message)); } }; if let Some(span) = span_to_replace { suggest_restrict(span, true, None); continue; } // When the type parameter has been provided bounds // // Message: // fn foo(t: T) where T: Foo { ... } // ^^^^^^ // | // help: consider further restricting this bound with `+ Bar` // // Suggestion: // fn foo(t: T) where T: Foo { ... } // ^ // | // replace with: ` + Bar` // // Or, if user has provided some bounds, suggest restricting them: // // fn foo(t: T) { ... } // --- // | // help: consider further restricting this bound with `+ Bar` // // Suggestion for tools in this case is: // // fn foo(t: T) { ... } // -- // | // replace with: `T: Bar +` if let Some((span, open_paren_sp)) = generics.bounds_span_for_suggestions(param.def_id) { suggest_restrict(span, true, open_paren_sp); continue; } if generics.has_where_clause_predicates { // This part is a bit tricky, because using the `where` clause user can // provide zero, one or many bounds for the same type parameter, so we // have following cases to consider: // // When the type parameter has been provided zero bounds // // Message: // fn foo(x: X, y: Y) where Y: Foo { ... } // - help: consider restricting this type parameter with `where X: Bar` // // Suggestion: // fn foo(x: X, y: Y) where Y: Foo { ... } // - insert: `, X: Bar` suggestions.push(( generics.tail_span_for_predicate_suggestion(), post, constraints.iter().fold(String::new(), |mut string, &(constraint, _, _)| { write!(string, ", {param_name}: {constraint}").unwrap(); string }), SuggestChangingConstraintsMessage::RestrictTypeFurther { ty: param_name }, )); continue; } // Additionally, there may be no `where` clause but the generic parameter has a default: // // Message: // trait Foo {... } // - help: consider further restricting this type parameter with `where T: Zar` // // Suggestion: // trait Foo {... } // - insert: `where T: Zar` if matches!(param.kind, hir::GenericParamKind::Type { default: Some(_), .. }) { // If we are here and the where clause span is of non-zero length // it means we're dealing with an empty where clause like this: // fn foo(x: X) where { ... } // In that case we don't want to add another "where" (Fixes #120838) let where_prefix = if generics.where_clause_span.is_empty() { " where" } else { "" }; // Suggest a bound, but there is no existing `where` clause *and* the type param has a // default (``), so we suggest adding `where T: Bar`. suggestions.push(( generics.tail_span_for_predicate_suggestion(), post, format!("{where_prefix} {param_name}: {constraint}"), SuggestChangingConstraintsMessage::RestrictTypeFurther { ty: param_name }, )); continue; } // If user has provided a colon, don't suggest adding another: // // fn foo(t: T) { ... } // - insert: consider restricting this type parameter with `T: Foo` if let Some(colon_span) = param.colon_span { suggestions.push(( colon_span.shrink_to_hi(), post, format!(" {constraint}"), SuggestChangingConstraintsMessage::RestrictType { ty: param_name }, )); continue; } // If user hasn't provided any bounds, suggest adding a new one: // // fn foo(t: T) { ... } // - help: consider restricting this type parameter with `T: Foo` suggestions.push(( param.span.shrink_to_hi(), post, format!(": {constraint}"), SuggestChangingConstraintsMessage::RestrictType { ty: param_name }, )); } // FIXME: remove the suggestions that are from derive, as the span is not correct suggestions = suggestions .into_iter() .filter(|(span, _, _, _)| !span.in_derive_expansion()) .collect::>(); let suggested = !suggestions.is_empty(); if suggestions.len() == 1 { let (span, post, suggestion, msg) = suggestions.pop().unwrap(); let msg = match msg { SuggestChangingConstraintsMessage::RestrictBoundFurther => { format!("consider further restricting this bound") } SuggestChangingConstraintsMessage::RestrictTypeFurther { ty } | SuggestChangingConstraintsMessage::RestrictType { ty } if ty.starts_with("impl ") => { format!("consider restricting opaque type `{ty}` with {post}") } SuggestChangingConstraintsMessage::RestrictType { ty } => { format!("consider restricting type parameter `{ty}` with {post}") } SuggestChangingConstraintsMessage::RestrictTypeFurther { ty } => { format!("consider further restricting type parameter `{ty}` with {post}") } SuggestChangingConstraintsMessage::RemoveMaybeUnsized => { format!("consider removing the `?Sized` bound to make the type parameter `Sized`") } SuggestChangingConstraintsMessage::ReplaceMaybeUnsizedWithSized => { format!("consider replacing `?Sized` with `Sized`") } }; err.span_suggestion_verbose(span, msg, suggestion, applicability); } else if suggestions.len() > 1 { let post = if unstable_suggestion { " (some of them are unstable traits)" } else { "" }; err.multipart_suggestion_verbose( format!("consider restricting type parameters{post}"), suggestions.into_iter().map(|(span, _, suggestion, _)| (span, suggestion)).collect(), applicability, ); } suggested } /// Collect al types that have an implicit `'static` obligation that we could suggest `'_` for. pub(crate) struct TraitObjectVisitor<'tcx>(pub(crate) Vec<&'tcx hir::Ty<'tcx>>); impl<'v> hir::intravisit::Visitor<'v> for TraitObjectVisitor<'v> { fn visit_ty(&mut self, ty: &'v hir::Ty<'v, AmbigArg>) { match ty.kind { hir::TyKind::TraitObject(_, tagged_ptr) if let hir::Lifetime { kind: hir::LifetimeKind::ImplicitObjectLifetimeDefault | hir::LifetimeKind::Static, .. } = tagged_ptr.pointer() => { self.0.push(ty.as_unambig_ty()) } hir::TyKind::OpaqueDef(..) => self.0.push(ty.as_unambig_ty()), _ => {} } hir::intravisit::walk_ty(self, ty); } } pub struct IsSuggestableVisitor<'tcx> { tcx: TyCtxt<'tcx>, infer_suggestable: bool, } impl<'tcx> TypeVisitor> for IsSuggestableVisitor<'tcx> { type Result = ControlFlow<()>; fn visit_ty(&mut self, t: Ty<'tcx>) -> Self::Result { match *t.kind() { Infer(InferTy::TyVar(_)) if self.infer_suggestable => {} FnDef(..) | Closure(..) | Infer(..) | Coroutine(..) | CoroutineWitness(..) | Bound(_, _) | Placeholder(_) | Error(_) => { return ControlFlow::Break(()); } Alias(Opaque, AliasTy { def_id, .. }) => { let parent = self.tcx.parent(def_id); let parent_ty = self.tcx.type_of(parent).instantiate_identity(); if let DefKind::TyAlias | DefKind::AssocTy = self.tcx.def_kind(parent) && let Alias(Opaque, AliasTy { def_id: parent_opaque_def_id, .. }) = *parent_ty.kind() && parent_opaque_def_id == def_id { // Okay } else { return ControlFlow::Break(()); } } Alias(Projection, AliasTy { def_id, .. }) if self.tcx.def_kind(def_id) != DefKind::AssocTy => { return ControlFlow::Break(()); } // FIXME: It would be nice to make this not use string manipulation, // but it's pretty hard to do this, since `ty::ParamTy` is missing // sufficient info to determine if it is synthetic, and we don't // always have a convenient way of getting `ty::Generics` at the call // sites we invoke `IsSuggestable::is_suggestable`. Param(param) if param.name.as_str().starts_with("impl ") => { return ControlFlow::Break(()); } _ => {} } t.super_visit_with(self) } fn visit_const(&mut self, c: Const<'tcx>) -> Self::Result { match c.kind() { ConstKind::Infer(InferConst::Var(_)) if self.infer_suggestable => {} ConstKind::Infer(..) | ConstKind::Bound(..) | ConstKind::Placeholder(..) | ConstKind::Error(..) => { return ControlFlow::Break(()); } _ => {} } c.super_visit_with(self) } } pub struct MakeSuggestableFolder<'tcx> { tcx: TyCtxt<'tcx>, infer_suggestable: bool, placeholder: Option>, } impl<'tcx> FallibleTypeFolder> for MakeSuggestableFolder<'tcx> { type Error = (); fn cx(&self) -> TyCtxt<'tcx> { self.tcx } fn try_fold_ty(&mut self, t: Ty<'tcx>) -> Result, Self::Error> { let t = match *t.kind() { Infer(InferTy::TyVar(_)) if self.infer_suggestable => t, FnDef(def_id, args) if self.placeholder.is_none() => { Ty::new_fn_ptr(self.tcx, self.tcx.fn_sig(def_id).instantiate(self.tcx, args)) } Closure(..) | FnDef(..) | Infer(..) | Coroutine(..) | CoroutineWitness(..) | Bound(_, _) | Placeholder(_) | Error(_) => { if let Some(placeholder) = self.placeholder { // We replace these with infer (which is passed in from an infcx). placeholder } else { return Err(()); } } Alias(Opaque, AliasTy { def_id, .. }) => { let parent = self.tcx.parent(def_id); let parent_ty = self.tcx.type_of(parent).instantiate_identity(); if let hir::def::DefKind::TyAlias | hir::def::DefKind::AssocTy = self.tcx.def_kind(parent) && let Alias(Opaque, AliasTy { def_id: parent_opaque_def_id, .. }) = *parent_ty.kind() && parent_opaque_def_id == def_id { t } else { return Err(()); } } // FIXME: It would be nice to make this not use string manipulation, // but it's pretty hard to do this, since `ty::ParamTy` is missing // sufficient info to determine if it is synthetic, and we don't // always have a convenient way of getting `ty::Generics` at the call // sites we invoke `IsSuggestable::is_suggestable`. Param(param) if param.name.as_str().starts_with("impl ") => { return Err(()); } _ => t, }; t.try_super_fold_with(self) } fn try_fold_const(&mut self, c: Const<'tcx>) -> Result, ()> { let c = match c.kind() { ConstKind::Infer(InferConst::Var(_)) if self.infer_suggestable => c, ConstKind::Infer(..) | ConstKind::Bound(..) | ConstKind::Placeholder(..) | ConstKind::Error(..) => { return Err(()); } _ => c, }; c.try_super_fold_with(self) } }