| //! Contains utilities for generating suggestions for borrowck errors related to unsatisfied |
| //! outlives constraints. |
| |
| #![allow(rustc::diagnostic_outside_of_impl)] |
| #![allow(rustc::untranslatable_diagnostic)] |
| |
| use std::collections::BTreeMap; |
| |
| use rustc_data_structures::fx::FxIndexSet; |
| use rustc_errors::Diag; |
| use rustc_middle::ty::RegionVid; |
| use smallvec::SmallVec; |
| use tracing::debug; |
| |
| use super::{ErrorConstraintInfo, RegionName, RegionNameSource}; |
| use crate::MirBorrowckCtxt; |
| |
| /// The different things we could suggest. |
| enum SuggestedConstraint { |
| /// Outlives(a, [b, c, d, ...]) => 'a: 'b + 'c + 'd + ... |
| Outlives(RegionName, SmallVec<[RegionName; 2]>), |
| |
| /// 'a = 'b |
| Equal(RegionName, RegionName), |
| |
| /// 'a: 'static i.e. 'a = 'static and the user should just use 'static |
| Static(RegionName), |
| } |
| |
| /// Collects information about outlives constraints that needed to be added for a given MIR node |
| /// corresponding to a function definition. |
| /// |
| /// Adds a help note suggesting adding a where clause with the needed constraints. |
| #[derive(Default)] |
| pub(crate) struct OutlivesSuggestionBuilder { |
| /// The list of outlives constraints that need to be added. Specifically, we map each free |
| /// region to all other regions that it must outlive. I will use the shorthand `fr: |
| /// outlived_frs`. Not all of these regions will already have names necessarily. Some could be |
| /// implicit free regions that we inferred. These will need to be given names in the final |
| /// suggestion message. |
| constraints_to_add: BTreeMap<RegionVid, Vec<RegionVid>>, |
| } |
| |
| impl OutlivesSuggestionBuilder { |
| /// Returns `true` iff the `RegionNameSource` is a valid source for an outlives |
| /// suggestion. |
| // |
| // FIXME: Currently, we only report suggestions if the `RegionNameSource` is an early-bound |
| // region or a named region, avoiding using regions with synthetic names altogether. This |
| // allows us to avoid giving impossible suggestions (e.g. adding bounds to closure args). |
| // We can probably be less conservative, since some inferred free regions are namable (e.g. |
| // the user can explicitly name them. To do this, we would allow some regions whose names |
| // come from `MatchedAdtAndSegment`, being careful to filter out bad suggestions, such as |
| // naming the `'self` lifetime in methods, etc. |
| fn region_name_is_suggestable(name: &RegionName) -> bool { |
| match name.source { |
| RegionNameSource::NamedEarlyParamRegion(..) |
| | RegionNameSource::NamedLateParamRegion(..) |
| | RegionNameSource::Static => true, |
| |
| // Don't give suggestions for upvars, closure return types, or other unnameable |
| // regions. |
| RegionNameSource::SynthesizedFreeEnvRegion(..) |
| | RegionNameSource::AnonRegionFromArgument(..) |
| | RegionNameSource::AnonRegionFromUpvar(..) |
| | RegionNameSource::AnonRegionFromOutput(..) |
| | RegionNameSource::AnonRegionFromYieldTy(..) |
| | RegionNameSource::AnonRegionFromAsyncFn(..) |
| | RegionNameSource::AnonRegionFromImplSignature(..) => { |
| debug!("Region {:?} is NOT suggestable", name); |
| false |
| } |
| } |
| } |
| |
| /// Returns a name for the region if it is suggestable. See `region_name_is_suggestable`. |
| fn region_vid_to_name( |
| &self, |
| mbcx: &MirBorrowckCtxt<'_, '_, '_>, |
| region: RegionVid, |
| ) -> Option<RegionName> { |
| mbcx.give_region_a_name(region).filter(Self::region_name_is_suggestable) |
| } |
| |
| /// Compiles a list of all suggestions to be printed in the final big suggestion. |
| fn compile_all_suggestions( |
| &self, |
| mbcx: &MirBorrowckCtxt<'_, '_, '_>, |
| ) -> SmallVec<[SuggestedConstraint; 2]> { |
| let mut suggested = SmallVec::new(); |
| |
| // Keep track of variables that we have already suggested unifying so that we don't print |
| // out silly duplicate messages. |
| let mut unified_already = FxIndexSet::default(); |
| |
| for (fr, outlived) in &self.constraints_to_add { |
| let Some(fr_name) = self.region_vid_to_name(mbcx, *fr) else { |
| continue; |
| }; |
| |
| let outlived = outlived |
| .iter() |
| // if there is a `None`, we will just omit that constraint |
| .filter_map(|fr| self.region_vid_to_name(mbcx, *fr).map(|rname| (fr, rname))) |
| .collect::<Vec<_>>(); |
| |
| // No suggestable outlived lifetimes. |
| if outlived.is_empty() { |
| continue; |
| } |
| |
| // There are three types of suggestions we can make: |
| // 1) Suggest a bound: 'a: 'b |
| // 2) Suggest replacing 'a with 'static. If any of `outlived` is `'static`, then we |
| // should just replace 'a with 'static. |
| // 3) Suggest unifying 'a with 'b if we have both 'a: 'b and 'b: 'a |
| |
| if outlived |
| .iter() |
| .any(|(_, outlived_name)| matches!(outlived_name.source, RegionNameSource::Static)) |
| { |
| suggested.push(SuggestedConstraint::Static(fr_name)); |
| } else { |
| // We want to isolate out all lifetimes that should be unified and print out |
| // separate messages for them. |
| |
| let (unified, other): (Vec<_>, Vec<_>) = outlived.into_iter().partition( |
| // Do we have both 'fr: 'r and 'r: 'fr? |
| |(r, _)| { |
| self.constraints_to_add |
| .get(r) |
| .is_some_and(|r_outlived| r_outlived.as_slice().contains(fr)) |
| }, |
| ); |
| |
| for (r, bound) in unified.into_iter() { |
| if !unified_already.contains(fr) { |
| suggested.push(SuggestedConstraint::Equal(fr_name, bound)); |
| unified_already.insert(r); |
| } |
| } |
| |
| if !other.is_empty() { |
| let other = other.iter().map(|(_, rname)| *rname).collect::<SmallVec<_>>(); |
| suggested.push(SuggestedConstraint::Outlives(fr_name, other)) |
| } |
| } |
| } |
| |
| suggested |
| } |
| |
| /// Add the outlives constraint `fr: outlived_fr` to the set of constraints we need to suggest. |
| pub(crate) fn collect_constraint(&mut self, fr: RegionVid, outlived_fr: RegionVid) { |
| debug!("Collected {:?}: {:?}", fr, outlived_fr); |
| |
| // Add to set of constraints for final help note. |
| self.constraints_to_add.entry(fr).or_default().push(outlived_fr); |
| } |
| |
| /// Emit an intermediate note on the given `Diag` if the involved regions are suggestable. |
| pub(crate) fn intermediate_suggestion( |
| &mut self, |
| mbcx: &MirBorrowckCtxt<'_, '_, '_>, |
| errci: &ErrorConstraintInfo<'_>, |
| diag: &mut Diag<'_>, |
| ) { |
| // Emit an intermediate note. |
| let fr_name = self.region_vid_to_name(mbcx, errci.fr); |
| let outlived_fr_name = self.region_vid_to_name(mbcx, errci.outlived_fr); |
| |
| if let (Some(fr_name), Some(outlived_fr_name)) = (fr_name, outlived_fr_name) |
| && !matches!(outlived_fr_name.source, RegionNameSource::Static) |
| { |
| diag.help(format!( |
| "consider adding the following bound: `{fr_name}: {outlived_fr_name}`", |
| )); |
| } |
| } |
| |
| /// If there is a suggestion to emit, add a diagnostic to the buffer. This is the final |
| /// suggestion including all collected constraints. |
| pub(crate) fn add_suggestion(&self, mbcx: &mut MirBorrowckCtxt<'_, '_, '_>) { |
| // No constraints to add? Done. |
| if self.constraints_to_add.is_empty() { |
| debug!("No constraints to suggest."); |
| return; |
| } |
| |
| // If there is only one constraint to suggest, then we already suggested it in the |
| // intermediate suggestion above. |
| if self.constraints_to_add.len() == 1 |
| && self.constraints_to_add.values().next().unwrap().len() == 1 |
| { |
| debug!("Only 1 suggestion. Skipping."); |
| return; |
| } |
| |
| // Get all suggestable constraints. |
| let suggested = self.compile_all_suggestions(mbcx); |
| |
| // If there are no suggestable constraints... |
| if suggested.is_empty() { |
| debug!("Only 1 suggestable constraint. Skipping."); |
| return; |
| } |
| |
| // If there is exactly one suggestable constraints, then just suggest it. Otherwise, emit a |
| // list of diagnostics. |
| let mut diag = if let [constraint] = suggested.as_slice() { |
| mbcx.dcx().struct_help(match constraint { |
| SuggestedConstraint::Outlives(a, bs) => { |
| let bs: SmallVec<[String; 2]> = bs.iter().map(|r| r.to_string()).collect(); |
| format!("add bound `{a}: {}`", bs.join(" + ")) |
| } |
| |
| SuggestedConstraint::Equal(a, b) => { |
| format!("`{a}` and `{b}` must be the same: replace one with the other") |
| } |
| SuggestedConstraint::Static(a) => format!("replace `{a}` with `'static`"), |
| }) |
| } else { |
| // Create a new diagnostic. |
| let mut diag = mbcx |
| .infcx |
| .tcx |
| .dcx() |
| .struct_help("the following changes may resolve your lifetime errors"); |
| |
| // Add suggestions. |
| for constraint in suggested { |
| match constraint { |
| SuggestedConstraint::Outlives(a, bs) => { |
| let bs: SmallVec<[String; 2]> = bs.iter().map(|r| r.to_string()).collect(); |
| diag.help(format!("add bound `{a}: {}`", bs.join(" + "))); |
| } |
| SuggestedConstraint::Equal(a, b) => { |
| diag.help(format!( |
| "`{a}` and `{b}` must be the same: replace one with the other", |
| )); |
| } |
| SuggestedConstraint::Static(a) => { |
| diag.help(format!("replace `{a}` with `'static`")); |
| } |
| } |
| } |
| |
| diag |
| }; |
| |
| // We want this message to appear after other messages on the mir def. |
| let mir_span = mbcx.body.span; |
| diag.sort_span = mir_span.shrink_to_hi(); |
| |
| // Buffer the diagnostic |
| mbcx.buffer_non_error(diag); |
| } |
| } |