]> git.lizzy.rs Git - rust.git/blob - compiler/rustc_borrowck/src/diagnostics/outlives_suggestion.rs
Rollup merge of #106640 - lcnr:update-test, r=jackh726
[rust.git] / compiler / rustc_borrowck / src / diagnostics / outlives_suggestion.rs
1 //! Contains utilities for generating suggestions for borrowck errors related to unsatisfied
2 //! outlives constraints.
3
4 use rustc_data_structures::fx::FxHashSet;
5 use rustc_errors::Diagnostic;
6 use rustc_middle::ty::RegionVid;
7 use smallvec::SmallVec;
8 use std::collections::BTreeMap;
9
10 use crate::MirBorrowckCtxt;
11
12 use super::{ErrorConstraintInfo, RegionName, RegionNameSource};
13
14 /// The different things we could suggest.
15 enum SuggestedConstraint {
16     /// Outlives(a, [b, c, d, ...]) => 'a: 'b + 'c + 'd + ...
17     Outlives(RegionName, SmallVec<[RegionName; 2]>),
18
19     /// 'a = 'b
20     Equal(RegionName, RegionName),
21
22     /// 'a: 'static i.e. 'a = 'static and the user should just use 'static
23     Static(RegionName),
24 }
25
26 /// Collects information about outlives constraints that needed to be added for a given MIR node
27 /// corresponding to a function definition.
28 ///
29 /// Adds a help note suggesting adding a where clause with the needed constraints.
30 #[derive(Default)]
31 pub struct OutlivesSuggestionBuilder {
32     /// The list of outlives constraints that need to be added. Specifically, we map each free
33     /// region to all other regions that it must outlive. I will use the shorthand `fr:
34     /// outlived_frs`. Not all of these regions will already have names necessarily. Some could be
35     /// implicit free regions that we inferred. These will need to be given names in the final
36     /// suggestion message.
37     constraints_to_add: BTreeMap<RegionVid, Vec<RegionVid>>,
38 }
39
40 impl OutlivesSuggestionBuilder {
41     /// Returns `true` iff the `RegionNameSource` is a valid source for an outlives
42     /// suggestion.
43     //
44     // FIXME: Currently, we only report suggestions if the `RegionNameSource` is an early-bound
45     // region or a named region, avoiding using regions with synthetic names altogether. This
46     // allows us to avoid giving impossible suggestions (e.g. adding bounds to closure args).
47     // We can probably be less conservative, since some inferred free regions are namable (e.g.
48     // the user can explicitly name them. To do this, we would allow some regions whose names
49     // come from `MatchedAdtAndSegment`, being careful to filter out bad suggestions, such as
50     // naming the `'self` lifetime in methods, etc.
51     fn region_name_is_suggestable(name: &RegionName) -> bool {
52         match name.source {
53             RegionNameSource::NamedEarlyBoundRegion(..)
54             | RegionNameSource::NamedFreeRegion(..)
55             | RegionNameSource::Static => true,
56
57             // Don't give suggestions for upvars, closure return types, or other unnameable
58             // regions.
59             RegionNameSource::SynthesizedFreeEnvRegion(..)
60             | RegionNameSource::AnonRegionFromArgument(..)
61             | RegionNameSource::AnonRegionFromUpvar(..)
62             | RegionNameSource::AnonRegionFromOutput(..)
63             | RegionNameSource::AnonRegionFromYieldTy(..)
64             | RegionNameSource::AnonRegionFromAsyncFn(..)
65             | RegionNameSource::AnonRegionFromImplSignature(..) => {
66                 debug!("Region {:?} is NOT suggestable", name);
67                 false
68             }
69         }
70     }
71
72     /// Returns a name for the region if it is suggestable. See `region_name_is_suggestable`.
73     fn region_vid_to_name(
74         &self,
75         mbcx: &MirBorrowckCtxt<'_, '_>,
76         region: RegionVid,
77     ) -> Option<RegionName> {
78         mbcx.give_region_a_name(region).filter(Self::region_name_is_suggestable)
79     }
80
81     /// Compiles a list of all suggestions to be printed in the final big suggestion.
82     fn compile_all_suggestions(
83         &self,
84         mbcx: &MirBorrowckCtxt<'_, '_>,
85     ) -> SmallVec<[SuggestedConstraint; 2]> {
86         let mut suggested = SmallVec::new();
87
88         // Keep track of variables that we have already suggested unifying so that we don't print
89         // out silly duplicate messages.
90         let mut unified_already = FxHashSet::default();
91
92         for (fr, outlived) in &self.constraints_to_add {
93             let Some(fr_name) = self.region_vid_to_name(mbcx, *fr) else {
94                 continue;
95             };
96
97             let outlived = outlived
98                 .iter()
99                 // if there is a `None`, we will just omit that constraint
100                 .filter_map(|fr| self.region_vid_to_name(mbcx, *fr).map(|rname| (fr, rname)))
101                 .collect::<Vec<_>>();
102
103             // No suggestable outlived lifetimes.
104             if outlived.is_empty() {
105                 continue;
106             }
107
108             // There are three types of suggestions we can make:
109             // 1) Suggest a bound: 'a: 'b
110             // 2) Suggest replacing 'a with 'static. If any of `outlived` is `'static`, then we
111             //    should just replace 'a with 'static.
112             // 3) Suggest unifying 'a with 'b if we have both 'a: 'b and 'b: 'a
113
114             if outlived
115                 .iter()
116                 .any(|(_, outlived_name)| matches!(outlived_name.source, RegionNameSource::Static))
117             {
118                 suggested.push(SuggestedConstraint::Static(fr_name));
119             } else {
120                 // We want to isolate out all lifetimes that should be unified and print out
121                 // separate messages for them.
122
123                 let (unified, other): (Vec<_>, Vec<_>) = outlived.into_iter().partition(
124                     // Do we have both 'fr: 'r and 'r: 'fr?
125                     |(r, _)| {
126                         self.constraints_to_add
127                             .get(r)
128                             .map(|r_outlived| r_outlived.as_slice().contains(fr))
129                             .unwrap_or(false)
130                     },
131                 );
132
133                 for (r, bound) in unified.into_iter() {
134                     if !unified_already.contains(fr) {
135                         suggested.push(SuggestedConstraint::Equal(fr_name.clone(), bound));
136                         unified_already.insert(r);
137                     }
138                 }
139
140                 if !other.is_empty() {
141                     let other =
142                         other.iter().map(|(_, rname)| rname.clone()).collect::<SmallVec<_>>();
143                     suggested.push(SuggestedConstraint::Outlives(fr_name, other))
144                 }
145             }
146         }
147
148         suggested
149     }
150
151     /// Add the outlives constraint `fr: outlived_fr` to the set of constraints we need to suggest.
152     pub(crate) fn collect_constraint(&mut self, fr: RegionVid, outlived_fr: RegionVid) {
153         debug!("Collected {:?}: {:?}", fr, outlived_fr);
154
155         // Add to set of constraints for final help note.
156         self.constraints_to_add.entry(fr).or_default().push(outlived_fr);
157     }
158
159     /// Emit an intermediate note on the given `Diagnostic` if the involved regions are
160     /// suggestable.
161     pub(crate) fn intermediate_suggestion(
162         &mut self,
163         mbcx: &MirBorrowckCtxt<'_, '_>,
164         errci: &ErrorConstraintInfo<'_>,
165         diag: &mut Diagnostic,
166     ) {
167         // Emit an intermediate note.
168         let fr_name = self.region_vid_to_name(mbcx, errci.fr);
169         let outlived_fr_name = self.region_vid_to_name(mbcx, errci.outlived_fr);
170
171         if let (Some(fr_name), Some(outlived_fr_name)) = (fr_name, outlived_fr_name)
172             && !matches!(outlived_fr_name.source, RegionNameSource::Static)
173         {
174             diag.help(&format!(
175                 "consider adding the following bound: `{fr_name}: {outlived_fr_name}`",
176             ));
177         }
178     }
179
180     /// If there is a suggestion to emit, add a diagnostic to the buffer. This is the final
181     /// suggestion including all collected constraints.
182     pub(crate) fn add_suggestion(&self, mbcx: &mut MirBorrowckCtxt<'_, '_>) {
183         // No constraints to add? Done.
184         if self.constraints_to_add.is_empty() {
185             debug!("No constraints to suggest.");
186             return;
187         }
188
189         // If there is only one constraint to suggest, then we already suggested it in the
190         // intermediate suggestion above.
191         if self.constraints_to_add.len() == 1
192             && self.constraints_to_add.values().next().unwrap().len() == 1
193         {
194             debug!("Only 1 suggestion. Skipping.");
195             return;
196         }
197
198         // Get all suggestable constraints.
199         let suggested = self.compile_all_suggestions(mbcx);
200
201         // If there are no suggestable constraints...
202         if suggested.is_empty() {
203             debug!("Only 1 suggestable constraint. Skipping.");
204             return;
205         }
206
207         // If there is exactly one suggestable constraints, then just suggest it. Otherwise, emit a
208         // list of diagnostics.
209         let mut diag = if suggested.len() == 1 {
210             mbcx.infcx.tcx.sess.diagnostic().struct_help(&match suggested.last().unwrap() {
211                 SuggestedConstraint::Outlives(a, bs) => {
212                     let bs: SmallVec<[String; 2]> = bs.iter().map(|r| r.to_string()).collect();
213                     format!("add bound `{a}: {}`", bs.join(" + "))
214                 }
215
216                 SuggestedConstraint::Equal(a, b) => {
217                     format!("`{a}` and `{b}` must be the same: replace one with the other")
218                 }
219                 SuggestedConstraint::Static(a) => format!("replace `{a}` with `'static`"),
220             })
221         } else {
222             // Create a new diagnostic.
223             let mut diag = mbcx
224                 .infcx
225                 .tcx
226                 .sess
227                 .diagnostic()
228                 .struct_help("the following changes may resolve your lifetime errors");
229
230             // Add suggestions.
231             for constraint in suggested {
232                 match constraint {
233                     SuggestedConstraint::Outlives(a, bs) => {
234                         let bs: SmallVec<[String; 2]> = bs.iter().map(|r| r.to_string()).collect();
235                         diag.help(&format!("add bound `{a}: {}`", bs.join(" + ")));
236                     }
237                     SuggestedConstraint::Equal(a, b) => {
238                         diag.help(&format!(
239                             "`{a}` and `{b}` must be the same: replace one with the other",
240                         ));
241                     }
242                     SuggestedConstraint::Static(a) => {
243                         diag.help(&format!("replace `{a}` with `'static`"));
244                     }
245                 }
246             }
247
248             diag
249         };
250
251         // We want this message to appear after other messages on the mir def.
252         let mir_span = mbcx.body.span;
253         diag.sort_span = mir_span.shrink_to_hi();
254
255         // Buffer the diagnostic
256         mbcx.buffer_non_error_diag(diag);
257     }
258 }