blob: 0cac91c234080c1b32fe9583dfb87dd64e7f13c8 [file] [log] [blame] [edit]
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;
}
}