|
| 1 | +use rustc_data_structures::fx::FxIndexMap; |
| 2 | +use rustc_hir::intravisit::{self, Visitor}; |
| 3 | +use rustc_hir::{self as hir, LifetimeSource, LifetimeSyntax}; |
| 4 | +use rustc_session::{declare_lint, declare_lint_pass}; |
| 5 | +use rustc_span::Span; |
| 6 | +use tracing::instrument; |
| 7 | + |
| 8 | +use crate::{LateContext, LateLintPass, LintContext, lints}; |
| 9 | + |
| 10 | +declare_lint! { |
| 11 | + /// The `mismatched_lifetime_syntaxes` lint detects when an |
| 12 | + /// elided lifetime uses different syntax between function |
| 13 | + /// arguments and return values. |
| 14 | + /// |
| 15 | + /// The three kinds of syntax are: |
| 16 | + /// 1. Named lifetimes, such as `'static` or `'a`. |
| 17 | + /// 2. The anonymous lifetime `'_`. |
| 18 | + /// 3. Hidden lifetimes, such as `&u8` or `ThisHasALifetimeGeneric`. |
| 19 | + /// |
| 20 | + /// As an exception, this lint allows references with hidden or |
| 21 | + /// anonymous lifetimes to be paired with paths using anonymous |
| 22 | + /// lifetimes. |
| 23 | + /// |
| 24 | + /// ### Example |
| 25 | + /// |
| 26 | + /// ```rust,compile_fail |
| 27 | + /// #![deny(mismatched_lifetime_syntaxes)] |
| 28 | + /// |
| 29 | + /// pub fn mixing_named_with_hidden(v: &'static u8) -> &u8 { |
| 30 | + /// v |
| 31 | + /// } |
| 32 | + /// |
| 33 | + /// struct Person<'a> { |
| 34 | + /// name: &'a str, |
| 35 | + /// } |
| 36 | + /// |
| 37 | + /// pub fn mixing_hidden_with_anonymous(v: Person) -> Person<'_> { |
| 38 | + /// v |
| 39 | + /// } |
| 40 | + /// |
| 41 | + /// struct Foo; |
| 42 | + /// |
| 43 | + /// impl Foo { |
| 44 | + /// // Lifetime elision results in the output lifetime becoming |
| 45 | + /// // `'static`, which is not what was intended. |
| 46 | + /// pub fn get_mut(&'static self, x: &mut u8) -> &mut u8 { |
| 47 | + /// unsafe { &mut *(x as *mut _) } |
| 48 | + /// } |
| 49 | + /// } |
| 50 | + /// ``` |
| 51 | + /// |
| 52 | + /// {{produces}} |
| 53 | + /// |
| 54 | + /// ### Explanation |
| 55 | + /// |
| 56 | + /// Lifetime elision is useful because it frees you from having to |
| 57 | + /// give each lifetime its own name and explicitly show the |
| 58 | + /// relation of input and output lifetimes for common |
| 59 | + /// cases. However, a lifetime that uses inconsistent syntax |
| 60 | + /// between related arguments and return values is more confusing. |
| 61 | + /// |
| 62 | + /// In certain `unsafe` code, lifetime elision combined with |
| 63 | + /// inconsistent lifetime syntax may result in unsound code. |
| 64 | + pub MISMATCHED_LIFETIME_SYNTAXES, |
| 65 | + Allow, |
| 66 | + "detects when an elided lifetime uses different syntax between arguments and return values" |
| 67 | +} |
| 68 | + |
| 69 | +declare_lint_pass!(LifetimeStyle => [MISMATCHED_LIFETIME_SYNTAXES]); |
| 70 | + |
| 71 | +impl<'tcx> LateLintPass<'tcx> for LifetimeStyle { |
| 72 | + #[instrument(skip_all)] |
| 73 | + fn check_fn( |
| 74 | + &mut self, |
| 75 | + cx: &LateContext<'tcx>, |
| 76 | + _: hir::intravisit::FnKind<'tcx>, |
| 77 | + fd: &'tcx hir::FnDecl<'tcx>, |
| 78 | + _: &'tcx hir::Body<'tcx>, |
| 79 | + _: rustc_span::Span, |
| 80 | + _: rustc_span::def_id::LocalDefId, |
| 81 | + ) { |
| 82 | + let mut input_map = Default::default(); |
| 83 | + let mut output_map = Default::default(); |
| 84 | + |
| 85 | + for input in fd.inputs { |
| 86 | + LifetimeInfoCollector::collect(input, &mut input_map); |
| 87 | + } |
| 88 | + |
| 89 | + if let hir::FnRetTy::Return(output) = fd.output { |
| 90 | + LifetimeInfoCollector::collect(output, &mut output_map); |
| 91 | + } |
| 92 | + |
| 93 | + report_mismatches(cx, &input_map, &output_map); |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +#[instrument(skip_all)] |
| 98 | +fn report_mismatches<'tcx>( |
| 99 | + cx: &LateContext<'tcx>, |
| 100 | + inputs: &LifetimeInfoMap<'tcx>, |
| 101 | + outputs: &LifetimeInfoMap<'tcx>, |
| 102 | +) { |
| 103 | + for (resolved_lifetime, output_info) in outputs { |
| 104 | + if let Some(input_info) = inputs.get(resolved_lifetime) { |
| 105 | + let relevant_lifetimes = input_info.iter().chain(output_info); |
| 106 | + |
| 107 | + // Categorize lifetimes into source/syntax buckets |
| 108 | + let mut hidden = Bucket::default(); |
| 109 | + let mut anonymous = Bucket::default(); |
| 110 | + let mut named = Bucket::default(); |
| 111 | + |
| 112 | + for info in relevant_lifetimes { |
| 113 | + use LifetimeSource::*; |
| 114 | + use LifetimeSyntax::*; |
| 115 | + |
| 116 | + let bucket = match info.lifetime.syntax { |
| 117 | + Hidden => &mut hidden, |
| 118 | + Anonymous => &mut anonymous, |
| 119 | + Named => &mut named, |
| 120 | + }; |
| 121 | + |
| 122 | + match info.lifetime.source { |
| 123 | + Reference | OutlivesBound | PreciseCapturing => bucket.n_ref += 1, |
| 124 | + Path { .. } => bucket.n_path += 1, |
| 125 | + Other => {} |
| 126 | + } |
| 127 | + |
| 128 | + bucket.members.push(info); |
| 129 | + } |
| 130 | + |
| 131 | + // Check if syntaxes are consistent |
| 132 | + |
| 133 | + let syntax_counts = ( |
| 134 | + hidden.n_ref, |
| 135 | + anonymous.n_ref, |
| 136 | + named.n_ref, |
| 137 | + hidden.n_path, |
| 138 | + anonymous.n_path, |
| 139 | + named.n_path, |
| 140 | + ); |
| 141 | + |
| 142 | + match syntax_counts { |
| 143 | + // The lifetimes are all one syntax |
| 144 | + (_, 0, 0, _, 0, 0) | (0, _, 0, 0, _, 0) | (0, 0, _, 0, 0, _) => continue, |
| 145 | + |
| 146 | + // Hidden references, anonymous references, and anonymous paths can call be mixed. |
| 147 | + (_, _, 0, 0, _, 0) => continue, |
| 148 | + |
| 149 | + _ => (), |
| 150 | + } |
| 151 | + |
| 152 | + let inputs = input_info.iter().map(|info| info.reporting_span()).collect(); |
| 153 | + let outputs = output_info.iter().map(|info| info.reporting_span()).collect(); |
| 154 | + |
| 155 | + // There can only ever be zero or one named lifetime |
| 156 | + // for a given lifetime resolution. |
| 157 | + let named_lifetime = named.members.first(); |
| 158 | + |
| 159 | + let named_suggestion = named_lifetime.map(|info| { |
| 160 | + build_mismatch_suggestion(info.lifetime_name(), &[&hidden, &anonymous]) |
| 161 | + }); |
| 162 | + |
| 163 | + let is_named_static = named_lifetime.is_some_and(|info| info.is_static()); |
| 164 | + |
| 165 | + let should_suggest_hidden = !hidden.members.is_empty() && !is_named_static; |
| 166 | + |
| 167 | + // FIXME: remove comma and space from paths with multiple generics |
| 168 | + // FIXME: remove angle brackets from paths when no more generics |
| 169 | + // FIXME: remove space after lifetime from references |
| 170 | + // FIXME: remove lifetime from function declaration |
| 171 | + let hidden_suggestion = should_suggest_hidden.then(|| { |
| 172 | + let suggestions = [&anonymous, &named] |
| 173 | + .into_iter() |
| 174 | + .flat_map(|b| &b.members) |
| 175 | + .map(|i| i.suggestion("'dummy").0) |
| 176 | + .collect(); |
| 177 | + |
| 178 | + lints::MismatchedLifetimeSyntaxesSuggestion::Hidden { |
| 179 | + suggestions, |
| 180 | + tool_only: false, |
| 181 | + } |
| 182 | + }); |
| 183 | + |
| 184 | + let should_suggest_anonymous = !anonymous.members.is_empty() && !is_named_static; |
| 185 | + |
| 186 | + let anonymous_suggestion = should_suggest_anonymous |
| 187 | + .then(|| build_mismatch_suggestion("'_", &[&hidden, &named])); |
| 188 | + |
| 189 | + let lifetime_name = |
| 190 | + named_lifetime.map(|info| info.lifetime_name()).unwrap_or("'_").to_owned(); |
| 191 | + |
| 192 | + // We can produce a number of suggestions which may overwhelm |
| 193 | + // the user. Instead, we order the suggestions based on Rust |
| 194 | + // idioms. The "best" choice is shown to the user and the |
| 195 | + // remaining choices are shown to tools only. |
| 196 | + let mut suggestions = Vec::new(); |
| 197 | + suggestions.extend(named_suggestion); |
| 198 | + suggestions.extend(anonymous_suggestion); |
| 199 | + suggestions.extend(hidden_suggestion); |
| 200 | + |
| 201 | + cx.emit_span_lint( |
| 202 | + MISMATCHED_LIFETIME_SYNTAXES, |
| 203 | + Vec::clone(&inputs), |
| 204 | + lints::MismatchedLifetimeSyntaxes { lifetime_name, inputs, outputs, suggestions }, |
| 205 | + ); |
| 206 | + } |
| 207 | + } |
| 208 | +} |
| 209 | + |
| 210 | +#[derive(Default)] |
| 211 | +struct Bucket<'a, 'tcx> { |
| 212 | + members: Vec<&'a Info<'tcx>>, |
| 213 | + n_ref: usize, |
| 214 | + n_path: usize, |
| 215 | +} |
| 216 | + |
| 217 | +fn build_mismatch_suggestion( |
| 218 | + lifetime_name: &str, |
| 219 | + buckets: &[&Bucket<'_, '_>], |
| 220 | +) -> lints::MismatchedLifetimeSyntaxesSuggestion { |
| 221 | + let lifetime_name = lifetime_name.to_owned(); |
| 222 | + |
| 223 | + let suggestions = buckets |
| 224 | + .iter() |
| 225 | + .flat_map(|b| &b.members) |
| 226 | + .map(|info| info.suggestion(&lifetime_name)) |
| 227 | + .collect(); |
| 228 | + |
| 229 | + lints::MismatchedLifetimeSyntaxesSuggestion::Named { |
| 230 | + lifetime_name, |
| 231 | + suggestions, |
| 232 | + tool_only: false, |
| 233 | + } |
| 234 | +} |
| 235 | + |
| 236 | +struct Info<'tcx> { |
| 237 | + type_span: Span, |
| 238 | + lifetime: &'tcx hir::Lifetime, |
| 239 | +} |
| 240 | + |
| 241 | +impl<'tcx> Info<'tcx> { |
| 242 | + fn lifetime_name(&self) -> &str { |
| 243 | + self.lifetime.ident.as_str() |
| 244 | + } |
| 245 | + |
| 246 | + fn is_static(&self) -> bool { |
| 247 | + self.lifetime.is_static() |
| 248 | + } |
| 249 | + |
| 250 | + /// When reporting a lifetime that is hidden, we expand the span |
| 251 | + /// to include the type. Otherwise we end up pointing at nothing, |
| 252 | + /// which is a bit confusing. |
| 253 | + fn reporting_span(&self) -> Span { |
| 254 | + if self.lifetime.is_syntactically_hidden() { |
| 255 | + self.type_span |
| 256 | + } else { |
| 257 | + self.lifetime.ident.span |
| 258 | + } |
| 259 | + } |
| 260 | + |
| 261 | + fn suggestion(&self, lifetime_name: &str) -> (Span, String) { |
| 262 | + self.lifetime.suggestion(lifetime_name) |
| 263 | + } |
| 264 | +} |
| 265 | + |
| 266 | +type LifetimeInfoMap<'tcx> = FxIndexMap<&'tcx hir::LifetimeName, Vec<Info<'tcx>>>; |
| 267 | + |
| 268 | +struct LifetimeInfoCollector<'a, 'tcx> { |
| 269 | + type_span: Span, |
| 270 | + map: &'a mut LifetimeInfoMap<'tcx>, |
| 271 | +} |
| 272 | + |
| 273 | +impl<'a, 'tcx> LifetimeInfoCollector<'a, 'tcx> { |
| 274 | + fn collect(ty: &'tcx hir::Ty<'tcx>, map: &'a mut LifetimeInfoMap<'tcx>) { |
| 275 | + let mut this = Self { type_span: ty.span, map }; |
| 276 | + |
| 277 | + intravisit::walk_unambig_ty(&mut this, ty); |
| 278 | + } |
| 279 | +} |
| 280 | + |
| 281 | +impl<'a, 'tcx> Visitor<'tcx> for LifetimeInfoCollector<'a, 'tcx> { |
| 282 | + #[instrument(skip(self))] |
| 283 | + fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) { |
| 284 | + let type_span = self.type_span; |
| 285 | + |
| 286 | + let info = Info { type_span, lifetime }; |
| 287 | + |
| 288 | + self.map.entry(&lifetime.res).or_default().push(info); |
| 289 | + } |
| 290 | + |
| 291 | + #[instrument(skip(self))] |
| 292 | + fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result { |
| 293 | + let old_type_span = self.type_span; |
| 294 | + |
| 295 | + self.type_span = ty.span; |
| 296 | + |
| 297 | + intravisit::walk_ty(self, ty); |
| 298 | + |
| 299 | + self.type_span = old_type_span; |
| 300 | + } |
| 301 | +} |
0 commit comments