Rollup merge of #126452 - compiler-errors:raw-lifetimes, r=spastorino

Implement raw lifetimes and labels (`'r#ident`)

This PR does two things:
1. Reserve lifetime prefixes, e.g. `'prefix#lt` in edition 2021.
2. Implements raw lifetimes, e.g. `'r#async` in edition 2021.

This PR additionally extends the `keyword_idents_2024` lint to also check lifetimes.

cc `@traviscross`
r? parser
This commit is contained in:
Matthias Krüger 2024-09-07 23:30:10 +02:00 committed by GitHub
commit ccf3f6e59d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 373 additions and 85 deletions

View File

@ -752,7 +752,7 @@ fn visit_lazy_tts<T: MutVisitor>(vis: &mut T, lazy_tts: &mut Option<LazyAttrToke
pub fn visit_token<T: MutVisitor>(vis: &mut T, t: &mut Token) { pub fn visit_token<T: MutVisitor>(vis: &mut T, t: &mut Token) {
let Token { kind, span } = t; let Token { kind, span } = t;
match kind { match kind {
token::Ident(name, _ /*raw*/) | token::Lifetime(name) => { token::Ident(name, _is_raw) | token::Lifetime(name, _is_raw) => {
let mut ident = Ident::new(*name, *span); let mut ident = Ident::new(*name, *span);
vis.visit_ident(&mut ident); vis.visit_ident(&mut ident);
*name = ident.name; *name = ident.name;
@ -762,7 +762,7 @@ pub fn visit_token<T: MutVisitor>(vis: &mut T, t: &mut Token) {
token::NtIdent(ident, _is_raw) => { token::NtIdent(ident, _is_raw) => {
vis.visit_ident(ident); vis.visit_ident(ident);
} }
token::NtLifetime(ident) => { token::NtLifetime(ident, _is_raw) => {
vis.visit_ident(ident); vis.visit_ident(ident);
} }
token::Interpolated(nt) => { token::Interpolated(nt) => {

View File

@ -331,11 +331,11 @@ pub enum TokenKind {
/// Do not forget about `NtLifetime` when you want to match on lifetime identifiers. /// Do not forget about `NtLifetime` when you want to match on lifetime identifiers.
/// It's recommended to use `Token::(lifetime,uninterpolate,uninterpolated_span)` to /// It's recommended to use `Token::(lifetime,uninterpolate,uninterpolated_span)` to
/// treat regular and interpolated lifetime identifiers in the same way. /// treat regular and interpolated lifetime identifiers in the same way.
Lifetime(Symbol), Lifetime(Symbol, IdentIsRaw),
/// This identifier (and its span) is the lifetime passed to the /// This identifier (and its span) is the lifetime passed to the
/// declarative macro. The span in the surrounding `Token` is the span of /// declarative macro. The span in the surrounding `Token` is the span of
/// the `lifetime` metavariable in the macro's RHS. /// the `lifetime` metavariable in the macro's RHS.
NtLifetime(Ident), NtLifetime(Ident, IdentIsRaw),
/// An embedded AST node, as produced by a macro. This only exists for /// An embedded AST node, as produced by a macro. This only exists for
/// historical reasons. We'd like to get rid of it, for multiple reasons. /// historical reasons. We'd like to get rid of it, for multiple reasons.
@ -458,7 +458,7 @@ impl Token {
/// if they keep spans or perform edition checks. /// if they keep spans or perform edition checks.
pub fn uninterpolated_span(&self) -> Span { pub fn uninterpolated_span(&self) -> Span {
match self.kind { match self.kind {
NtIdent(ident, _) | NtLifetime(ident) => ident.span, NtIdent(ident, _) | NtLifetime(ident, _) => ident.span,
Interpolated(ref nt) => nt.use_span(), Interpolated(ref nt) => nt.use_span(),
_ => self.span, _ => self.span,
} }
@ -661,7 +661,9 @@ impl Token {
pub fn uninterpolate(&self) -> Cow<'_, Token> { pub fn uninterpolate(&self) -> Cow<'_, Token> {
match self.kind { match self.kind {
NtIdent(ident, is_raw) => Cow::Owned(Token::new(Ident(ident.name, is_raw), ident.span)), NtIdent(ident, is_raw) => Cow::Owned(Token::new(Ident(ident.name, is_raw), ident.span)),
NtLifetime(ident) => Cow::Owned(Token::new(Lifetime(ident.name), ident.span)), NtLifetime(ident, is_raw) => {
Cow::Owned(Token::new(Lifetime(ident.name, is_raw), ident.span))
}
_ => Cow::Borrowed(self), _ => Cow::Borrowed(self),
} }
} }
@ -679,11 +681,11 @@ impl Token {
/// Returns a lifetime identifier if this token is a lifetime. /// Returns a lifetime identifier if this token is a lifetime.
#[inline] #[inline]
pub fn lifetime(&self) -> Option<Ident> { pub fn lifetime(&self) -> Option<(Ident, IdentIsRaw)> {
// We avoid using `Token::uninterpolate` here because it's slow. // We avoid using `Token::uninterpolate` here because it's slow.
match self.kind { match self.kind {
Lifetime(name) => Some(Ident::new(name, self.span)), Lifetime(name, is_raw) => Some((Ident::new(name, self.span), is_raw)),
NtLifetime(ident) => Some(ident), NtLifetime(ident, is_raw) => Some((ident, is_raw)),
_ => None, _ => None,
} }
} }
@ -865,7 +867,7 @@ impl Token {
_ => return None, _ => return None,
}, },
SingleQuote => match joint.kind { SingleQuote => match joint.kind {
Ident(name, IdentIsRaw::No) => Lifetime(Symbol::intern(&format!("'{name}"))), Ident(name, is_raw) => Lifetime(Symbol::intern(&format!("'{name}")), is_raw),
_ => return None, _ => return None,
}, },

View File

@ -482,11 +482,11 @@ impl TokenStream {
token::NtIdent(ident, is_raw) => { token::NtIdent(ident, is_raw) => {
TokenTree::Token(Token::new(token::Ident(ident.name, is_raw), ident.span), spacing) TokenTree::Token(Token::new(token::Ident(ident.name, is_raw), ident.span), spacing)
} }
token::NtLifetime(ident) => TokenTree::Delimited( token::NtLifetime(ident, is_raw) => TokenTree::Delimited(
DelimSpan::from_single(token.span), DelimSpan::from_single(token.span),
DelimSpacing::new(Spacing::JointHidden, spacing), DelimSpacing::new(Spacing::JointHidden, spacing),
Delimiter::Invisible, Delimiter::Invisible,
TokenStream::token_alone(token::Lifetime(ident.name), ident.span), TokenStream::token_alone(token::Lifetime(ident.name, is_raw), ident.span),
), ),
token::Interpolated(ref nt) => TokenTree::Delimited( token::Interpolated(ref nt) => TokenTree::Delimited(
DelimSpan::from_single(token.span), DelimSpan::from_single(token.span),

View File

@ -11,7 +11,9 @@ use std::borrow::Cow;
use ast::TraitBoundModifiers; use ast::TraitBoundModifiers;
use rustc_ast::attr::AttrIdGenerator; use rustc_ast::attr::AttrIdGenerator;
use rustc_ast::ptr::P; use rustc_ast::ptr::P;
use rustc_ast::token::{self, BinOpToken, CommentKind, Delimiter, Nonterminal, Token, TokenKind}; use rustc_ast::token::{
self, BinOpToken, CommentKind, Delimiter, IdentIsRaw, Nonterminal, Token, TokenKind,
};
use rustc_ast::tokenstream::{Spacing, TokenStream, TokenTree}; use rustc_ast::tokenstream::{Spacing, TokenStream, TokenTree};
use rustc_ast::util::classify; use rustc_ast::util::classify;
use rustc_ast::util::comments::{Comment, CommentStyle}; use rustc_ast::util::comments::{Comment, CommentStyle};
@ -947,8 +949,13 @@ pub trait PrintState<'a>: std::ops::Deref<Target = pp::Printer> + std::ops::Dere
token::NtIdent(ident, is_raw) => { token::NtIdent(ident, is_raw) => {
IdentPrinter::for_ast_ident(ident, is_raw.into()).to_string().into() IdentPrinter::for_ast_ident(ident, is_raw.into()).to_string().into()
} }
token::Lifetime(name) => name.to_string().into(),
token::NtLifetime(ident) => ident.name.to_string().into(), token::Lifetime(name, IdentIsRaw::No)
| token::NtLifetime(Ident { name, .. }, IdentIsRaw::No) => name.to_string().into(),
token::Lifetime(name, IdentIsRaw::Yes)
| token::NtLifetime(Ident { name, .. }, IdentIsRaw::Yes) => {
format!("'r#{}", &name.as_str()[1..]).into()
}
/* Other */ /* Other */
token::DocComment(comment_kind, attr_style, data) => { token::DocComment(comment_kind, attr_style, data) => {

View File

@ -398,8 +398,10 @@ pub(crate) enum NamedMatch {
fn token_name_eq(t1: &Token, t2: &Token) -> bool { fn token_name_eq(t1: &Token, t2: &Token) -> bool {
if let (Some((ident1, is_raw1)), Some((ident2, is_raw2))) = (t1.ident(), t2.ident()) { if let (Some((ident1, is_raw1)), Some((ident2, is_raw2))) = (t1.ident(), t2.ident()) {
ident1.name == ident2.name && is_raw1 == is_raw2 ident1.name == ident2.name && is_raw1 == is_raw2
} else if let (Some(ident1), Some(ident2)) = (t1.lifetime(), t2.lifetime()) { } else if let (Some((ident1, is_raw1)), Some((ident2, is_raw2))) =
ident1.name == ident2.name (t1.lifetime(), t2.lifetime())
{
ident1.name == ident2.name && is_raw1 == is_raw2
} else { } else {
t1.kind == t2.kind t1.kind == t2.kind
} }

View File

@ -283,9 +283,9 @@ pub(super) fn transcribe<'a>(
let kind = token::NtIdent(*ident, *is_raw); let kind = token::NtIdent(*ident, *is_raw);
TokenTree::token_alone(kind, sp) TokenTree::token_alone(kind, sp)
} }
MatchedSingle(ParseNtResult::Lifetime(ident)) => { MatchedSingle(ParseNtResult::Lifetime(ident, is_raw)) => {
marker.visit_span(&mut sp); marker.visit_span(&mut sp);
let kind = token::NtLifetime(*ident); let kind = token::NtLifetime(*ident, *is_raw);
TokenTree::token_alone(kind, sp) TokenTree::token_alone(kind, sp)
} }
MatchedSingle(ParseNtResult::Nt(nt)) => { MatchedSingle(ParseNtResult::Nt(nt)) => {

View File

@ -229,15 +229,16 @@ impl FromInternal<(TokenStream, &mut Rustc<'_, '_>)> for Vec<TokenTree<TokenStre
span: ident.span, span: ident.span,
})), })),
Lifetime(name) => { Lifetime(name, is_raw) => {
let ident = symbol::Ident::new(name, span).without_first_quote(); let ident = symbol::Ident::new(name, span).without_first_quote();
trees.extend([ trees.extend([
TokenTree::Punct(Punct { ch: b'\'', joint: true, span }), TokenTree::Punct(Punct { ch: b'\'', joint: true, span }),
TokenTree::Ident(Ident { sym: ident.name, is_raw: false, span }), TokenTree::Ident(Ident { sym: ident.name, is_raw: is_raw.into(), span }),
]); ]);
} }
NtLifetime(ident) => { NtLifetime(ident, is_raw) => {
let stream = TokenStream::token_alone(token::Lifetime(ident.name), ident.span); let stream =
TokenStream::token_alone(token::Lifetime(ident.name, is_raw), ident.span);
trees.push(TokenTree::Group(Group { trees.push(TokenTree::Group(Group {
delimiter: pm::Delimiter::None, delimiter: pm::Delimiter::None,
stream: Some(stream), stream: Some(stream),

View File

@ -91,6 +91,15 @@ pub enum TokenKind {
/// tokens. /// tokens.
UnknownPrefix, UnknownPrefix,
/// An unknown prefix in a lifetime, like `'foo#`.
///
/// Note that like above, only the `'` and prefix are included in the token
/// and not the separator.
UnknownPrefixLifetime,
/// `'r#lt`, which in edition < 2021 is split into several tokens: `'r # lt`.
RawLifetime,
/// Similar to the above, but *always* an error on every edition. This is used /// Similar to the above, but *always* an error on every edition. This is used
/// for emoji identifier recovery, as those are not meant to be ever accepted. /// for emoji identifier recovery, as those are not meant to be ever accepted.
InvalidPrefix, InvalidPrefix,
@ -677,9 +686,17 @@ impl Cursor<'_> {
return Literal { kind, suffix_start }; return Literal { kind, suffix_start };
} }
if self.first() == 'r' && self.second() == '#' && is_id_start(self.third()) {
// Eat "r" and `#`, and identifier start characters.
self.bump();
self.bump();
self.bump();
self.eat_while(is_id_continue);
return RawLifetime;
}
// Either a lifetime or a character literal with // Either a lifetime or a character literal with
// length greater than 1. // length greater than 1.
let starts_with_number = self.first().is_ascii_digit(); let starts_with_number = self.first().is_ascii_digit();
// Skip the literal contents. // Skip the literal contents.
@ -688,15 +705,17 @@ impl Cursor<'_> {
self.bump(); self.bump();
self.eat_while(is_id_continue); self.eat_while(is_id_continue);
// Check if after skipping literal contents we've met a closing match self.first() {
// single quote (which means that user attempted to create a // Check if after skipping literal contents we've met a closing
// string with single quotes). // single quote (which means that user attempted to create a
if self.first() == '\'' { // string with single quotes).
self.bump(); '\'' => {
let kind = Char { terminated: true }; self.bump();
Literal { kind, suffix_start: self.pos_within_token() } let kind = Char { terminated: true };
} else { Literal { kind, suffix_start: self.pos_within_token() }
Lifetime { starts_with_number } }
'#' if !starts_with_number => UnknownPrefixLifetime,
_ => Lifetime { starts_with_number },
} }
} }

View File

@ -708,6 +708,10 @@ lint_range_endpoint_out_of_range = range endpoint is out of range for `{$ty}`
lint_range_use_inclusive_range = use an inclusive range instead lint_range_use_inclusive_range = use an inclusive range instead
lint_raw_prefix = prefix `'r` is reserved
.label = reserved prefix
.suggestion = insert whitespace here to avoid this being parsed as a prefix in Rust 2021
lint_reason_must_be_string_literal = reason must be a string literal 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_reason_must_come_last = reason in lint attribute must come last

View File

@ -1851,9 +1851,16 @@ impl KeywordIdents {
TokenTree::Token(token, _) => { TokenTree::Token(token, _) => {
if let Some((ident, token::IdentIsRaw::No)) = token.ident() { if let Some((ident, token::IdentIsRaw::No)) = token.ident() {
if !prev_dollar { if !prev_dollar {
self.check_ident_token(cx, UnderMacro(true), ident); self.check_ident_token(cx, UnderMacro(true), ident, "");
} }
} else if *token == TokenKind::Dollar { } else if let Some((ident, token::IdentIsRaw::No)) = token.lifetime() {
self.check_ident_token(
cx,
UnderMacro(true),
ident.without_first_quote(),
"'",
);
} else if token.kind == TokenKind::Dollar {
prev_dollar = true; prev_dollar = true;
continue; continue;
} }
@ -1869,6 +1876,7 @@ impl KeywordIdents {
cx: &EarlyContext<'_>, cx: &EarlyContext<'_>,
UnderMacro(under_macro): UnderMacro, UnderMacro(under_macro): UnderMacro,
ident: Ident, ident: Ident,
prefix: &'static str,
) { ) {
let (lint, edition) = match ident.name { let (lint, edition) = match ident.name {
kw::Async | kw::Await | kw::Try => (KEYWORD_IDENTS_2018, Edition::Edition2018), kw::Async | kw::Await | kw::Try => (KEYWORD_IDENTS_2018, Edition::Edition2018),
@ -1902,7 +1910,7 @@ impl KeywordIdents {
cx.emit_span_lint( cx.emit_span_lint(
lint, lint,
ident.span, ident.span,
BuiltinKeywordIdents { kw: ident, next: edition, suggestion: ident.span }, BuiltinKeywordIdents { kw: ident, next: edition, suggestion: ident.span, prefix },
); );
} }
} }
@ -1915,7 +1923,11 @@ impl EarlyLintPass for KeywordIdents {
self.check_tokens(cx, &mac.args.tokens); self.check_tokens(cx, &mac.args.tokens);
} }
fn check_ident(&mut self, cx: &EarlyContext<'_>, ident: Ident) { fn check_ident(&mut self, cx: &EarlyContext<'_>, ident: Ident) {
self.check_ident_token(cx, UnderMacro(false), ident); if ident.name.as_str().starts_with('\'') {
self.check_ident_token(cx, UnderMacro(false), ident.without_first_quote(), "'");
} else {
self.check_ident_token(cx, UnderMacro(false), ident, "");
}
} }
} }

View File

@ -172,6 +172,10 @@ pub(super) fn decorate_lint(sess: &Session, diagnostic: BuiltinLintDiag, diag: &
} }
.decorate_lint(diag); .decorate_lint(diag);
} }
BuiltinLintDiag::RawPrefix(label_span) => {
lints::RawPrefix { label: label_span, suggestion: label_span.shrink_to_hi() }
.decorate_lint(diag);
}
BuiltinLintDiag::UnusedBuiltinAttribute { attr_name, macro_name, invoc_span } => { BuiltinLintDiag::UnusedBuiltinAttribute { attr_name, macro_name, invoc_span } => {
lints::UnusedBuiltinAttribute { invoc_span, attr_name, macro_name }.decorate_lint(diag); lints::UnusedBuiltinAttribute { invoc_span, attr_name, macro_name }.decorate_lint(diag);
} }

View File

@ -363,8 +363,9 @@ pub(crate) enum BuiltinEllipsisInclusiveRangePatternsLint {
pub(crate) struct BuiltinKeywordIdents { pub(crate) struct BuiltinKeywordIdents {
pub kw: Ident, pub kw: Ident,
pub next: Edition, pub next: Edition,
#[suggestion(code = "r#{kw}", applicability = "machine-applicable")] #[suggestion(code = "{prefix}r#{kw}", applicability = "machine-applicable")]
pub suggestion: Span, pub suggestion: Span,
pub prefix: &'static str,
} }
#[derive(LintDiagnostic)] #[derive(LintDiagnostic)]
@ -2814,6 +2815,15 @@ pub(crate) struct ReservedPrefix {
pub prefix: String, pub prefix: String,
} }
#[derive(LintDiagnostic)]
#[diag(lint_raw_prefix)]
pub(crate) struct RawPrefix {
#[label]
pub label: Span,
#[suggestion(code = " ", applicability = "machine-applicable")]
pub suggestion: Span,
}
#[derive(LintDiagnostic)] #[derive(LintDiagnostic)]
#[diag(lint_unused_builtin_attribute)] #[diag(lint_unused_builtin_attribute)]
pub(crate) struct UnusedBuiltinAttribute { pub(crate) struct UnusedBuiltinAttribute {

View File

@ -612,6 +612,8 @@ pub enum BuiltinLintDiag {
LegacyDeriveHelpers(Span), LegacyDeriveHelpers(Span),
OrPatternsBackCompat(Span, String), OrPatternsBackCompat(Span, String),
ReservedPrefix(Span, String), ReservedPrefix(Span, String),
/// `'r#` in edition < 2021.
RawPrefix(Span),
TrailingMacro(bool, Ident), TrailingMacro(bool, Ident),
BreakWithLabelAndLoop(Span), BreakWithLabelAndLoop(Span),
UnicodeTextFlow(Span, String), UnicodeTextFlow(Span, String),

View File

@ -13,7 +13,6 @@ use rustc_session::lint::builtin::{
}; };
use rustc_session::lint::BuiltinLintDiag; use rustc_session::lint::BuiltinLintDiag;
use rustc_session::parse::ParseSess; use rustc_session::parse::ParseSess;
use rustc_span::edition::Edition;
use rustc_span::symbol::Symbol; use rustc_span::symbol::Symbol;
use rustc_span::{BytePos, Pos, Span}; use rustc_span::{BytePos, Pos, Span};
use tracing::debug; use tracing::debug;
@ -188,9 +187,7 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
preceded_by_whitespace = true; preceded_by_whitespace = true;
continue; continue;
} }
rustc_lexer::TokenKind::Ident => { rustc_lexer::TokenKind::Ident => self.ident(start),
self.ident(start)
}
rustc_lexer::TokenKind::RawIdent => { rustc_lexer::TokenKind::RawIdent => {
let sym = nfc_normalize(self.str_from(start + BytePos(2))); let sym = nfc_normalize(self.str_from(start + BytePos(2)));
let span = self.mk_sp(start, self.pos); let span = self.mk_sp(start, self.pos);
@ -205,20 +202,31 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
self.report_unknown_prefix(start); self.report_unknown_prefix(start);
self.ident(start) self.ident(start)
} }
rustc_lexer::TokenKind::InvalidIdent rustc_lexer::TokenKind::UnknownPrefixLifetime => {
| rustc_lexer::TokenKind::InvalidPrefix self.report_unknown_prefix(start);
// Include the leading `'` in the real identifier, for macro
// expansion purposes. See #12512 for the gory details of why
// this is necessary.
let lifetime_name = self.str_from(start);
self.last_lifetime = Some(self.mk_sp(start, start + BytePos(1)));
let ident = Symbol::intern(lifetime_name);
token::Lifetime(ident, IdentIsRaw::No)
}
rustc_lexer::TokenKind::InvalidIdent | rustc_lexer::TokenKind::InvalidPrefix
// Do not recover an identifier with emoji if the codepoint is a confusable // Do not recover an identifier with emoji if the codepoint is a confusable
// with a recoverable substitution token, like ``. // with a recoverable substitution token, like ``.
if !UNICODE_ARRAY if !UNICODE_ARRAY.iter().any(|&(c, _, _)| {
.iter() let sym = self.str_from(start);
.any(|&(c, _, _)| { sym.chars().count() == 1 && c == sym.chars().next().unwrap()
let sym = self.str_from(start); }) =>
sym.chars().count() == 1 && c == sym.chars().next().unwrap()
}) =>
{ {
let sym = nfc_normalize(self.str_from(start)); let sym = nfc_normalize(self.str_from(start));
let span = self.mk_sp(start, self.pos); let span = self.mk_sp(start, self.pos);
self.psess.bad_unicode_identifiers.borrow_mut().entry(sym).or_default() self.psess
.bad_unicode_identifiers
.borrow_mut()
.entry(sym)
.or_default()
.push(span); .push(span);
token::Ident(sym, IdentIsRaw::No) token::Ident(sym, IdentIsRaw::No)
} }
@ -249,9 +257,9 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
let suffix = if suffix_start < self.pos { let suffix = if suffix_start < self.pos {
let string = self.str_from(suffix_start); let string = self.str_from(suffix_start);
if string == "_" { if string == "_" {
self self.dcx().emit_err(errors::UnderscoreLiteralSuffix {
.dcx() span: self.mk_sp(suffix_start, self.pos),
.emit_err(errors::UnderscoreLiteralSuffix { span: self.mk_sp(suffix_start, self.pos) }); });
None None
} else { } else {
Some(Symbol::intern(string)) Some(Symbol::intern(string))
@ -269,12 +277,47 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
self.last_lifetime = Some(self.mk_sp(start, start + BytePos(1))); self.last_lifetime = Some(self.mk_sp(start, start + BytePos(1)));
if starts_with_number { if starts_with_number {
let span = self.mk_sp(start, self.pos); let span = self.mk_sp(start, self.pos);
self.dcx().struct_err("lifetimes cannot start with a number") self.dcx()
.struct_err("lifetimes cannot start with a number")
.with_span(span) .with_span(span)
.stash(span, StashKey::LifetimeIsChar); .stash(span, StashKey::LifetimeIsChar);
} }
let ident = Symbol::intern(lifetime_name); let ident = Symbol::intern(lifetime_name);
token::Lifetime(ident) token::Lifetime(ident, IdentIsRaw::No)
}
rustc_lexer::TokenKind::RawLifetime => {
self.last_lifetime = Some(self.mk_sp(start, start + BytePos(1)));
let ident_start = start + BytePos(3);
let prefix_span = self.mk_sp(start, ident_start);
if prefix_span.at_least_rust_2021() {
let lifetime_name_without_tick = self.str_from(ident_start);
// Put the `'` back onto the lifetime name.
let mut lifetime_name = String::with_capacity(lifetime_name_without_tick.len() + 1);
lifetime_name.push('\'');
lifetime_name += lifetime_name_without_tick;
let sym = Symbol::intern(&lifetime_name);
token::Lifetime(sym, IdentIsRaw::Yes)
} else {
// Otherwise, this should be parsed like `'r`. Warn about it though.
self.psess.buffer_lint(
RUST_2021_PREFIXES_INCOMPATIBLE_SYNTAX,
prefix_span,
ast::CRATE_NODE_ID,
BuiltinLintDiag::RawPrefix(prefix_span),
);
// Reset the state so we just lex the `'r`.
let lt_start = start + BytePos(2);
self.pos = lt_start;
self.cursor = Cursor::new(&str_before[2 as usize..]);
let lifetime_name = self.str_from(start);
let ident = Symbol::intern(lifetime_name);
token::Lifetime(ident, IdentIsRaw::No)
}
} }
rustc_lexer::TokenKind::Semi => token::Semi, rustc_lexer::TokenKind::Semi => token::Semi,
rustc_lexer::TokenKind::Comma => token::Comma, rustc_lexer::TokenKind::Comma => token::Comma,
@ -331,16 +374,19 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
// first remove compound tokens like `<<` from `rustc_lexer`, and then add // first remove compound tokens like `<<` from `rustc_lexer`, and then add
// fancier error recovery to it, as there will be less overall work to do this // fancier error recovery to it, as there will be less overall work to do this
// way. // way.
let (token, sugg) = unicode_chars::check_for_substitution(self, start, c, repeats+1); let (token, sugg) =
unicode_chars::check_for_substitution(self, start, c, repeats + 1);
self.dcx().emit_err(errors::UnknownTokenStart { self.dcx().emit_err(errors::UnknownTokenStart {
span: self.mk_sp(start, self.pos + Pos::from_usize(repeats * c.len_utf8())), span: self.mk_sp(start, self.pos + Pos::from_usize(repeats * c.len_utf8())),
escaped: escaped_char(c), escaped: escaped_char(c),
sugg, sugg,
null: if c == '\x00' {Some(errors::UnknownTokenNull)} else {None}, null: if c == '\x00' { Some(errors::UnknownTokenNull) } else { None },
repeat: if repeats > 0 { repeat: if repeats > 0 {
swallow_next_invalid = repeats; swallow_next_invalid = repeats;
Some(errors::UnknownTokenRepeat { repeats }) Some(errors::UnknownTokenRepeat { repeats })
} else {None} } else {
None
},
}); });
if let Some(token) = token { if let Some(token) = token {
@ -699,7 +745,7 @@ impl<'psess, 'src> StringReader<'psess, 'src> {
let expn_data = prefix_span.ctxt().outer_expn_data(); let expn_data = prefix_span.ctxt().outer_expn_data();
if expn_data.edition >= Edition::Edition2021 { if expn_data.edition.at_least_rust_2021() {
// In Rust 2021, this is a hard error. // In Rust 2021, this is a hard error.
let sugg = if prefix == "rb" { let sugg = if prefix == "rb" {
Some(errors::UnknownPrefixSugg::UseBr(prefix_span)) Some(errors::UnknownPrefixSugg::UseBr(prefix_span))

View File

@ -2050,7 +2050,7 @@ impl<'a> Parser<'a> {
}; };
// On an error path, eagerly consider a lifetime to be an unclosed character lit, if that // On an error path, eagerly consider a lifetime to be an unclosed character lit, if that
// makes sense. // makes sense.
if let Some(ident) = self.token.lifetime() if let Some((ident, IdentIsRaw::No)) = self.token.lifetime()
&& could_be_unclosed_char_literal(ident) && could_be_unclosed_char_literal(ident)
{ {
let lt = self.expect_lifetime(); let lt = self.expect_lifetime();
@ -2925,9 +2925,9 @@ impl<'a> Parser<'a> {
} }
pub(crate) fn eat_label(&mut self) -> Option<Label> { pub(crate) fn eat_label(&mut self) -> Option<Label> {
if let Some(ident) = self.token.lifetime() { if let Some((ident, is_raw)) = self.token.lifetime() {
// Disallow `'fn`, but with a better error message than `expect_lifetime`. // Disallow `'fn`, but with a better error message than `expect_lifetime`.
if ident.without_first_quote().is_reserved() { if matches!(is_raw, IdentIsRaw::No) && ident.without_first_quote().is_reserved() {
self.dcx().emit_err(errors::InvalidLabel { span: ident.span, name: ident.name }); self.dcx().emit_err(errors::InvalidLabel { span: ident.span, name: ident.name });
} }

View File

@ -1666,7 +1666,7 @@ enum FlatToken {
pub enum ParseNtResult { pub enum ParseNtResult {
Tt(TokenTree), Tt(TokenTree),
Ident(Ident, IdentIsRaw), Ident(Ident, IdentIsRaw),
Lifetime(Ident), Lifetime(Ident, IdentIsRaw),
/// This case will eventually be removed, along with `Token::Interpolate`. /// This case will eventually be removed, along with `Token::Interpolate`.
Nt(Lrc<Nonterminal>), Nt(Lrc<Nonterminal>),

View File

@ -88,7 +88,7 @@ impl<'a> Parser<'a> {
}, },
NonterminalKind::Pat(pat_kind) => token.can_begin_pattern(pat_kind), NonterminalKind::Pat(pat_kind) => token.can_begin_pattern(pat_kind),
NonterminalKind::Lifetime => match &token.kind { NonterminalKind::Lifetime => match &token.kind {
token::Lifetime(_) | token::NtLifetime(..) => true, token::Lifetime(..) | token::NtLifetime(..) => true,
_ => false, _ => false,
}, },
NonterminalKind::TT | NonterminalKind::Item | NonterminalKind::Stmt => { NonterminalKind::TT | NonterminalKind::Item | NonterminalKind::Stmt => {
@ -171,9 +171,9 @@ impl<'a> Parser<'a> {
NonterminalKind::Lifetime => { NonterminalKind::Lifetime => {
// We want to keep `'keyword` parsing, just like `keyword` is still // We want to keep `'keyword` parsing, just like `keyword` is still
// an ident for nonterminal purposes. // an ident for nonterminal purposes.
return if let Some(ident) = self.token.lifetime() { return if let Some((ident, is_raw)) = self.token.lifetime() {
self.bump(); self.bump();
Ok(ParseNtResult::Lifetime(ident)) Ok(ParseNtResult::Lifetime(ident, is_raw))
} else { } else {
Err(self.dcx().create_err(UnexpectedNonterminal::Lifetime { Err(self.dcx().create_err(UnexpectedNonterminal::Lifetime {
span: self.token.span, span: self.token.span,

View File

@ -1,6 +1,6 @@
use rustc_ast::mut_visit::{walk_pat, MutVisitor}; use rustc_ast::mut_visit::{walk_pat, MutVisitor};
use rustc_ast::ptr::P; use rustc_ast::ptr::P;
use rustc_ast::token::{self, BinOpToken, Delimiter, Token}; use rustc_ast::token::{self, BinOpToken, Delimiter, IdentIsRaw, Token};
use rustc_ast::{ use rustc_ast::{
self as ast, AttrVec, BindingMode, ByRef, Expr, ExprKind, MacCall, Mutability, Pat, PatField, self as ast, AttrVec, BindingMode, ByRef, Expr, ExprKind, MacCall, Mutability, Pat, PatField,
PatFieldsRest, PatKind, Path, QSelf, RangeEnd, RangeSyntax, PatFieldsRest, PatKind, Path, QSelf, RangeEnd, RangeSyntax,
@ -548,7 +548,7 @@ impl<'a> Parser<'a> {
None => PatKind::Path(qself, path), None => PatKind::Path(qself, path),
} }
} }
} else if let Some(lt) = self.token.lifetime() } else if let Some((lt, IdentIsRaw::No)) = self.token.lifetime()
// In pattern position, we're totally fine with using "next token isn't colon" // In pattern position, we're totally fine with using "next token isn't colon"
// as a heuristic. We could probably just always try to recover if it's a lifetime, // as a heuristic. We could probably just always try to recover if it's a lifetime,
// because we never have `'a: label {}` in a pattern position anyways, but it does // because we never have `'a: label {}` in a pattern position anyways, but it does
@ -689,7 +689,7 @@ impl<'a> Parser<'a> {
/// Parse `&pat` / `&mut pat`. /// Parse `&pat` / `&mut pat`.
fn parse_pat_deref(&mut self, expected: Option<Expected>) -> PResult<'a, PatKind> { fn parse_pat_deref(&mut self, expected: Option<Expected>) -> PResult<'a, PatKind> {
self.expect_and()?; self.expect_and()?;
if let Some(lifetime) = self.token.lifetime() { if let Some((lifetime, _)) = self.token.lifetime() {
self.bump(); // `'a` self.bump(); // `'a`
self.dcx().emit_err(UnexpectedLifetimeInPattern { self.dcx().emit_err(UnexpectedLifetimeInPattern {

View File

@ -1,5 +1,5 @@
use rustc_ast::ptr::P; use rustc_ast::ptr::P;
use rustc_ast::token::{self, BinOpToken, Delimiter, Token, TokenKind}; use rustc_ast::token::{self, BinOpToken, Delimiter, IdentIsRaw, Token, TokenKind};
use rustc_ast::util::case::Case; use rustc_ast::util::case::Case;
use rustc_ast::{ use rustc_ast::{
self as ast, BareFnTy, BoundAsyncness, BoundConstness, BoundPolarity, FnRetTy, GenericBound, self as ast, BareFnTy, BoundAsyncness, BoundConstness, BoundPolarity, FnRetTy, GenericBound,
@ -1285,8 +1285,9 @@ impl<'a> Parser<'a> {
/// Parses a single lifetime `'a` or panics. /// Parses a single lifetime `'a` or panics.
pub(super) fn expect_lifetime(&mut self) -> Lifetime { pub(super) fn expect_lifetime(&mut self) -> Lifetime {
if let Some(ident) = self.token.lifetime() { if let Some((ident, is_raw)) = self.token.lifetime() {
if ident.without_first_quote().is_reserved() if matches!(is_raw, IdentIsRaw::No)
&& ident.without_first_quote().is_reserved()
&& ![kw::UnderscoreLifetime, kw::StaticLifetime].contains(&ident.name) && ![kw::UnderscoreLifetime, kw::StaticLifetime].contains(&ident.name)
{ {
self.dcx().emit_err(errors::KeywordLifetime { span: ident.span }); self.dcx().emit_err(errors::KeywordLifetime { span: ident.span });

View File

@ -879,7 +879,9 @@ impl<'src> Classifier<'src> {
| TokenKind::UnknownPrefix | TokenKind::UnknownPrefix
| TokenKind::InvalidPrefix | TokenKind::InvalidPrefix
| TokenKind::InvalidIdent => Class::Ident(self.new_span(before, text)), | TokenKind::InvalidIdent => Class::Ident(self.new_span(before, text)),
TokenKind::Lifetime { .. } => Class::Lifetime, TokenKind::Lifetime { .. }
| TokenKind::RawLifetime
| TokenKind::UnknownPrefixLifetime => Class::Lifetime,
TokenKind::Eof => panic!("Eof in advance"), TokenKind::Eof => panic!("Eof in advance"),
}; };
// Anything that didn't return above is the simple case where we the // Anything that didn't return above is the simple case where we the

View File

@ -198,6 +198,13 @@ impl<'a> Converter<'a> {
} }
LIFETIME_IDENT LIFETIME_IDENT
} }
rustc_lexer::TokenKind::UnknownPrefixLifetime => {
err = "Unknown lifetime prefix";
LIFETIME_IDENT
}
rustc_lexer::TokenKind::RawLifetime => {
LIFETIME_IDENT
}
rustc_lexer::TokenKind::Semi => T![;], rustc_lexer::TokenKind::Semi => T![;],
rustc_lexer::TokenKind::Comma => T![,], rustc_lexer::TokenKind::Comma => T![,],

View File

@ -462,7 +462,7 @@ fn rewrite_empty_block(
return None; return None;
} }
let label_str = rewrite_label(label); let label_str = rewrite_label(context, label);
if attrs.map_or(false, |a| !inner_attributes(a).is_empty()) { if attrs.map_or(false, |a| !inner_attributes(a).is_empty()) {
return None; return None;
} }
@ -527,7 +527,7 @@ fn rewrite_single_line_block(
if let Some(block_expr) = stmt::Stmt::from_simple_block(context, block, attrs) { if let Some(block_expr) = stmt::Stmt::from_simple_block(context, block, attrs) {
let expr_shape = shape.offset_left(last_line_width(prefix))?; let expr_shape = shape.offset_left(last_line_width(prefix))?;
let expr_str = block_expr.rewrite(context, expr_shape)?; let expr_str = block_expr.rewrite(context, expr_shape)?;
let label_str = rewrite_label(label); let label_str = rewrite_label(context, label);
let result = format!("{prefix}{label_str}{{ {expr_str} }}"); let result = format!("{prefix}{label_str}{{ {expr_str} }}");
if result.len() <= shape.width && !result.contains('\n') { if result.len() <= shape.width && !result.contains('\n') {
return Some(result); return Some(result);
@ -562,7 +562,7 @@ pub(crate) fn rewrite_block_with_visitor(
} }
let inner_attrs = attrs.map(inner_attributes); let inner_attrs = attrs.map(inner_attributes);
let label_str = rewrite_label(label); let label_str = rewrite_label(context, label);
visitor.visit_block(block, inner_attrs.as_deref(), has_braces); visitor.visit_block(block, inner_attrs.as_deref(), has_braces);
let visitor_context = visitor.get_context(); let visitor_context = visitor.get_context();
context context
@ -939,7 +939,7 @@ impl<'a> ControlFlow<'a> {
fresh_shape fresh_shape
}; };
let label_string = rewrite_label(self.label); let label_string = rewrite_label(context, self.label);
// 1 = space after keyword. // 1 = space after keyword.
let offset = self.keyword.len() + label_string.len() + 1; let offset = self.keyword.len() + label_string.len() + 1;
@ -1168,9 +1168,9 @@ impl<'a> Rewrite for ControlFlow<'a> {
} }
} }
fn rewrite_label(opt_label: Option<ast::Label>) -> Cow<'static, str> { fn rewrite_label(context: &RewriteContext<'_>, opt_label: Option<ast::Label>) -> Cow<'static, str> {
match opt_label { match opt_label {
Some(label) => Cow::from(format!("{}: ", label.ident)), Some(label) => Cow::from(format!("{}: ", context.snippet(label.ident.span))),
None => Cow::from(""), None => Cow::from(""),
} }
} }

View File

@ -1074,7 +1074,7 @@ fn force_space_before(tok: &TokenKind) -> bool {
fn ident_like(tok: &Token) -> bool { fn ident_like(tok: &Token) -> bool {
matches!( matches!(
tok.kind, tok.kind,
TokenKind::Ident(..) | TokenKind::Literal(..) | TokenKind::Lifetime(_) TokenKind::Ident(..) | TokenKind::Literal(..) | TokenKind::Lifetime(..)
) )
} }
@ -1099,7 +1099,9 @@ fn next_space(tok: &TokenKind) -> SpaceState {
| TokenKind::OpenDelim(_) | TokenKind::OpenDelim(_)
| TokenKind::CloseDelim(_) => SpaceState::Never, | TokenKind::CloseDelim(_) => SpaceState::Never,
TokenKind::Literal(..) | TokenKind::Ident(..) | TokenKind::Lifetime(_) => SpaceState::Ident, TokenKind::Literal(..) | TokenKind::Ident(..) | TokenKind::Lifetime(..) => {
SpaceState::Ident
}
_ => SpaceState::Always, _ => SpaceState::Always,
} }

View File

@ -548,7 +548,7 @@ impl Rewrite for ast::AnonConst {
impl Rewrite for ast::Lifetime { impl Rewrite for ast::Lifetime {
fn rewrite(&self, context: &RewriteContext<'_>, _: Shape) -> Option<String> { fn rewrite(&self, context: &RewriteContext<'_>, _: Shape) -> Option<String> {
Some(rewrite_ident(context, self.ident).to_owned()) Some(context.snippet(self.ident.span).to_owned())
} }
} }

View File

@ -0,0 +1,15 @@
// rustfmt-edition: 2021
// Simple idempotence test for raw lifetimes.
fn test<'r#gen>() -> &'r#gen () {
// Test raw lifetimes...
}
fn label() {
'r#label: {
// Test raw labels.
}
}
fn main() {}

View File

@ -0,0 +1,10 @@
//@ edition: 2021
macro_rules! w {
($($tt:tt)*) => {};
}
w!('foo#lifetime);
//~^ ERROR prefix `'foo` is unknown
fn main() {}

View File

@ -0,0 +1,14 @@
error: prefix `'foo` is unknown
--> $DIR/prefixed-lifetime.rs:7:4
|
LL | w!('foo#lifetime);
| ^^^^ unknown prefix
|
= note: prefixed identifiers and literals are reserved since Rust 2021
help: consider inserting whitespace here
|
LL | w!('foo #lifetime);
| +
error: aborting due to 1 previous error

View File

@ -0,0 +1,8 @@
error: lifetimes cannot use keyword names
--> $DIR/gen-lt.rs:11:11
|
LL | fn gen_lt<'gen>() {}
| ^^^^
error: aborting due to 1 previous error

View File

@ -0,0 +1,14 @@
//@ revisions: e2021 e2024
//@[e2021] edition:2021
//@[e2024] edition:2024
//@[e2024] compile-flags: -Zunstable-options
//@[e2021] check-pass
fn raw_gen_lt<'r#gen>() {}
fn gen_lt<'gen>() {}
//[e2024]~^ ERROR lifetimes cannot use keyword names
fn main() {}

View File

@ -0,0 +1,8 @@
//@ edition: 2021
//@ check-pass
// Test that `'r#a` is `'a`.
fn test<'r#a>(x: &'a ()) {}
fn main() {}

View File

@ -0,0 +1,12 @@
//@ check-pass
//@ edition: 2021
macro_rules! lifetime {
($lt:lifetime) => {
fn hello<$lt>() {}
}
}
lifetime!('r#struct);
fn main() {}

View File

@ -0,0 +1,6 @@
//@ edition: 2021
fn test(x: &'r#r#r ()) {}
//~^ ERROR expected type, found `#`
fn main() {}

View File

@ -0,0 +1,8 @@
error: expected type, found `#`
--> $DIR/multiple-prefixes.rs:3:17
|
LL | fn test(x: &'r#r#r ()) {}
| ^ expected type
error: aborting due to 1 previous error

View File

@ -0,0 +1,8 @@
//@ check-pass
//@ edition: 2021
// Checks a primitive name can be defined as a lifetime.
fn foo<'r#i32>() {}
fn main() {}

View File

@ -0,0 +1,21 @@
//@ check-pass
//@ edition: 2021
fn foo<'r#struct>() {}
fn hr<T>() where for<'r#struct> T: Into<&'r#struct ()> {}
trait Foo<'r#struct> {}
trait Bar<'r#struct> {
fn method(&'r#struct self) {}
fn method2(self: &'r#struct Self) {}
}
fn labeled() {
'r#struct: loop {
break 'r#struct;
}
}
fn main() {}

View File

@ -0,0 +1,8 @@
//@ check-pass
//@ edition: 2021
// Makes sure that `'r#static` is `'static`
const FOO: &'r#static str = "hello, world";
fn main() {}

View File

@ -0,0 +1,12 @@
//@ edition: 2015
//@ check-pass
// Ensure that we parse `'r#lt` as three tokens in edition 2015.
macro_rules! ed2015 {
('r # lt) => {};
($lt:lifetime) => { compile_error!() };
}
ed2015!('r#lt);
fn main() {}

View File

@ -31,5 +31,14 @@ LL | () => { mod test { fn gen() {} } }
= warning: this is accepted in the current edition (Rust 2015) but is a hard error in Rust 2024! = warning: this is accepted in the current edition (Rust 2015) but is a hard error in Rust 2024!
= note: for more information, see issue #49716 <https://github.com/rust-lang/rust/issues/49716> = note: for more information, see issue #49716 <https://github.com/rust-lang/rust/issues/49716>
error: aborting due to 3 previous errors error: `gen` is a keyword in the 2024 edition
--> $DIR/gen-kw.rs:25:9
|
LL | fn test<'gen>() {}
| ^^^^ help: you can use a raw identifier to stay compatible: `'r#gen`
|
= warning: this is accepted in the current edition (Rust 2015) but is a hard error in Rust 2024!
= note: for more information, see issue #49716 <https://github.com/rust-lang/rust/issues/49716>
error: aborting due to 4 previous errors

View File

@ -31,5 +31,14 @@ LL | () => { mod test { fn gen() {} } }
= warning: this is accepted in the current edition (Rust 2018) but is a hard error in Rust 2024! = warning: this is accepted in the current edition (Rust 2018) but is a hard error in Rust 2024!
= note: for more information, see issue #49716 <https://github.com/rust-lang/rust/issues/49716> = note: for more information, see issue #49716 <https://github.com/rust-lang/rust/issues/49716>
error: aborting due to 3 previous errors error: `gen` is a keyword in the 2024 edition
--> $DIR/gen-kw.rs:25:9
|
LL | fn test<'gen>() {}
| ^^^^ help: you can use a raw identifier to stay compatible: `'r#gen`
|
= warning: this is accepted in the current edition (Rust 2018) but is a hard error in Rust 2024!
= note: for more information, see issue #49716 <https://github.com/rust-lang/rust/issues/49716>
error: aborting due to 4 previous errors

View File

@ -22,4 +22,9 @@ macro_rules! t {
//[e2018]~| WARNING this is accepted in the current edition (Rust 2018) but is a hard error in Rust 2024! //[e2018]~| WARNING this is accepted in the current edition (Rust 2018) but is a hard error in Rust 2024!
} }
fn test<'gen>() {}
//~^ ERROR `gen` is a keyword in the 2024 edition
//[e2015]~| WARNING this is accepted in the current edition (Rust 2015) but is a hard error in Rust 2024!
//[e2018]~| WARNING this is accepted in the current edition (Rust 2018) but is a hard error in Rust 2024!
t!(); t!();