| 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 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>, |
| _: hir::intravisit::FnKind<'tcx>, |
| fd: &'tcx hir::FnDecl<'tcx>, |
| _: &'tcx hir::Body<'tcx>, |
| _: rustc_span::Span, |
| _: rustc_span::def_id::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 rustc_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>) { |
| let mut input_map = Default::default(); |
| let mut output_map = Default::default(); |
| |
| for input in fd.inputs { |
| LifetimeInfoCollector::collect(input, &mut input_map); |
| } |
| |
| if let hir::FnRetTy::Return(output) = fd.output { |
| LifetimeInfoCollector::collect(output, &mut output_map); |
| } |
| |
| report_mismatches(cx, &input_map, &output_map); |
| } |
| |
| #[instrument(skip_all)] |
| fn report_mismatches<'tcx>( |
| cx: &LateContext<'tcx>, |
| inputs: &LifetimeInfoMap<'tcx>, |
| outputs: &LifetimeInfoMap<'tcx>, |
| ) { |
| for (resolved_lifetime, output_info) in outputs { |
| if let Some(input_info) = inputs.get(resolved_lifetime) { |
| if !lifetimes_use_matched_syntax(input_info, output_info) { |
| emit_mismatch_diagnostic(cx, input_info, output_info); |
| } |
| } |
| } |
| } |
| |
| #[derive(Debug, Copy, Clone, PartialEq)] |
| enum LifetimeSyntaxCategory { |
| Hidden, |
| Elided, |
| Named, |
| } |
| |
| impl LifetimeSyntaxCategory { |
| fn new(syntax_source: (hir::LifetimeSyntax, LifetimeSource)) -> Option<Self> { |
| use LifetimeSource::*; |
| use hir::LifetimeSyntax::*; |
| |
| match syntax_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 flatten(&self) -> impl Iterator<Item = &T> { |
| let Self { hidden, elided, named } = self; |
| [hidden.iter(), elided.iter(), named.iter()].into_iter().flatten() |
| } |
| } |
| |
| 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 mut syntax_counts = LifetimeSyntaxCategories::<usize>::default(); |
| |
| for info in input_info.iter().chain(output_info) { |
| if let Some(category) = info.lifetime_syntax_category() { |
| *syntax_counts.select(category) += 1; |
| } |
| } |
| |
| tracing::debug!(?syntax_counts); |
| |
| matches!( |
| syntax_counts, |
| LifetimeSyntaxCategories { hidden: _, elided: 0, named: 0 } |
| | LifetimeSyntaxCategories { hidden: 0, elided: _, named: 0 } |
| | LifetimeSyntaxCategories { hidden: 0, elided: 0, named: _ } |
| ) |
| } |
| |
| 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 syntax_source = info.syntax_source(); |
| |
| if let (_, Other) = syntax_source { |
| // Ignore any other kind of lifetime. |
| continue; |
| } |
| |
| if let (ExplicitBound, _) = syntax_source { |
| bound_lifetime = Some(info); |
| } |
| |
| match syntax_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<'_>`. |
| (ExplicitAnonymous, Path { .. }) => { |
| 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>`. |
| (ExplicitBound, Path { .. }) => { |
| 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"); |
| } |
| |
| // E.g. `+ '_`, `+ use<'_>`. |
| (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => { |
| suggest_change_to_explicit_bound.push(info); |
| } |
| |
| // E.g. `+ 'a`, `+ use<'a>`. |
| (ExplicitBound, OutlivesBound | PreciseCapturing) => { |
| suggest_change_to_mixed_explicit_anonymous.push(info); |
| suggest_change_to_explicit_anonymous.push(info); |
| } |
| |
| (_, Other) => { |
| panic!("This syntax / source combination has already been skipped"); |
| } |
| } |
| |
| if matches!(syntax_source, (_, Path { .. } | OutlivesBound | PreciseCapturing)) { |
| allow_suggesting_implicit = false; |
| } |
| |
| match syntax_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 { |
| if let Some(category) = info.lifetime_syntax_category() { |
| categories.select(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_name(), &suggest_change_to_explicit_bound) |
| }); |
| |
| let is_bound_static = bound_lifetime.is_some_and(|info| info.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, |
| tool_only: 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, tool_only: 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.flatten().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, |
| tool_only: false, |
| } |
| } |
| |
| #[derive(Debug)] |
| struct Info<'tcx> { |
| type_span: Span, |
| referenced_type_span: Option<Span>, |
| lifetime: &'tcx hir::Lifetime, |
| } |
| |
| impl<'tcx> Info<'tcx> { |
| fn syntax_source(&self) -> (hir::LifetimeSyntax, LifetimeSource) { |
| (self.lifetime.syntax, self.lifetime.source) |
| } |
| |
| fn lifetime_syntax_category(&self) -> Option<LifetimeSyntaxCategory> { |
| LifetimeSyntaxCategory::new(self.syntax_source()) |
| } |
| |
| fn lifetime_name(&self) -> &str { |
| self.lifetime.ident.as_str() |
| } |
| |
| fn is_static(&self) -> bool { |
| self.lifetime.is_static() |
| } |
| |
| /// 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.type_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.suggestion("'dummy").0; |
| |
| if let Some(referenced_type_span) = self.referenced_type_span { |
| span = span.until(referenced_type_span); |
| } |
| |
| span |
| } |
| |
| fn suggestion(&self, lifetime_name: &str) -> (Span, String) { |
| self.lifetime.suggestion(lifetime_name) |
| } |
| } |
| |
| type LifetimeInfoMap<'tcx> = FxIndexMap<&'tcx hir::LifetimeKind, Vec<Info<'tcx>>>; |
| |
| struct LifetimeInfoCollector<'a, 'tcx> { |
| type_span: Span, |
| referenced_type_span: Option<Span>, |
| map: &'a mut LifetimeInfoMap<'tcx>, |
| } |
| |
| impl<'a, 'tcx> LifetimeInfoCollector<'a, 'tcx> { |
| fn collect(ty: &'tcx hir::Ty<'tcx>, map: &'a mut LifetimeInfoMap<'tcx>) { |
| let mut this = Self { type_span: ty.span, referenced_type_span: None, map }; |
| |
| intravisit::walk_unambig_ty(&mut this, ty); |
| } |
| } |
| |
| impl<'a, 'tcx> Visitor<'tcx> for LifetimeInfoCollector<'a, 'tcx> { |
| #[instrument(skip(self))] |
| fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) { |
| let type_span = self.type_span; |
| let referenced_type_span = self.referenced_type_span; |
| |
| let info = Info { type_span, referenced_type_span, lifetime }; |
| |
| self.map.entry(&lifetime.kind).or_default().push(info); |
| } |
| |
| #[instrument(skip(self))] |
| fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result { |
| let old_type_span = self.type_span; |
| let old_referenced_type_span = self.referenced_type_span; |
| |
| self.type_span = ty.span; |
| if let hir::TyKind::Ref(_, ty) = ty.kind { |
| self.referenced_type_span = Some(ty.ty.span); |
| } |
| |
| intravisit::walk_ty(self, ty); |
| |
| self.type_span = old_type_span; |
| self.referenced_type_span = old_referenced_type_span; |
| } |
| } |