Skip to content

Commit

Permalink
Auto merge of rust-lang#118391 - compiler-errors:lifetimes-eq, r=<try>
Browse files Browse the repository at this point in the history
Extend `UNUSED_LIFETIMES` lint to detect lifetimes which are semantically redundant

There already is a `UNUSED_LIFETIMES` lint which is fired when we detect where clause bounds like `where 'a: 'static`, however, it doesn't use the full power of lexical region resolution to detect failures.

Right now `UNUSED_LIFETIMES` is an `Allow` lint, though presumably we could bump it to warn? I can (somewhat) easily implement a structured suggestion so this can be rustfix'd automatically, since we can just walk through the HIR body, replacing instances of the redundant lifetime.

Fixes rust-lang#118376
r? lcnr
  • Loading branch information
bors committed Nov 27, 2023
2 parents 6eb9524 + b2c18d9 commit 58aa0f0
Show file tree
Hide file tree
Showing 17 changed files with 300 additions and 121 deletions.
1 change: 1 addition & 0 deletions compiler/rustc_errors/src/diagnostic_impls.rs
Expand Up @@ -44,6 +44,7 @@ impl<'a, T: Clone + IntoDiagnosticArg> IntoDiagnosticArg for &'a T {
}
}

#[macro_export]
macro_rules! into_diagnostic_arg_using_display {
($( $ty:ty ),+ $(,)?) => {
$(
Expand Down
26 changes: 0 additions & 26 deletions compiler/rustc_hir_analysis/src/collect/resolve_bound_vars.rs
Expand Up @@ -19,7 +19,6 @@ use rustc_middle::hir::nested_filter;
use rustc_middle::middle::resolve_bound_vars::*;
use rustc_middle::query::Providers;
use rustc_middle::ty::{self, TyCtxt, TypeSuperVisitable, TypeVisitor};
use rustc_session::lint;
use rustc_span::def_id::DefId;
use rustc_span::symbol::{sym, Ident};
use rustc_span::{Span, DUMMY_SP};
Expand Down Expand Up @@ -905,31 +904,6 @@ impl<'a, 'tcx> Visitor<'tcx> for BoundVarContext<'a, 'tcx> {
}) => {
self.visit_lifetime(lifetime);
walk_list!(self, visit_param_bound, bounds);

if lifetime.res != hir::LifetimeName::Static {
for bound in bounds {
let hir::GenericBound::Outlives(lt) = bound else {
continue;
};
if lt.res != hir::LifetimeName::Static {
continue;
}
self.insert_lifetime(lt, ResolvedArg::StaticLifetime);
self.tcx.struct_span_lint_hir(
lint::builtin::UNUSED_LIFETIMES,
lifetime.hir_id,
lifetime.ident.span,
format!("unnecessary lifetime parameter `{}`", lifetime.ident),
|lint| {
let help = format!(
"you can use the `'static` lifetime directly, in place of `{}`",
lifetime.ident,
);
lint.help(help)
},
);
}
}
}
&hir::WherePredicate::EqPredicate(hir::WhereEqPredicate { lhs_ty, rhs_ty, .. }) => {
self.visit_ty(lhs_ty);
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_lint/messages.ftl
Expand Up @@ -470,6 +470,9 @@ lint_reason_must_be_string_literal = reason must be a string literal
lint_reason_must_come_last = reason in lint attribute must come last
lint_redundant_lifetime_args = unnecessary lifetime parameter `{$victim}`
.note = you can use the `{$candidate}` lifetime directly, in place of `{$victim}`
lint_redundant_semicolons =
unnecessary trailing {$multiple ->
[true] semicolons
Expand Down
3 changes: 3 additions & 0 deletions compiler/rustc_lint/src/lib.rs
Expand Up @@ -82,6 +82,7 @@ mod opaque_hidden_inferred_bound;
mod pass_by_value;
mod passes;
mod ptr_nulls;
mod redundant_lifetime_args;
mod redundant_semicolon;
mod reference_casting;
mod traits;
Expand Down Expand Up @@ -119,6 +120,7 @@ use noop_method_call::*;
use opaque_hidden_inferred_bound::*;
use pass_by_value::*;
use ptr_nulls::*;
use redundant_lifetime_args::RedundantLifetimeArgs;
use redundant_semicolon::*;
use reference_casting::*;
use traits::*;
Expand Down Expand Up @@ -240,6 +242,7 @@ late_lint_methods!(
MissingDebugImplementations: MissingDebugImplementations,
MissingDoc: MissingDoc,
AsyncFnInTrait: AsyncFnInTrait,
RedundantLifetimeArgs: RedundantLifetimeArgs,
]
]
);
Expand Down
177 changes: 177 additions & 0 deletions compiler/rustc_lint/src/redundant_lifetime_args.rs
@@ -0,0 +1,177 @@
#![allow(rustc::diagnostic_outside_of_impl)]
#![allow(rustc::untranslatable_diagnostic)]

use rustc_data_structures::fx::FxHashSet;
use rustc_hir as hir;
use rustc_hir::def::DefKind;
use rustc_infer::infer::outlives::env::OutlivesEnvironment;
use rustc_infer::infer::{SubregionOrigin, TyCtxtInferExt};
use rustc_macros::LintDiagnostic;
use rustc_middle::ty::{self, TyCtxt};
use rustc_session::lint::builtin::UNUSED_LIFETIMES;
use rustc_span::DUMMY_SP;
use rustc_trait_selection::traits::{outlives_bounds::InferCtxtExt, ObligationCtxt};

use crate::{LateContext, LateLintPass};

declare_lint_pass!(RedundantLifetimeArgs => []);

impl<'tcx> LateLintPass<'tcx> for RedundantLifetimeArgs {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'tcx>) {
check(cx.tcx, cx.param_env, item.owner_id);
}

fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'tcx>) {
check(cx.tcx, cx.param_env, item.owner_id);
}

fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'tcx>) {
if cx
.tcx
.hir()
.expect_item(cx.tcx.local_parent(item.owner_id.def_id))
.expect_impl()
.of_trait
.is_some()
{
// Don't check for redundant lifetimes for trait implementations,
// since the signature is required to be compatible with the trait.
return;
}

check(cx.tcx, cx.param_env, item.owner_id);
}
}

fn check<'tcx>(tcx: TyCtxt<'tcx>, param_env: ty::ParamEnv<'tcx>, owner_id: hir::OwnerId) {
let def_kind = tcx.def_kind(owner_id);
match def_kind {
DefKind::Struct
| DefKind::Union
| DefKind::Enum
| DefKind::Trait
| DefKind::TraitAlias
| DefKind::AssocTy
| DefKind::Fn
| DefKind::Const
| DefKind::AssocFn
| DefKind::AssocConst
| DefKind::Impl { of_trait: _ } => {
// Proceed
}
DefKind::Mod
| DefKind::Variant
| DefKind::TyAlias
| DefKind::ForeignTy
| DefKind::TyParam
| DefKind::ConstParam
| DefKind::Static(_)
| DefKind::Ctor(_, _)
| DefKind::Macro(_)
| DefKind::ExternCrate
| DefKind::Use
| DefKind::ForeignMod
| DefKind::AnonConst
| DefKind::InlineConst
| DefKind::OpaqueTy
| DefKind::Field
| DefKind::LifetimeParam
| DefKind::GlobalAsm
| DefKind::Closure => return,
}

let infcx = &tcx.infer_ctxt().build();
let ocx = ObligationCtxt::new(infcx);

// Compute the implied outlives bounds for the item. This ensures that we treat
// a signature with an argument like `&'a &'b ()` as implicitly having `'b: 'a`.
let Ok(assumed_wf_types) = ocx.assumed_wf_types(param_env, owner_id.def_id) else {
return;
};
let implied_bounds = infcx.implied_bounds_tys(param_env, owner_id.def_id, assumed_wf_types);
let outlives_env = &OutlivesEnvironment::with_bounds(param_env, implied_bounds);

// The ordering of this lifetime map is a bit subtle.
//
// Specifically, we want to find a "candidate" lifetime that precedes a "victim" lifetime,
// where we can prove that `'candidate = 'victim`.
//
// `'static` must come first in this list because we can never replace `'static` with
// something else, but if we find some lifetime `'a` where `'a = 'static`, we want to
// suggest replacing `'a` with `'static`.
let mut lifetimes = vec![tcx.lifetimes.re_static];
lifetimes.extend(
ty::GenericArgs::identity_for_item(tcx, owner_id).iter().filter_map(|arg| arg.as_region()),
);
// If we are in a function, add its late-bound lifetimes too.
if matches!(def_kind, DefKind::Fn | DefKind::AssocFn) {
for var in tcx.fn_sig(owner_id).instantiate_identity().bound_vars() {
let ty::BoundVariableKind::Region(kind) = var else { continue };
lifetimes.push(ty::Region::new_late_param(tcx, owner_id.to_def_id(), kind));
}
}

// Keep track of lifetimes which have already been replaced with other lifetimes.
// This makes sure that if `'a = 'b = 'c`, we don't say `'c` should be replaced by
// both `'a` and `'b`.
let mut shadowed = FxHashSet::default();

for (idx, &candidate) in lifetimes.iter().enumerate() {
// Don't suggest removing a lifetime twice.
if shadowed.contains(&candidate) {
continue;
}

// Can't rename a named lifetime named `'_` without ambiguity.
if !candidate.has_name() {
continue;
}

for &victim in &lifetimes[(idx + 1)..] {
// We only care about lifetimes that are "real", i.e. that have a def-id.
let (ty::ReEarlyParam(ty::EarlyParamRegion { def_id, .. })
| ty::ReLateParam(ty::LateParamRegion {
bound_region: ty::BoundRegionKind::BrNamed(def_id, _),
..
})) = victim.kind()
else {
continue;
};

// Do not rename lifetimes not local to this item since they'll overlap
// with the lint running on the parent. We still want to consider parent
// lifetimes which make child lifetimes redundant, otherwise we would
// have truncated the `identity_for_item` args above.
if tcx.parent(def_id) != owner_id.to_def_id() {
continue;
}

let infcx = infcx.fork();

// Require that `'candidate = 'victim`
infcx.sub_regions(SubregionOrigin::RelateRegionParamBound(DUMMY_SP), candidate, victim);
infcx.sub_regions(SubregionOrigin::RelateRegionParamBound(DUMMY_SP), victim, candidate);

// If there are no lifetime errors, then we have proven that `'candidate = 'victim`!
if infcx.resolve_regions(outlives_env).is_empty() {
shadowed.insert(victim);
tcx.emit_spanned_lint(
UNUSED_LIFETIMES,
tcx.local_def_id_to_hir_id(def_id.expect_local()),
tcx.def_span(def_id),
RedundantLifetimeArgsLint { candidate, victim },
);
}
}
}
}

#[derive(LintDiagnostic)]
#[diag(lint_redundant_lifetime_args)]
#[note]
struct RedundantLifetimeArgsLint<'tcx> {
/// The lifetime we have found to be redundant.
victim: ty::Region<'tcx>,
// The lifetime we can replace the victim with.
candidate: ty::Region<'tcx>,
}
8 changes: 7 additions & 1 deletion compiler/rustc_lint_defs/src/builtin.rs
Expand Up @@ -1532,14 +1532,20 @@ declare_lint! {

declare_lint! {
/// The `unused_lifetimes` lint detects lifetime parameters that are never
/// used.
/// used, or are redundant because they are equal to another named lifetime.
///
/// ### Example
///
/// ```rust,compile_fail
/// #[deny(unused_lifetimes)]
///
/// pub fn foo<'a>() {}
///
/// // `'a = 'static`, so all usages of `'a` can be replaced with `'static`
/// pub fn bar<'a: 'static>() {}
///
/// // `'a = 'b`, so all usages of `'b` can be replaced with `'a`
/// pub fn bar<'a: 'b, 'b: 'a>() {}
/// ```
///
/// {{produces}}
Expand Down
18 changes: 10 additions & 8 deletions compiler/rustc_middle/src/ty/diagnostics.rs
Expand Up @@ -5,24 +5,26 @@ use std::fmt::Write;
use std::ops::ControlFlow;

use crate::ty::{
AliasTy, Const, ConstKind, FallibleTypeFolder, InferConst, InferTy, Opaque, PolyTraitPredicate,
Projection, Ty, TyCtxt, TypeFoldable, TypeSuperFoldable, TypeSuperVisitable, TypeVisitable,
TypeVisitor,
self, AliasTy, Const, ConstKind, FallibleTypeFolder, InferConst, InferTy, Opaque,
PolyTraitPredicate, Projection, Ty, TyCtxt, TypeFoldable, TypeSuperFoldable,
TypeSuperVisitable, TypeVisitable, TypeVisitor,
};

use rustc_data_structures::fx::FxHashMap;
use rustc_errors::{Applicability, Diagnostic, DiagnosticArgValue, IntoDiagnosticArg};
use rustc_errors::{
into_diagnostic_arg_using_display, Applicability, Diagnostic, DiagnosticArgValue,
IntoDiagnosticArg,
};
use rustc_hir as hir;
use rustc_hir::def::DefKind;
use rustc_hir::def_id::DefId;
use rustc_hir::{PredicateOrigin, WherePredicate};
use rustc_span::{BytePos, Span};
use rustc_type_ir::TyKind::*;

impl<'tcx> IntoDiagnosticArg for Ty<'tcx> {
fn into_diagnostic_arg(self) -> DiagnosticArgValue<'static> {
self.to_string().into_diagnostic_arg()
}
into_diagnostic_arg_using_display! {
Ty<'_>,
ty::Region<'_>,
}

impl<'tcx> Ty<'tcx> {
Expand Down
@@ -1,8 +1,5 @@
#![warn(unused_lifetimes)]

pub trait X {
type Y<'a: 'static>;
//~^ WARNING unnecessary lifetime parameter
}

impl X for () {
Expand Down

0 comments on commit 58aa0f0

Please sign in to comment.