use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
use quote::quote;
use syn::visit_mut::{self, VisitMut};
use crate::migrate_v2::rewriter::FileRewriter;
pub struct Rule;
impl FileRewriter for Rule {
fn name(&self) -> &'static str {
"bare_ident"
}
fn rewrite(&self, mut file: syn::File) -> syn::File {
PageMacroBodyVisitor.visit_file_mut(&mut file);
file
}
}
struct PageMacroBodyVisitor;
impl VisitMut for PageMacroBodyVisitor {
fn visit_macro_mut(&mut self, m: &mut syn::Macro) {
if m.path
.segments
.last()
.map(|s| s.ident == "page")
.unwrap_or(false)
{
m.tokens = rewrite_page_body(m.tokens.clone());
}
visit_mut::visit_macro_mut(self, m);
}
}
fn rewrite_page_body(input: TokenStream) -> TokenStream {
let mut out: Vec<TokenTree> = Vec::new();
for tt in input {
match tt {
TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => {
let inner = rewrite_brace_body(g.stream());
out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
}
other => out.push(other),
}
}
out.into_iter().collect()
}
fn rewrite_brace_body(input: TokenStream) -> TokenStream {
let mut out: Vec<TokenTree> = Vec::new();
let mut iter = input.into_iter().peekable();
while let Some(tt) = iter.next() {
if let TokenTree::Ident(id) = &tt
&& starts_lowercase(&id.to_string())
&& !is_reserved_keyword(&id.to_string())
{
let is_followed_by_continuation = match iter.peek() {
Some(TokenTree::Group(g))
if matches!(
g.delimiter(),
Delimiter::Brace | Delimiter::Parenthesis | Delimiter::Bracket
) =>
{
true
}
Some(TokenTree::Punct(p)) if matches!(p.as_char(), ':' | '!' | '.' | ',') => true,
_ => false,
};
if !is_followed_by_continuation {
let ident = id.clone();
let wrapped = quote! { #ident };
out.push(TokenTree::Group(Group::new(Delimiter::Brace, wrapped)));
continue;
}
}
if let TokenTree::Group(g) = &tt
&& g.delimiter() == Delimiter::Brace
{
if is_already_wrapped_expression_slot(&g.stream()) {
out.push(tt);
} else {
let inner = rewrite_brace_body(g.stream());
out.push(TokenTree::Group(Group::new(Delimiter::Brace, inner)));
}
continue;
}
out.push(tt);
}
out.into_iter().collect()
}
fn is_already_wrapped_expression_slot(stream: &TokenStream) -> bool {
let mut iter = stream.clone().into_iter();
let first = iter.next();
let rest = iter.next();
match (first, rest) {
(Some(TokenTree::Group(g)), None) => g.delimiter() == Delimiter::Brace,
_ => false,
}
}
fn starts_lowercase(s: &str) -> bool {
s.chars()
.next()
.map(|c| c.is_ascii_lowercase())
.unwrap_or(false)
}
fn is_reserved_keyword(s: &str) -> bool {
matches!(
s,
"if" | "else"
| "match" | "for"
| "while" | "loop"
| "let" | "return"
| "break" | "continue"
| "move" | "ref"
| "mut" | "async"
| "await" | "yield"
| "do" | "in"
| "as" | "where"
| "use" | "fn"
| "true" | "false"
| "self" | "Self"
| "super" | "crate"
| "impl" | "trait"
| "struct"
| "enum" | "type"
| "const" | "static"
| "pub" | "mod"
| "unsafe"
| "extern"
)
}