blob: b61c37b061396139d974c99c5f668087dc67aba9 [file] [log] [blame]
//! Contains utilities for generating suggestions for borrowck errors related to unsatisified
//! outlives constraints.
use std::collections::BTreeMap;
use log::debug;
use rustc::{hir::def_id::DefId, infer::InferCtxt, ty::RegionVid};
use rustc::mir::{Body, Local};
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::{Diagnostic, DiagnosticBuilder};
use rustc_index::vec::IndexVec;
use syntax_pos::symbol::Symbol;
use smallvec::SmallVec;
use crate::borrow_check::region_infer::RegionInferenceContext;
use super::{
RegionName, RegionNameSource, ErrorConstraintInfo, ErrorReportingCtx, RegionErrorNamingCtx,
};
/// 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.
pub struct OutlivesSuggestionBuilder<'a> {
/// The MIR DefId of the fn with the lifetime error.
mir_def_id: DefId,
local_names: &'a IndexVec<Local, Option<Symbol>>,
/// 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<'a> {
/// Create a new builder for the given MIR node representing a fn definition.
crate fn new(
mir_def_id: DefId,
local_names: &'a IndexVec<Local, Option<Symbol>>,
) -> Self {
OutlivesSuggestionBuilder {
mir_def_id,
local_names,
constraints_to_add: BTreeMap::default(),
}
}
/// 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 unnamable
// regions.
RegionNameSource::SynthesizedFreeEnvRegion(..)
| RegionNameSource::CannotMatchHirTy(..)
| RegionNameSource::MatchedHirTy(..)
| RegionNameSource::MatchedAdtAndSegment(..)
| RegionNameSource::AnonRegionFromUpvar(..)
| RegionNameSource::AnonRegionFromOutput(..)
| RegionNameSource::AnonRegionFromYieldTy(..)
| RegionNameSource::AnonRegionFromAsyncFn(..) => {
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,
errctx: &ErrorReportingCtx<'_, '_, '_>,
renctx: &mut RegionErrorNamingCtx,
region: RegionVid,
) -> Option<RegionName> {
errctx
.region_infcx
.give_region_a_name(errctx, renctx, 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<'tcx>(
&self,
body: &Body<'tcx>,
region_infcx: &RegionInferenceContext<'tcx>,
infcx: &InferCtxt<'_, 'tcx>,
renctx: &mut RegionErrorNamingCtx,
) -> 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();
let errctx = ErrorReportingCtx {
region_infcx,
infcx,
body,
mir_def_id: self.mir_def_id,
local_names: self.local_names,
// We should not be suggesting naming upvars, so we pass in a dummy set of upvars that
// should never be used.
upvars: &[],
};
for (fr, outlived) in &self.constraints_to_add {
let fr_name = if let Some(fr_name) = self.region_vid_to_name(&errctx, renctx, *fr) {
fr_name
} 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(&errctx, renctx, *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)| {
if let RegionNameSource::Static = outlived_name.source {
true
} else {
false
}
}) {
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.
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_insert(Vec::new()).push(outlived_fr);
}
/// Emit an intermediate note on the given `Diagnostic` if the involved regions are
/// suggestable.
crate fn intermediate_suggestion(
&mut self,
errctx: &ErrorReportingCtx<'_, '_, '_>,
errci: &ErrorConstraintInfo,
renctx: &mut RegionErrorNamingCtx,
diag: &mut DiagnosticBuilder<'_>,
) {
// Emit an intermediate note.
let fr_name = self.region_vid_to_name(errctx, renctx, errci.fr);
let outlived_fr_name = self.region_vid_to_name(errctx, renctx, errci.outlived_fr);
if let (Some(fr_name), Some(outlived_fr_name)) = (fr_name, outlived_fr_name) {
if let RegionNameSource::Static = outlived_fr_name.source {
diag.help(&format!("consider replacing `{}` with `'static`", fr_name));
} else {
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.
crate fn add_suggestion<'tcx>(
&self,
body: &Body<'tcx>,
region_infcx: &RegionInferenceContext<'tcx>,
infcx: &InferCtxt<'_, 'tcx>,
errors_buffer: &mut Vec<Diagnostic>,
renctx: &mut RegionErrorNamingCtx,
) {
// 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 {
debug!("Only 1 suggestion. Skipping.");
return;
}
// Get all suggestable constraints.
let suggested = self.compile_all_suggestions(body, region_infcx, infcx, renctx);
// 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 {
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 = 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 = infcx.tcx.def_span(self.mir_def_id);
diag.sort_span = mir_span.shrink_to_hi();
// Buffer the diagnostic
diag.buffer(errors_buffer);
}
}