Don't use kw::Empty in hir::Lifetime::ident.

`hir::Lifetime::ident` currently sometimes uses `kw::Empty` for elided
lifetimes and sometimes uses `kw::UnderscoreLifetime`, and the
distinction is used when creating some error suggestions, e.g. in
`Lifetime::suggestion` and `ImplicitLifetimeFinder::visit_ty`. I found
this *really* confusing, and it took me a while to understand what was
going on.

This commit replaces all uses of `kw::Empty` in `hir::Lifetime::ident`
with `kw::UnderscoreLifetime`. It adds a new field
`hir::Lifetime::is_path_anon` that mostly replaces the old
empty/underscore distinction and makes things much clearer.

Some other notable changes:

- Adds a big comment to `Lifetime` talking about permissable field
  values.

- Adds some assertions in `new_named_lifetime` about what ident values
  are permissible for the different `LifetimeRes` values.

- Adds a `Lifetime::new` constructor that does some checking to make
  sure the `is_elided` and `is_anonymous` states are valid.

- `add_static_impl_trait_suggestion` now looks at `Lifetime::res`
  instead of the ident when creating the suggestion. This is the one
  case where `is_path_anon` doesn't replace the old empty/underscore
  distinction.

- A couple of minor pretty-printing improvements.
This commit is contained in:
Nicholas Nethercote 2025-03-25 08:21:28 +11:00
parent cfd00f9c16
commit 8d2c63f514
9 changed files with 136 additions and 51 deletions

View file

@ -35,20 +35,60 @@ use crate::def_id::{DefId, LocalDefIdMap};
pub(crate) use crate::hir_id::{HirId, ItemLocalId, ItemLocalMap, OwnerId};
use crate::intravisit::{FnKind, VisitorExt};
#[derive(Debug, Copy, Clone, PartialEq, Eq, HashStable_Generic)]
pub enum IsAnonInPath {
No,
Yes,
}
/// A lifetime. The valid field combinations are non-obvious. The following
/// example shows some of them. See also the comments on `LifetimeName`.
/// ```
/// #[repr(C)]
/// struct S<'a>(&'a u32); // res=Param, name='a, IsAnonInPath::No
/// unsafe extern "C" {
/// fn f1(s: S); // res=Param, name='_, IsAnonInPath::Yes
/// fn f2(s: S<'_>); // res=Param, name='_, IsAnonInPath::No
/// fn f3<'a>(s: S<'a>); // res=Param, name='a, IsAnonInPath::No
/// }
///
/// struct St<'a> { x: &'a u32 } // res=Param, name='a, IsAnonInPath::No
/// fn f() {
/// _ = St { x: &0 }; // res=Infer, name='_, IsAnonInPath::Yes
/// _ = St::<'_> { x: &0 }; // res=Infer, name='_, IsAnonInPath::No
/// }
///
/// struct Name<'a>(&'a str); // res=Param, name='a, IsAnonInPath::No
/// const A: Name = Name("a"); // res=Static, name='_, IsAnonInPath::Yes
/// const B: &str = ""; // res=Static, name='_, IsAnonInPath::No
/// static C: &'_ str = ""; // res=Static, name='_, IsAnonInPath::No
/// static D: &'static str = ""; // res=Static, name='static, IsAnonInPath::No
///
/// trait Tr {}
/// fn tr(_: Box<dyn Tr>) {} // res=ImplicitObjectLifetimeDefault, name='_, IsAnonInPath::No
///
/// // (commented out because these cases trigger errors)
/// // struct S1<'a>(&'a str); // res=Param, name='a, IsAnonInPath::No
/// // struct S2(S1); // res=Error, name='_, IsAnonInPath::Yes
/// // struct S3(S1<'_>); // res=Error, name='_, IsAnonInPath::No
/// // struct S4(S1<'a>); // res=Error, name='a, IsAnonInPath::No
/// ```
#[derive(Debug, Copy, Clone, HashStable_Generic)]
pub struct Lifetime {
#[stable_hasher(ignore)]
pub hir_id: HirId,
/// Either "`'a`", referring to a named lifetime definition,
/// `'_` referring to an anonymous lifetime (either explicitly `'_` or `&type`),
/// or "``" (i.e., `kw::Empty`) when appearing in path.
///
/// See `Lifetime::suggestion_position` for practical use.
/// Either a named lifetime definition (e.g. `'a`, `'static`) or an
/// anonymous lifetime (`'_`, either explicitly written, or inserted for
/// things like `&type`).
pub ident: Ident,
/// Semantics of this lifetime.
pub res: LifetimeName,
/// Is the lifetime anonymous and in a path? Used only for error
/// suggestions. See `Lifetime::suggestion` for example use.
pub is_anon_in_path: IsAnonInPath,
}
#[derive(Debug, Copy, Clone, HashStable_Generic)]
@ -111,11 +151,12 @@ pub enum LifetimeName {
/// that was already reported.
Error,
/// User wrote an anonymous lifetime, either `'_` or nothing.
/// The semantics of this lifetime should be inferred by typechecking code.
/// User wrote an anonymous lifetime, either `'_` or nothing (which gets
/// converted to `'_`). The semantics of this lifetime should be inferred
/// by typechecking code.
Infer,
/// User wrote `'static`.
/// User wrote `'static` or nothing (which gets converted to `'_`).
Static,
}
@ -135,34 +176,56 @@ impl LifetimeName {
impl fmt::Display for Lifetime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.ident.name != kw::Empty { self.ident.name.fmt(f) } else { "'_".fmt(f) }
self.ident.name.fmt(f)
}
}
impl Lifetime {
pub fn new(
hir_id: HirId,
ident: Ident,
res: LifetimeName,
is_anon_in_path: IsAnonInPath,
) -> Lifetime {
let lifetime = Lifetime { hir_id, ident, res, is_anon_in_path };
// Sanity check: elided lifetimes form a strict subset of anonymous lifetimes.
#[cfg(debug_assertions)]
match (lifetime.is_elided(), lifetime.is_anonymous()) {
(false, false) => {} // e.g. `'a`
(false, true) => {} // e.g. explicit `'_`
(true, true) => {} // e.g. `&x`
(true, false) => panic!("bad Lifetime"),
}
lifetime
}
pub fn is_elided(&self) -> bool {
self.res.is_elided()
}
pub fn is_anonymous(&self) -> bool {
self.ident.name == kw::Empty || self.ident.name == kw::UnderscoreLifetime
self.ident.name == kw::UnderscoreLifetime
}
pub fn suggestion(&self, new_lifetime: &str) -> (Span, String) {
debug_assert!(new_lifetime.starts_with('\''));
match (self.ident.name.is_empty(), self.ident.span.is_empty()) {
match (self.is_anon_in_path, self.ident.span.is_empty()) {
// The user wrote `Path<T>`, and omitted the `'_,`.
(true, true) => (self.ident.span, format!("{new_lifetime}, ")),
(IsAnonInPath::Yes, true) => (self.ident.span, format!("{new_lifetime}, ")),
// The user wrote `Path` and omitted the `<'_>`.
(true, false) => (self.ident.span.shrink_to_hi(), format!("<{new_lifetime}>")),
(IsAnonInPath::Yes, false) => {
(self.ident.span.shrink_to_hi(), format!("<{new_lifetime}>"))
}
// The user wrote `&type` or `&mut type`.
(false, true) => (self.ident.span, format!("{new_lifetime} ")),
(IsAnonInPath::No, true) => (self.ident.span, format!("{new_lifetime} ")),
// The user wrote `'a` or `'_`.
(false, false) => (self.ident.span, format!("{new_lifetime}")),
(IsAnonInPath::No, false) => (self.ident.span, format!("{new_lifetime}")),
}
}
}

View file

@ -58,6 +58,7 @@ fn trait_object_roundtrips_impl(syntax: TraitObjectSyntax) {
hir_id: HirId::INVALID,
ident: Ident::new(sym::name, DUMMY_SP),
res: LifetimeName::Static,
is_anon_in_path: IsAnonInPath::No,
}
},
syntax,