use std::collections::BTreeSet;
use rustc_ast::token::{Delimiter, IdentIsRaw, Token, TokenKind};
use rustc_ast::tokenstream::{TokenStream, TokenTree};
use rustc_span::kw;
#[derive(Clone, Copy)]
pub(super) struct PurityContext<'a> {
pub methods: &'a BTreeSet<String>,
pub macros: &'a BTreeSet<String>,
}
pub(super) fn split_top_level_arguments(stream: &TokenStream) -> Option<Vec<Vec<TokenTree>>> {
let mut arguments: Vec<Vec<TokenTree>> = Vec::new();
let mut current: Vec<TokenTree> = Vec::new();
for tree in stream.iter() {
if let TokenTree::Token(token, _) = tree {
match token.kind {
TokenKind::Semi => return None,
TokenKind::Comma => {
arguments.push(std::mem::take(&mut current));
continue;
}
_ => {}
}
}
current.push(tree.clone());
}
if !current.is_empty() {
arguments.push(current);
}
Some(arguments)
}
pub(super) fn looks_like_expression(argument: &[TokenTree]) -> bool {
if let Some(TokenTree::Token(token, _)) = argument.first()
&& !token.can_begin_expr()
{
return false;
}
if argument.iter().any(|tree| match tree {
TokenTree::Token(token, _) => is_dsl_marker(token),
_ => false,
}) {
return false;
}
if let [TokenTree::Delimited(_, _, Delimiter::Brace, inner)] = argument
&& brace_inner_looks_like_dsl(inner)
{
return false;
}
true
}
fn brace_inner_looks_like_dsl(stream: &TokenStream) -> bool {
let trees: Vec<&TokenTree> = stream.iter().collect();
let mut whitelist_let = false;
let mut cursor = 0;
while cursor < trees.len() {
let after_attrs = skip_leading_attributes(&trees, cursor);
if let Some(TokenTree::Token(token, _)) = trees.get(after_attrs)
&& token.is_keyword(kw::Let)
{
whitelist_let = true;
}
cursor = after_attrs;
while cursor < trees.len() {
if let TokenTree::Token(token, _) = trees[cursor] {
match token.kind {
TokenKind::Semi => {
whitelist_let = false;
cursor += 1;
break;
}
TokenKind::FatArrow => return true,
TokenKind::Colon if !whitelist_let => return true,
_ => {}
}
}
cursor += 1;
}
}
false
}
fn skip_leading_attributes(trees: &[&TokenTree], mut start: usize) -> usize {
loop {
let Some(tree) = trees.get(start) else {
return start;
};
let TokenTree::Token(token, _) = tree else {
return start;
};
match token.kind {
TokenKind::DocComment(..) => start += 1,
TokenKind::Pound => {
let mut after_pound = start + 1;
if matches!(trees.get(after_pound), Some(TokenTree::Token(t, _)) if t.kind == TokenKind::Bang)
{
after_pound += 1;
}
if matches!(
trees.get(after_pound),
Some(TokenTree::Delimited(_, _, Delimiter::Bracket, _)),
) {
start = after_pound + 1;
} else {
return start;
}
}
_ => return start,
}
}
}
fn is_dsl_marker(token: &Token) -> bool {
if token.is_keyword(kw::In) {
return true;
}
matches!(
token.kind,
TokenKind::FatArrow
| TokenKind::RArrow
| TokenKind::Colon
| TokenKind::Eq
| TokenKind::PlusEq
| TokenKind::MinusEq
| TokenKind::StarEq
| TokenKind::SlashEq
| TokenKind::PercentEq
| TokenKind::AndEq
| TokenKind::OrEq
| TokenKind::CaretEq
| TokenKind::ShlEq
| TokenKind::ShrEq,
)
}
pub(super) fn is_pure_expression(tokens: &[TokenTree], ctx: PurityContext<'_>) -> bool {
take_pure_expression(tokens, ctx).is_some_and(<[_]>::is_empty)
}
fn take_pure_expression<'a>(
tokens: &'a [TokenTree],
ctx: PurityContext<'_>,
) -> Option<&'a [TokenTree]> {
let after_atom = take_pure_atom(tokens, ctx)?;
let after_suffix = take_pure_suffixes(after_atom, ctx);
Some(take_pure_binary_tail(after_suffix, ctx))
}
fn take_pure_atom<'a>(tokens: &'a [TokenTree], ctx: PurityContext<'_>) -> Option<&'a [TokenTree]> {
let (head, rest) = tokens.split_first()?;
match head {
TokenTree::Delimited(_, _, Delimiter::Parenthesis, inner) => {
if is_pure_paren_inner(inner, ctx) {
Some(rest)
} else {
None
}
}
TokenTree::Delimited(_, _, Delimiter::Bracket, inner) => {
if is_pure_array_inner(inner, ctx) {
Some(rest)
} else {
None
}
}
TokenTree::Token(token, _) => match token.kind {
TokenKind::Literal(_) => Some(rest),
TokenKind::Ident(name, IdentIsRaw::No) if name == kw::True || name == kw::False => {
Some(rest)
}
TokenKind::And => take_reference_tail(rest, ctx),
TokenKind::AndAnd => take_reference_tail(rest, ctx),
TokenKind::Star => take_pure_expression(rest, ctx),
TokenKind::Bang => take_pure_expression(rest, ctx),
TokenKind::Ident(name, _) => {
Some(take_path_and_optional_macro_call(name, rest, ctx.macros))
}
TokenKind::PathSep => take_atom_path_after_sep(rest, ctx.macros),
_ => None,
},
_ => None,
}
}
fn is_pure_paren_inner(stream: &TokenStream, ctx: PurityContext<'_>) -> bool {
let Some(arguments) = split_top_level_arguments(stream) else {
return false;
};
arguments
.iter()
.all(|argument| !argument.is_empty() && is_pure_expression(argument, ctx))
}
fn is_pure_array_inner(stream: &TokenStream, ctx: PurityContext<'_>) -> bool {
if let Some(arguments) = split_top_level_arguments(stream) {
return arguments
.iter()
.all(|argument| !argument.is_empty() && is_pure_expression(argument, ctx));
}
let Some((expr, count)) = split_array_repeat(stream) else {
return false;
};
!expr.is_empty()
&& is_pure_expression(&expr, ctx)
&& !count.is_empty()
&& is_pure_expression(&count, ctx)
}
fn split_array_repeat(stream: &TokenStream) -> Option<(Vec<TokenTree>, Vec<TokenTree>)> {
let mut before: Vec<TokenTree> = Vec::new();
let mut after: Option<Vec<TokenTree>> = None;
for tree in stream.iter() {
if let TokenTree::Token(token, _) = tree {
match token.kind {
TokenKind::Semi => {
if after.is_some() {
return None;
}
after = Some(Vec::new());
continue;
}
TokenKind::Comma => return None,
_ => {}
}
}
match after.as_mut() {
Some(buf) => buf.push(tree.clone()),
None => before.push(tree.clone()),
}
}
after.map(|count| (before, count))
}
fn take_reference_tail<'a>(
tokens: &'a [TokenTree],
ctx: PurityContext<'_>,
) -> Option<&'a [TokenTree]> {
let after_mut = match tokens.split_first() {
Some((TokenTree::Token(token, _), rest)) if token.is_keyword(kw::Mut) => rest,
_ => tokens,
};
take_pure_expression(after_mut, ctx)
}
fn walk_path_tail(
first_name: rustc_span::Symbol,
mut tokens: &[TokenTree],
) -> (rustc_span::Symbol, &[TokenTree]) {
let mut last = first_name;
while let Some((TokenTree::Token(sep, _), after_sep)) = tokens.split_first() {
if sep.kind != TokenKind::PathSep {
break;
}
let Some((TokenTree::Token(ident, _), after_ident)) = after_sep.split_first() else {
break;
};
let TokenKind::Ident(name, _) = ident.kind else {
break;
};
last = name;
tokens = after_ident;
}
(last, tokens)
}
fn take_path_tail(mut tokens: &[TokenTree]) -> &[TokenTree] {
while let Some((TokenTree::Token(sep, _), after_sep)) = tokens.split_first() {
if sep.kind != TokenKind::PathSep {
break;
}
let Some((TokenTree::Token(ident, _), after_ident)) = after_sep.split_first() else {
break;
};
if !matches!(ident.kind, TokenKind::Ident(_, _)) {
break;
}
tokens = after_ident;
}
tokens
}
fn take_path_and_optional_macro_call<'a>(
first_name: rustc_span::Symbol,
tokens: &'a [TokenTree],
pure_macros: &BTreeSet<String>,
) -> &'a [TokenTree] {
let (tail_name, after_path) = walk_path_tail(first_name, tokens);
take_pure_macro_call(after_path, tail_name, pure_macros).unwrap_or(after_path)
}
fn take_pure_macro_call<'a>(
tokens: &'a [TokenTree],
macro_name: rustc_span::Symbol,
pure_macros: &BTreeSet<String>,
) -> Option<&'a [TokenTree]> {
let (bang, after_bang) = tokens.split_first()?;
let TokenTree::Token(bang_token, _) = bang else {
return None;
};
if bang_token.kind != TokenKind::Bang {
return None;
}
if !pure_macros.contains(macro_name.as_str()) {
return None;
}
let (delim, after_delim) = after_bang.split_first()?;
let TokenTree::Delimited(_, _, delim_kind, _) = delim else {
return None;
};
if !matches!(delim_kind, Delimiter::Parenthesis | Delimiter::Bracket) {
return None;
}
Some(after_delim)
}
fn take_atom_path_after_sep<'a>(
tokens: &'a [TokenTree],
pure_macros: &BTreeSet<String>,
) -> Option<&'a [TokenTree]> {
let (ident, rest) = tokens.split_first()?;
let TokenTree::Token(token, _) = ident else {
return None;
};
let TokenKind::Ident(name, _) = token.kind else {
return None;
};
Some(take_path_and_optional_macro_call(name, rest, pure_macros))
}
fn take_path_after_sep(tokens: &[TokenTree]) -> Option<&[TokenTree]> {
let (ident, rest) = tokens.split_first()?;
let TokenTree::Token(token, _) = ident else {
return None;
};
if !matches!(token.kind, TokenKind::Ident(_, _)) {
return None;
}
Some(take_path_tail(rest))
}
fn take_pure_suffixes<'a>(mut tokens: &'a [TokenTree], ctx: PurityContext<'_>) -> &'a [TokenTree] {
loop {
let Some((head, rest)) = tokens.split_first() else {
return tokens;
};
match head {
TokenTree::Token(token, _) => match token.kind {
TokenKind::Dot => {
let Some((next, after)) = rest.split_first() else {
return tokens;
};
let TokenTree::Token(next_token, _) = next else {
return tokens;
};
match next_token.kind {
TokenKind::Ident(name, IdentIsRaw::No) if name == kw::Await => {
return tokens;
}
TokenKind::Ident(name, IdentIsRaw::No) => {
if is_pure_method_call(after, name.as_str(), ctx.methods) {
tokens = &after[1..];
} else {
tokens = after;
}
}
TokenKind::Ident(_, _) | TokenKind::Literal(_) => tokens = after,
_ => return tokens,
}
}
TokenKind::Ident(name, IdentIsRaw::No) if name == kw::As => {
let Some(after) = take_pure_type(rest) else {
return tokens;
};
tokens = after;
}
_ => return tokens,
},
TokenTree::Delimited(_, _, Delimiter::Bracket, inner) => {
if !is_pure_expression_stream(inner, ctx) {
return tokens;
}
tokens = rest;
}
_ => return tokens,
}
}
}
fn is_pure_method_call(
tokens: &[TokenTree],
method_name: &str,
pure_methods: &BTreeSet<String>,
) -> bool {
let Some(TokenTree::Delimited(_, _, Delimiter::Parenthesis, inner)) = tokens.first() else {
return false;
};
inner.is_empty() && pure_methods.contains(method_name)
}
fn take_pure_binary_tail<'a>(
mut tokens: &'a [TokenTree],
ctx: PurityContext<'_>,
) -> &'a [TokenTree] {
while let Some(after_op) = take_pure_binary_operator(tokens) {
let Some(after_atom) = take_pure_atom(after_op, ctx) else {
return tokens;
};
tokens = take_pure_suffixes(after_atom, ctx);
}
tokens
}
fn take_pure_binary_operator(tokens: &[TokenTree]) -> Option<&[TokenTree]> {
let (head, rest) = tokens.split_first()?;
let TokenTree::Token(token, _) = head else {
return None;
};
matches!(
token.kind,
TokenKind::EqEq
| TokenKind::Ne
| TokenKind::Lt
| TokenKind::Gt
| TokenKind::Le
| TokenKind::Ge
| TokenKind::AndAnd
| TokenKind::OrOr
| TokenKind::Plus
| TokenKind::Minus
| TokenKind::Star
| TokenKind::Slash
| TokenKind::Percent
| TokenKind::Caret
| TokenKind::And
| TokenKind::Or
| TokenKind::Shl
| TokenKind::Shr,
)
.then_some(rest)
}
fn take_pure_type(tokens: &[TokenTree]) -> Option<&[TokenTree]> {
let (head, rest) = tokens.split_first()?;
let TokenTree::Token(token, _) = head else {
return None;
};
match token.kind {
TokenKind::Ident(_, _) => Some(take_path_tail(rest)),
TokenKind::PathSep => take_path_after_sep(rest),
_ => None,
}
}
fn is_pure_expression_stream(stream: &TokenStream, ctx: PurityContext<'_>) -> bool {
let trees: Vec<TokenTree> = stream.iter().cloned().collect();
is_pure_expression(&trees, ctx)
}