diff options
Diffstat (limited to 'compiler/rustc_borrowck/src/diagnostics/outlives_suggestion.rs')
-rw-r--r-- | compiler/rustc_borrowck/src/diagnostics/outlives_suggestion.rs | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/compiler/rustc_borrowck/src/diagnostics/outlives_suggestion.rs b/compiler/rustc_borrowck/src/diagnostics/outlives_suggestion.rs new file mode 100644 index 000000000..d359d7efb --- /dev/null +++ b/compiler/rustc_borrowck/src/diagnostics/outlives_suggestion.rs @@ -0,0 +1,261 @@ +//! Contains utilities for generating suggestions for borrowck errors related to unsatisfied +//! outlives constraints. + +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::Diagnostic; +use rustc_middle::ty::RegionVid; +use smallvec::SmallVec; +use std::collections::BTreeMap; +use tracing::debug; + +use crate::MirBorrowckCtxt; + +use super::{ErrorConstraintInfo, RegionName, RegionNameSource}; + +/// 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 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::NamedEarlyBoundRegion(..) + | RegionNameSource::NamedFreeRegion(..) + | 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 = FxHashSet::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) + .map(|r_outlived| r_outlived.as_slice().contains(fr)) + .unwrap_or(false) + }, + ); + + for (r, bound) in unified.into_iter() { + if !unified_already.contains(fr) { + suggested.push(SuggestedConstraint::Equal(fr_name.clone(), bound)); + unified_already.insert(r); + } + } + + if !other.is_empty() { + let other = + other.iter().map(|(_, rname)| rname.clone()).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 `Diagnostic` if the involved regions are + /// suggestable. + pub(crate) fn intermediate_suggestion( + &mut self, + mbcx: &MirBorrowckCtxt<'_, '_>, + errci: &ErrorConstraintInfo<'_>, + diag: &mut Diagnostic, + ) { + // 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 suggested.len() == 1 { + mbcx.infcx.tcx.sess.diagnostic().struct_help(&match suggested.last().unwrap() { + SuggestedConstraint::Outlives(a, bs) => { + let bs: SmallVec<[String; 2]> = bs.iter().map(|r| format!("{}", r)).collect(); + format!("add bound `{}: {}`", a, bs.join(" + ")) + } + + SuggestedConstraint::Equal(a, b) => { + format!("`{}` and `{}` must be the same: replace one with the other", a, b) + } + SuggestedConstraint::Static(a) => format!("replace `{}` with `'static`", a), + }) + } else { + // Create a new diagnostic. + let mut diag = mbcx + .infcx + .tcx + .sess + .diagnostic() + .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| format!("{}", r)).collect(); + diag.help(&format!("add bound `{}: {}`", a, bs.join(" + "))); + } + SuggestedConstraint::Equal(a, b) => { + diag.help(&format!( + "`{}` and `{}` must be the same: replace one with the other", + a, b + )); + } + SuggestedConstraint::Static(a) => { + diag.help(&format!("replace `{}` with `'static`", a)); + } + } + } + + 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(diag); + } +} |