| use rustc_data_structures::fx::FxIndexMap; |
| use rustc_hir::intravisit::{self, Visitor}; |
| use rustc_hir::{self as hir, LifetimeSource}; |
| use rustc_session::{declare_lint, declare_lint_pass}; |
| use rustc_span::Span; |
| use rustc_span::def_id::LocalDefId; |
| use tracing::instrument; |
| |
| use crate::{LateContext, LateLintPass, LintContext, lints}; |
| |
| declare_lint! { |
| /// The `mismatched_lifetime_syntaxes` lint detects when the same |
| /// lifetime is referred to by different syntaxes between function |
| /// arguments and return values. |
| /// |
| /// The three kinds of syntaxes are: |
| /// |
| /// 1. Named lifetimes. These are references (`&'a str`) or paths |
| /// (`Person<'a>`) that use a lifetime with a name, such as |
| /// `'static` or `'a`. |
| /// |
| /// 2. Elided lifetimes. These are references with no explicit |
| /// lifetime (`&str`), references using the anonymous lifetime |
| /// (`&'_ str`), and paths using the anonymous lifetime |
| /// (`Person<'_>`). |
| /// |
| /// 3. Hidden lifetimes. These are paths that do not contain any |
| /// visual indication that it contains a lifetime (`Person`). |
| /// |
| /// ### Example |
| /// |
| /// ```rust,compile_fail |
| /// #![deny(mismatched_lifetime_syntaxes)] |
| /// |
| /// pub fn mixing_named_with_elided(v: &'static u8) -> &u8 { |
| /// v |
| /// } |
| /// |
| /// struct Person<'a> { |
| /// name: &'a str, |
| /// } |
| /// |
| /// pub fn mixing_hidden_with_elided(v: Person) -> Person<'_> { |
| /// v |
| /// } |
| /// |
| /// struct Foo; |
| /// |
| /// impl Foo { |
| /// // Lifetime elision results in the output lifetime becoming |
| /// // `'static`, which is not what was intended. |
| /// pub fn get_mut(&'static self, x: &mut u8) -> &mut u8 { |
| /// unsafe { &mut *(x as *mut _) } |
| /// } |
| /// } |
| /// ``` |
| /// |
| /// {{produces}} |
| /// |
| /// ### Explanation |
| /// |
| /// Lifetime elision is useful because it frees you from having to |
| /// give each lifetime its own name and show the relation of input |
| /// and output lifetimes for common cases. However, a lifetime |
| /// that uses inconsistent syntax between related arguments and |
| /// return values is more confusing. |
| /// |
| /// In certain `unsafe` code, lifetime elision combined with |
| /// inconsistent lifetime syntax may result in unsound code. |
| pub MISMATCHED_LIFETIME_SYNTAXES, |
| Warn, |
| "detects when a lifetime uses different syntax between arguments and return values" |
| } |
| |
| declare_lint_pass!(LifetimeSyntax => [MISMATCHED_LIFETIME_SYNTAXES]); |
| |
| impl<'tcx> LateLintPass<'tcx> for LifetimeSyntax { |
| #[instrument(skip_all)] |
| fn check_fn( |
| &mut self, |
| cx: &LateContext<'tcx>, |
| _: intravisit::FnKind<'tcx>, |
| fd: &'tcx hir::FnDecl<'tcx>, |
| _: &'tcx hir::Body<'tcx>, |
| _: Span, |
| _: LocalDefId, |
| ) { |
| check_fn_like(cx, fd); |
| } |
| |
| #[instrument(skip_all)] |
| fn check_trait_item(&mut self, cx: &LateContext<'tcx>, ti: &'tcx hir::TraitItem<'tcx>) { |
| match ti.kind { |
| hir::TraitItemKind::Const(..) => {} |
| hir::TraitItemKind::Fn(fn_sig, _trait_fn) => check_fn_like(cx, fn_sig.decl), |
| hir::TraitItemKind::Type(..) => {} |
| } |
| } |
| |
| #[instrument(skip_all)] |
| fn check_foreign_item(&mut self, cx: &LateContext<'tcx>, fi: &'tcx hir::ForeignItem<'tcx>) { |
| match fi.kind { |
| hir::ForeignItemKind::Fn(fn_sig, _idents, _generics) => check_fn_like(cx, fn_sig.decl), |
| hir::ForeignItemKind::Static(..) => {} |
| hir::ForeignItemKind::Type => {} |
| } |
| } |
| } |
| |
| fn check_fn_like<'tcx>(cx: &LateContext<'tcx>, fd: &'tcx hir::FnDecl<'tcx>) { |
| if fd.inputs.is_empty() { |
| return; |
| } |
| let hir::FnRetTy::Return(output) = fd.output else { |
| return; |
| }; |
| |
| let mut map: FxIndexMap<hir::LifetimeKind, LifetimeGroup<'_>> = FxIndexMap::default(); |
| |
| LifetimeInfoCollector::collect(output, |info| { |
| let group = map.entry(info.lifetime.kind).or_default(); |
| group.outputs.push(info); |
| }); |
| if map.is_empty() { |
| return; |
| } |
| |
| for input in fd.inputs { |
| LifetimeInfoCollector::collect(input, |info| { |
| if let Some(group) = map.get_mut(&info.lifetime.kind) { |
| group.inputs.push(info); |
| } |
| }); |
| } |
| |
| for LifetimeGroup { ref inputs, ref outputs } in map.into_values() { |
| if inputs.is_empty() { |
| continue; |
| } |
| if !lifetimes_use_matched_syntax(inputs, outputs) { |
| emit_mismatch_diagnostic(cx, inputs, outputs); |
| } |
| } |
| } |
| |
| #[derive(Default)] |
| struct LifetimeGroup<'tcx> { |
| inputs: Vec<Info<'tcx>>, |
| outputs: Vec<Info<'tcx>>, |
| } |
| |
| #[derive(Debug, Copy, Clone, PartialEq)] |
| enum LifetimeSyntaxCategory { |
| Hidden, |
| Elided, |
| Named, |
| } |
| |
| impl LifetimeSyntaxCategory { |
| fn new(lifetime: &hir::Lifetime) -> Option<Self> { |
| use LifetimeSource::*; |
| use hir::LifetimeSyntax::*; |
| |
| match (lifetime.syntax, lifetime.source) { |
| // E.g. `&T`. |
| (Implicit, Reference) | |
| // E.g. `&'_ T`. |
| (ExplicitAnonymous, Reference) | |
| // E.g. `ContainsLifetime<'_>`. |
| (ExplicitAnonymous, Path { .. }) | |
| // E.g. `+ '_`, `+ use<'_>`. |
| (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => { |
| Some(Self::Elided) |
| } |
| |
| // E.g. `ContainsLifetime`. |
| (Implicit, Path { .. }) => { |
| Some(Self::Hidden) |
| } |
| |
| // E.g. `&'a T`. |
| (ExplicitBound, Reference) | |
| // E.g. `ContainsLifetime<'a>`. |
| (ExplicitBound, Path { .. }) | |
| // E.g. `+ 'a`, `+ use<'a>`. |
| (ExplicitBound, OutlivesBound | PreciseCapturing) => { |
| Some(Self::Named) |
| } |
| |
| (Implicit, OutlivesBound | PreciseCapturing) | |
| (_, Other) => { |
| None |
| } |
| } |
| } |
| } |
| |
| #[derive(Debug, Default)] |
| pub struct LifetimeSyntaxCategories<T> { |
| pub hidden: T, |
| pub elided: T, |
| pub named: T, |
| } |
| |
| impl<T> LifetimeSyntaxCategories<T> { |
| fn select(&mut self, category: LifetimeSyntaxCategory) -> &mut T { |
| use LifetimeSyntaxCategory::*; |
| |
| match category { |
| Elided => &mut self.elided, |
| Hidden => &mut self.hidden, |
| Named => &mut self.named, |
| } |
| } |
| } |
| |
| impl<T> LifetimeSyntaxCategories<Vec<T>> { |
| pub fn len(&self) -> LifetimeSyntaxCategories<usize> { |
| LifetimeSyntaxCategories { |
| hidden: self.hidden.len(), |
| elided: self.elided.len(), |
| named: self.named.len(), |
| } |
| } |
| |
| pub fn iter_unnamed(&self) -> impl Iterator<Item = &T> { |
| let Self { hidden, elided, named: _ } = self; |
| std::iter::chain(hidden, elided) |
| } |
| } |
| |
| impl std::ops::Add for LifetimeSyntaxCategories<usize> { |
| type Output = Self; |
| |
| fn add(self, rhs: Self) -> Self::Output { |
| Self { |
| hidden: self.hidden + rhs.hidden, |
| elided: self.elided + rhs.elided, |
| named: self.named + rhs.named, |
| } |
| } |
| } |
| |
| fn lifetimes_use_matched_syntax(input_info: &[Info<'_>], output_info: &[Info<'_>]) -> bool { |
| let (first, inputs) = input_info.split_first().unwrap(); |
| std::iter::chain(inputs, output_info).all(|info| info.syntax_category == first.syntax_category) |
| } |
| |
| fn emit_mismatch_diagnostic<'tcx>( |
| cx: &LateContext<'tcx>, |
| input_info: &[Info<'_>], |
| output_info: &[Info<'_>], |
| ) { |
| // There can only ever be zero or one bound lifetime |
| // for a given lifetime resolution. |
| let mut bound_lifetime = None; |
| |
| // We offer the following kinds of suggestions (when appropriate |
| // such that the suggestion wouldn't violate the lint): |
| // |
| // 1. Every lifetime becomes named, when there is already a |
| // user-provided name. |
| // |
| // 2. A "mixed" signature, where references become implicit |
| // and paths become explicitly anonymous. |
| // |
| // 3. Every lifetime becomes implicit. |
| // |
| // 4. Every lifetime becomes explicitly anonymous. |
| // |
| // Number 2 is arguably the most common pattern and the one we |
| // should push strongest. Number 3 is likely the next most common, |
| // followed by number 1. Coming in at a distant last would be |
| // number 4. |
| // |
| // Beyond these, there are variants of acceptable signatures that |
| // we won't suggest because they are very low-value. For example, |
| // we will never suggest `fn(&T1, &'_ T2) -> &T3` even though that |
| // would pass the lint. |
| // |
| // The following collections are the lifetime instances that we |
| // suggest changing to a given alternate style. |
| |
| // 1. Convert all to named. |
| let mut suggest_change_to_explicit_bound = Vec::new(); |
| |
| // 2. Convert to mixed. We track each kind of change separately. |
| let mut suggest_change_to_mixed_implicit = Vec::new(); |
| let mut suggest_change_to_mixed_explicit_anonymous = Vec::new(); |
| |
| // 3. Convert all to implicit. |
| let mut suggest_change_to_implicit = Vec::new(); |
| |
| // 4. Convert all to explicit anonymous. |
| let mut suggest_change_to_explicit_anonymous = Vec::new(); |
| |
| // Some styles prevent using implicit syntax at all. |
| let mut allow_suggesting_implicit = true; |
| |
| // It only makes sense to suggest mixed if we have both sources. |
| let mut saw_a_reference = false; |
| let mut saw_a_path = false; |
| |
| for info in input_info.iter().chain(output_info) { |
| use LifetimeSource::*; |
| use hir::LifetimeSyntax::*; |
| |
| let lifetime = info.lifetime; |
| |
| if lifetime.syntax == ExplicitBound { |
| bound_lifetime = Some(info); |
| } |
| |
| match (lifetime.syntax, lifetime.source) { |
| // E.g. `&T`. |
| (Implicit, Reference) => { |
| suggest_change_to_explicit_anonymous.push(info); |
| suggest_change_to_explicit_bound.push(info); |
| } |
| |
| // E.g. `&'_ T`. |
| (ExplicitAnonymous, Reference) => { |
| suggest_change_to_implicit.push(info); |
| suggest_change_to_explicit_bound.push(info); |
| } |
| |
| // E.g. `ContainsLifetime`. |
| (Implicit, Path { .. }) => { |
| suggest_change_to_mixed_explicit_anonymous.push(info); |
| suggest_change_to_explicit_anonymous.push(info); |
| suggest_change_to_explicit_bound.push(info); |
| } |
| |
| // E.g. `ContainsLifetime<'_>`, `+ '_`, `+ use<'_>`. |
| (ExplicitAnonymous, Path { .. } | OutlivesBound | PreciseCapturing) => { |
| suggest_change_to_explicit_bound.push(info); |
| } |
| |
| // E.g. `&'a T`. |
| (ExplicitBound, Reference) => { |
| suggest_change_to_implicit.push(info); |
| suggest_change_to_mixed_implicit.push(info); |
| suggest_change_to_explicit_anonymous.push(info); |
| } |
| |
| // E.g. `ContainsLifetime<'a>`, `+ 'a`, `+ use<'a>`. |
| (ExplicitBound, Path { .. } | OutlivesBound | PreciseCapturing) => { |
| suggest_change_to_mixed_explicit_anonymous.push(info); |
| suggest_change_to_explicit_anonymous.push(info); |
| } |
| |
| (Implicit, OutlivesBound | PreciseCapturing) => { |
| panic!("This syntax / source combination is not possible"); |
| } |
| |
| (_, Other) => { |
| panic!("This syntax / source combination has already been skipped"); |
| } |
| } |
| |
| if matches!(lifetime.source, Path { .. } | OutlivesBound | PreciseCapturing) { |
| allow_suggesting_implicit = false; |
| } |
| |
| match lifetime.source { |
| Reference => saw_a_reference = true, |
| Path { .. } => saw_a_path = true, |
| _ => {} |
| } |
| } |
| |
| let categorize = |infos: &[Info<'_>]| { |
| let mut categories = LifetimeSyntaxCategories::<Vec<_>>::default(); |
| for info in infos { |
| categories.select(info.syntax_category).push(info.reporting_span()); |
| } |
| categories |
| }; |
| |
| let inputs = categorize(input_info); |
| let outputs = categorize(output_info); |
| |
| let make_implicit_suggestions = |
| |infos: &[&Info<'_>]| infos.iter().map(|i| i.removing_span()).collect::<Vec<_>>(); |
| |
| let explicit_bound_suggestion = bound_lifetime.map(|info| { |
| build_mismatch_suggestion(info.lifetime.ident.as_str(), &suggest_change_to_explicit_bound) |
| }); |
| |
| let is_bound_static = bound_lifetime.is_some_and(|info| info.lifetime.is_static()); |
| |
| tracing::debug!(?bound_lifetime, ?explicit_bound_suggestion, ?is_bound_static); |
| |
| let should_suggest_mixed = |
| // Do we have a mixed case? |
| (saw_a_reference && saw_a_path) && |
| // Is there anything to change? |
| (!suggest_change_to_mixed_implicit.is_empty() || |
| !suggest_change_to_mixed_explicit_anonymous.is_empty()) && |
| // If we have `'static`, we don't want to remove it. |
| !is_bound_static; |
| |
| let mixed_suggestion = should_suggest_mixed.then(|| { |
| let implicit_suggestions = make_implicit_suggestions(&suggest_change_to_mixed_implicit); |
| |
| let explicit_anonymous_suggestions = suggest_change_to_mixed_explicit_anonymous |
| .iter() |
| .map(|info| info.suggestion("'_")) |
| .collect(); |
| |
| lints::MismatchedLifetimeSyntaxesSuggestion::Mixed { |
| implicit_suggestions, |
| explicit_anonymous_suggestions, |
| optional_alternative: false, |
| } |
| }); |
| |
| tracing::debug!( |
| ?suggest_change_to_mixed_implicit, |
| ?suggest_change_to_mixed_explicit_anonymous, |
| ?mixed_suggestion, |
| ); |
| |
| let should_suggest_implicit = |
| // Is there anything to change? |
| !suggest_change_to_implicit.is_empty() && |
| // We never want to hide the lifetime in a path (or similar). |
| allow_suggesting_implicit && |
| // If we have `'static`, we don't want to remove it. |
| !is_bound_static; |
| |
| let implicit_suggestion = should_suggest_implicit.then(|| { |
| let suggestions = make_implicit_suggestions(&suggest_change_to_implicit); |
| |
| lints::MismatchedLifetimeSyntaxesSuggestion::Implicit { |
| suggestions, |
| optional_alternative: false, |
| } |
| }); |
| |
| tracing::debug!( |
| ?should_suggest_implicit, |
| ?suggest_change_to_implicit, |
| allow_suggesting_implicit, |
| ?implicit_suggestion, |
| ); |
| |
| let should_suggest_explicit_anonymous = |
| // Is there anything to change? |
| !suggest_change_to_explicit_anonymous.is_empty() && |
| // If we have `'static`, we don't want to remove it. |
| !is_bound_static; |
| |
| let explicit_anonymous_suggestion = should_suggest_explicit_anonymous |
| .then(|| build_mismatch_suggestion("'_", &suggest_change_to_explicit_anonymous)); |
| |
| tracing::debug!( |
| ?should_suggest_explicit_anonymous, |
| ?suggest_change_to_explicit_anonymous, |
| ?explicit_anonymous_suggestion, |
| ); |
| |
| // We can produce a number of suggestions which may overwhelm |
| // the user. Instead, we order the suggestions based on Rust |
| // idioms. The "best" choice is shown to the user and the |
| // remaining choices are shown to tools only. |
| let mut suggestions = Vec::new(); |
| suggestions.extend(explicit_bound_suggestion); |
| suggestions.extend(mixed_suggestion); |
| suggestions.extend(implicit_suggestion); |
| suggestions.extend(explicit_anonymous_suggestion); |
| |
| cx.emit_span_lint( |
| MISMATCHED_LIFETIME_SYNTAXES, |
| inputs.iter_unnamed().chain(outputs.iter_unnamed()).copied().collect::<Vec<_>>(), |
| lints::MismatchedLifetimeSyntaxes { inputs, outputs, suggestions }, |
| ); |
| } |
| |
| fn build_mismatch_suggestion( |
| lifetime_name: &str, |
| infos: &[&Info<'_>], |
| ) -> lints::MismatchedLifetimeSyntaxesSuggestion { |
| let lifetime_name = lifetime_name.to_owned(); |
| |
| let suggestions = infos.iter().map(|info| info.suggestion(&lifetime_name)).collect(); |
| |
| lints::MismatchedLifetimeSyntaxesSuggestion::Explicit { |
| lifetime_name, |
| suggestions, |
| optional_alternative: false, |
| } |
| } |
| |
| #[derive(Debug)] |
| struct Info<'tcx> { |
| lifetime: &'tcx hir::Lifetime, |
| syntax_category: LifetimeSyntaxCategory, |
| ty: &'tcx hir::Ty<'tcx>, |
| } |
| |
| impl<'tcx> Info<'tcx> { |
| /// When reporting a lifetime that is implicit, we expand the span |
| /// to include the type. Otherwise we end up pointing at nothing, |
| /// which is a bit confusing. |
| fn reporting_span(&self) -> Span { |
| if self.lifetime.is_implicit() { self.ty.span } else { self.lifetime.ident.span } |
| } |
| |
| /// When removing an explicit lifetime from a reference, |
| /// we want to remove the whitespace after the lifetime. |
| /// |
| /// ```rust |
| /// fn x(a: &'_ u8) {} |
| /// ``` |
| /// |
| /// Should become: |
| /// |
| /// ```rust |
| /// fn x(a: &u8) {} |
| /// ``` |
| // FIXME: Ideally, we'd also remove the lifetime declaration. |
| fn removing_span(&self) -> Span { |
| let mut span = self.lifetime.ident.span; |
| if let hir::TyKind::Ref(_, mut_ty) = self.ty.kind { |
| span = span.until(mut_ty.ty.span); |
| } |
| span |
| } |
| |
| fn suggestion(&self, lifetime_name: &str) -> (Span, String) { |
| self.lifetime.suggestion(lifetime_name) |
| } |
| } |
| |
| struct LifetimeInfoCollector<'tcx, F> { |
| info_func: F, |
| ty: &'tcx hir::Ty<'tcx>, |
| } |
| |
| impl<'tcx, F> LifetimeInfoCollector<'tcx, F> |
| where |
| F: FnMut(Info<'tcx>), |
| { |
| fn collect(ty: &'tcx hir::Ty<'tcx>, info_func: F) { |
| let mut this = Self { info_func, ty }; |
| |
| intravisit::walk_unambig_ty(&mut this, ty); |
| } |
| } |
| |
| impl<'tcx, F> Visitor<'tcx> for LifetimeInfoCollector<'tcx, F> |
| where |
| F: FnMut(Info<'tcx>), |
| { |
| #[instrument(skip(self))] |
| fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) { |
| if let Some(syntax_category) = LifetimeSyntaxCategory::new(lifetime) { |
| let info = Info { lifetime, syntax_category, ty: self.ty }; |
| (self.info_func)(info); |
| } |
| } |
| |
| #[instrument(skip(self))] |
| fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result { |
| let old_ty = std::mem::replace(&mut self.ty, ty.as_unambig_ty()); |
| intravisit::walk_ty(self, ty); |
| self.ty = old_ty; |
| } |
| } |