mirror of
https://github.com/rust-lang/rust.git
synced 2025-06-22 04:27:33 +00:00
504 lines
17 KiB
Rust
504 lines
17 KiB
Rust
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,
|
|
) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn lifetimes_use_matched_syntax(input_info: &[Info<'_>], output_info: &[Info<'_>]) -> bool {
|
|
// Categorize lifetimes into source/syntax buckets.
|
|
let mut n_hidden = 0;
|
|
let mut n_elided = 0;
|
|
let mut n_named = 0;
|
|
|
|
for info in input_info.iter().chain(output_info) {
|
|
use LifetimeSource::*;
|
|
use hir::LifetimeSyntax::*;
|
|
|
|
let syntax_source = (info.lifetime.syntax, info.lifetime.source);
|
|
|
|
match syntax_source {
|
|
// Ignore any other kind of lifetime.
|
|
(_, Other) => continue,
|
|
|
|
// E.g. `&T`.
|
|
(Implicit, Reference | OutlivesBound | PreciseCapturing) |
|
|
// E.g. `&'_ T`.
|
|
(ExplicitAnonymous, Reference | OutlivesBound | PreciseCapturing) |
|
|
// E.g. `ContainsLifetime<'_>`.
|
|
(ExplicitAnonymous, Path { .. }) => n_elided += 1,
|
|
|
|
// E.g. `ContainsLifetime`.
|
|
(Implicit, Path { .. }) => n_hidden += 1,
|
|
|
|
// E.g. `&'a T`.
|
|
(ExplicitBound, Reference | OutlivesBound | PreciseCapturing) |
|
|
// E.g. `ContainsLifetime<'a>`.
|
|
(ExplicitBound, Path { .. }) => n_named += 1,
|
|
};
|
|
}
|
|
|
|
let syntax_counts = (n_hidden, n_elided, n_named);
|
|
tracing::debug!(?syntax_counts);
|
|
|
|
matches!(syntax_counts, (_, 0, 0) | (0, _, 0) | (0, 0, _))
|
|
}
|
|
|
|
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.lifetime.syntax, info.lifetime.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_mixed_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 make_implicit_suggestions =
|
|
|infos: &[&Info<'_>]| infos.iter().map(|i| i.removing_span()).collect::<Vec<_>>();
|
|
|
|
let inputs = input_info.iter().map(|info| info.reporting_span()).collect();
|
|
let outputs = output_info.iter().map(|info| info.reporting_span()).collect();
|
|
|
|
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,
|
|
);
|
|
|
|
let lifetime_name = bound_lifetime.map(|info| info.lifetime_name()).unwrap_or("'_").to_owned();
|
|
|
|
// 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,
|
|
Vec::clone(&inputs),
|
|
lints::MismatchedLifetimeSyntaxes { lifetime_name, 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 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;
|
|
}
|
|
}
|