blob: 70d796cda11c0aed8a10681ed7e14b8adba0a567 [file] [log] [blame]
use rustc_ast::token::{self, Delimiter, IdentIsRaw, Lit, Token, TokenKind};
use rustc_ast::tokenstream::{TokenStream, TokenStreamIter, TokenTree};
use rustc_ast::{LitIntType, LitKind};
use rustc_ast_pretty::pprust;
use rustc_errors::{Applicability, PResult};
use rustc_macros::{Decodable, Encodable};
use rustc_session::parse::ParseSess;
use rustc_span::{Ident, Span, Symbol, sym};
use crate::errors;
pub(crate) const RAW_IDENT_ERR: &str = "`${concat(..)}` currently does not support raw identifiers";
pub(crate) const UNSUPPORTED_CONCAT_ELEM_ERR: &str = "expected identifier or string literal";
/// A meta-variable expression, for expansions based on properties of meta-variables.
#[derive(Debug, PartialEq, Encodable, Decodable)]
pub(crate) enum MetaVarExpr {
/// Unification of two or more identifiers.
Concat(Box<[MetaVarExprConcatElem]>),
/// The number of repetitions of an identifier.
Count(Ident, usize),
/// Ignore a meta-variable for repetition without expansion.
Ignore(Ident),
/// The index of the repetition at a particular depth, where 0 is the innermost
/// repetition. The `usize` is the depth.
Index(usize),
/// The length of the repetition at a particular depth, where 0 is the innermost
/// repetition. The `usize` is the depth.
Len(usize),
}
impl MetaVarExpr {
/// Attempt to parse a meta-variable expression from a token stream.
pub(crate) fn parse<'psess>(
input: &TokenStream,
outer_span: Span,
psess: &'psess ParseSess,
) -> PResult<'psess, MetaVarExpr> {
let mut iter = input.iter();
let ident = parse_ident(&mut iter, psess, outer_span)?;
let next = iter.next();
let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, args)) = next else {
// No `()`; wrong or no delimiters. Point at a problematic span or a place to
// add parens if it makes sense.
let (unexpected_span, insert_span) = match next {
Some(TokenTree::Delimited(..)) => (None, None),
Some(tt) => (Some(tt.span()), None),
None => (None, Some(ident.span.shrink_to_hi())),
};
let err =
errors::MveMissingParen { ident_span: ident.span, unexpected_span, insert_span };
return Err(psess.dcx().create_err(err));
};
// Ensure there are no trailing tokens in the braces, e.g. `${foo() extra}`
if iter.peek().is_some() {
let span = iter_span(&iter).expect("checked is_some above");
let err = errors::MveExtraTokens {
span,
ident_span: ident.span,
extra_count: iter.count(),
..Default::default()
};
return Err(psess.dcx().create_err(err));
}
let mut iter = args.iter();
let rslt = match ident.name {
sym::concat => parse_concat(&mut iter, psess, outer_span, ident.span)?,
sym::count => parse_count(&mut iter, psess, ident.span)?,
sym::ignore => {
eat_dollar(&mut iter, psess, ident.span)?;
MetaVarExpr::Ignore(parse_ident(&mut iter, psess, ident.span)?)
}
sym::index => MetaVarExpr::Index(parse_depth(&mut iter, psess, ident.span)?),
sym::len => MetaVarExpr::Len(parse_depth(&mut iter, psess, ident.span)?),
_ => {
let err = errors::MveUnrecognizedExpr {
span: ident.span,
valid_expr_list: "`count`, `ignore`, `index`, `len`, and `concat`",
};
return Err(psess.dcx().create_err(err));
}
};
check_trailing_tokens(&mut iter, psess, ident)?;
Ok(rslt)
}
pub(crate) fn for_each_metavar<A>(&self, mut aux: A, mut cb: impl FnMut(A, &Ident) -> A) -> A {
match self {
MetaVarExpr::Concat(elems) => {
for elem in elems {
if let MetaVarExprConcatElem::Var(ident) = elem {
aux = cb(aux, ident)
}
}
aux
}
MetaVarExpr::Count(ident, _) | MetaVarExpr::Ignore(ident) => cb(aux, ident),
MetaVarExpr::Index(..) | MetaVarExpr::Len(..) => aux,
}
}
}
/// Checks if there are any remaining tokens (for example, `${ignore($valid, extra)}`) and create
/// a diag with the correct arg count if so.
fn check_trailing_tokens<'psess>(
iter: &mut TokenStreamIter<'_>,
psess: &'psess ParseSess,
ident: Ident,
) -> PResult<'psess, ()> {
if iter.peek().is_none() {
// All tokens consumed, as expected
return Ok(());
}
// `None` for max indicates the arg count must be exact, `Some` indicates a range is accepted.
let (min_or_exact_args, max_args) = match ident.name {
sym::concat => panic!("concat takes unlimited tokens but didn't eat them all"),
sym::ignore => (1, None),
// 1 or 2 args
sym::count => (1, Some(2)),
// 0 or 1 arg
sym::index | sym::len => (0, Some(1)),
other => unreachable!("unknown MVEs should be rejected earlier (got `{other}`)"),
};
let err = errors::MveExtraTokens {
span: iter_span(iter).expect("checked is_none above"),
ident_span: ident.span,
extra_count: iter.count(),
exact_args_note: if max_args.is_some() { None } else { Some(()) },
range_args_note: if max_args.is_some() { Some(()) } else { None },
min_or_exact_args,
max_args: max_args.unwrap_or_default(),
name: ident.to_string(),
};
Err(psess.dcx().create_err(err))
}
/// Returns a span encompassing all tokens in the iterator if there is at least one item.
fn iter_span(iter: &TokenStreamIter<'_>) -> Option<Span> {
let mut iter = iter.clone(); // cloning is cheap
let first_sp = iter.next()?.span();
let last_sp = iter.last().map(TokenTree::span).unwrap_or(first_sp);
let span = first_sp.with_hi(last_sp.hi());
Some(span)
}
/// Indicates what is placed in a `concat` parameter. For example, literals
/// (`${concat("foo", "bar")}`) or adhoc identifiers (`${concat(foo, bar)}`).
#[derive(Debug, Decodable, Encodable, PartialEq)]
pub(crate) enum MetaVarExprConcatElem {
/// Identifier WITHOUT a preceding dollar sign, which means that this identifier should be
/// interpreted as a literal.
Ident(Ident),
/// For example, a number or a string.
Literal(Symbol),
/// Identifier WITH a preceding dollar sign, which means that this identifier should be
/// expanded and interpreted as a variable.
Var(Ident),
}
/// Parse a meta-variable `concat` expression: `concat($metavar, ident, ...)`.
fn parse_concat<'psess>(
iter: &mut TokenStreamIter<'_>,
psess: &'psess ParseSess,
outer_span: Span,
expr_ident_span: Span,
) -> PResult<'psess, MetaVarExpr> {
let mut result = Vec::new();
loop {
let is_var = try_eat_dollar(iter);
let token = parse_token(iter, psess, outer_span)?;
let element = if is_var {
MetaVarExprConcatElem::Var(parse_ident_from_token(psess, token)?)
} else if let TokenKind::Literal(Lit { kind: token::LitKind::Str, symbol, suffix: None }) =
token.kind
{
MetaVarExprConcatElem::Literal(symbol)
} else {
match parse_ident_from_token(psess, token) {
Err(err) => {
err.cancel();
return Err(psess
.dcx()
.struct_span_err(token.span, UNSUPPORTED_CONCAT_ELEM_ERR));
}
Ok(elem) => MetaVarExprConcatElem::Ident(elem),
}
};
result.push(element);
if iter.peek().is_none() {
break;
}
if !try_eat_comma(iter) {
return Err(psess.dcx().struct_span_err(outer_span, "expected comma"));
}
}
if result.len() < 2 {
return Err(psess
.dcx()
.struct_span_err(expr_ident_span, "`concat` must have at least two elements"));
}
Ok(MetaVarExpr::Concat(result.into()))
}
/// Parse a meta-variable `count` expression: `count(ident[, depth])`
fn parse_count<'psess>(
iter: &mut TokenStreamIter<'_>,
psess: &'psess ParseSess,
span: Span,
) -> PResult<'psess, MetaVarExpr> {
eat_dollar(iter, psess, span)?;
let ident = parse_ident(iter, psess, span)?;
let depth = if try_eat_comma(iter) {
if iter.peek().is_none() {
return Err(psess.dcx().struct_span_err(
span,
"`count` followed by a comma must have an associated index indicating its depth",
));
}
parse_depth(iter, psess, span)?
} else {
0
};
Ok(MetaVarExpr::Count(ident, depth))
}
/// Parses the depth used by index(depth) and len(depth).
fn parse_depth<'psess>(
iter: &mut TokenStreamIter<'_>,
psess: &'psess ParseSess,
span: Span,
) -> PResult<'psess, usize> {
let Some(tt) = iter.next() else { return Ok(0) };
let TokenTree::Token(Token { kind: TokenKind::Literal(lit), .. }, _) = tt else {
return Err(psess
.dcx()
.struct_span_err(span, "meta-variable expression depth must be a literal"));
};
if let Ok(lit_kind) = LitKind::from_token_lit(*lit)
&& let LitKind::Int(n_u128, LitIntType::Unsuffixed) = lit_kind
&& let Ok(n_usize) = usize::try_from(n_u128.get())
{
Ok(n_usize)
} else {
let msg = "only unsuffixes integer literals are supported in meta-variable expressions";
Err(psess.dcx().struct_span_err(span, msg))
}
}
/// Parses an generic ident
fn parse_ident<'psess>(
iter: &mut TokenStreamIter<'_>,
psess: &'psess ParseSess,
fallback_span: Span,
) -> PResult<'psess, Ident> {
let token = parse_token(iter, psess, fallback_span)?;
parse_ident_from_token(psess, token)
}
fn parse_ident_from_token<'psess>(
psess: &'psess ParseSess,
token: &Token,
) -> PResult<'psess, Ident> {
if let Some((elem, is_raw)) = token.ident() {
if let IdentIsRaw::Yes = is_raw {
return Err(psess.dcx().struct_span_err(elem.span, RAW_IDENT_ERR));
}
return Ok(elem);
}
let token_str = pprust::token_to_string(token);
let mut err = psess
.dcx()
.struct_span_err(token.span, format!("expected identifier, found `{token_str}`"));
err.span_suggestion(
token.span,
format!("try removing `{token_str}`"),
"",
Applicability::MaybeIncorrect,
);
Err(err)
}
fn parse_token<'psess, 't>(
iter: &mut TokenStreamIter<'t>,
psess: &'psess ParseSess,
fallback_span: Span,
) -> PResult<'psess, &'t Token> {
let Some(tt) = iter.next() else {
return Err(psess.dcx().struct_span_err(fallback_span, UNSUPPORTED_CONCAT_ELEM_ERR));
};
let TokenTree::Token(token, _) = tt else {
return Err(psess.dcx().struct_span_err(tt.span(), UNSUPPORTED_CONCAT_ELEM_ERR));
};
Ok(token)
}
/// Tries to move the iterator forward returning `true` if there is a comma. If not, then the
/// iterator is not modified and the result is `false`.
fn try_eat_comma(iter: &mut TokenStreamIter<'_>) -> bool {
if let Some(TokenTree::Token(Token { kind: token::Comma, .. }, _)) = iter.peek() {
let _ = iter.next();
return true;
}
false
}
/// Tries to move the iterator forward returning `true` if there is a dollar sign. If not, then the
/// iterator is not modified and the result is `false`.
fn try_eat_dollar(iter: &mut TokenStreamIter<'_>) -> bool {
if let Some(TokenTree::Token(Token { kind: token::Dollar, .. }, _)) = iter.peek() {
let _ = iter.next();
return true;
}
false
}
/// Expects that the next item is a dollar sign.
fn eat_dollar<'psess>(
iter: &mut TokenStreamIter<'_>,
psess: &'psess ParseSess,
span: Span,
) -> PResult<'psess, ()> {
if try_eat_dollar(iter) {
return Ok(());
}
Err(psess.dcx().struct_span_err(
span,
"meta-variables within meta-variable expressions must be referenced using a dollar sign",
))
}