use proc_macro2::{Delimiter, Span, TokenStream, TokenTree};
use syn::parse::{ParseStream, Parser};
use syn::{Expr, Lit, LitStr, Token};
pub(crate) struct Arg {
pub(crate) name: String,
pub(crate) value: Expr,
}
pub(crate) struct Call {
pub(crate) context: bool,
pub(crate) id: LitStr,
pub(crate) args: Option<Vec<Arg>>,
}
pub(crate) enum Form {
Call(Call),
DynamicId(Span),
If(Span),
Cfg(Span),
ArgumentName(Span),
Unparsed(Span),
}
enum Segment {
Id(LitStr),
Map(proc_macro2::Group),
Other(Span),
}
pub(crate) fn parse(group: &proc_macro2::Group) -> Form {
let tokens: Vec<TokenTree> = group.stream().into_iter().collect();
let Some(first) = tokens.first() else {
return Form::Unparsed(group.span());
};
let mut segments: Vec<Vec<TokenTree>> = vec![Vec::new()];
for token in &tokens {
match token {
TokenTree::Punct(p) if p.as_char() == '#' => return Form::Cfg(p.span()),
TokenTree::Punct(p) if p.as_char() == ',' => segments.push(Vec::new()),
other => {
if let Some(last) = segments.last_mut() {
last.push(other.clone());
}
}
}
}
if segments.last().is_some_and(Vec::is_empty) {
segments.pop();
}
if segments.iter().any(|s| is_if(s)) {
let at = segments
.iter()
.find(|s| is_if(s))
.and_then(|s| s.first())
.map_or(first.span(), TokenTree::span);
return Form::If(at);
}
let classified: Vec<Segment> = segments.iter().map(|s| classify(s)).collect();
let (context, id, map) = match classified.as_slice() {
[Segment::Id(id)] => (false, id, None),
[Segment::Id(id), Segment::Map(map)] => (false, id, Some(map)),
[Segment::Other(_), Segment::Id(id)] => (true, id, None),
[Segment::Other(_), Segment::Id(id), Segment::Map(map)] => (true, id, Some(map)),
[Segment::Other(at)]
| [Segment::Other(at), Segment::Map(_)]
| [Segment::Other(_), Segment::Other(at)]
| [Segment::Other(_), Segment::Other(at), Segment::Map(_)] => {
return Form::DynamicId(*at);
}
_ => return Form::Unparsed(first.span()),
};
let args = match map {
None => None,
Some(map) => match parse_map(map) {
Ok(args) => Some(args),
Err(form) => return form,
},
};
Form::Call(Call {
context,
id: id.clone(),
args,
})
}
fn is_if(segment: &[TokenTree]) -> bool {
matches!(segment.first(), Some(TokenTree::Ident(i)) if i == "if")
}
fn classify(segment: &[TokenTree]) -> Segment {
match segment {
[TokenTree::Literal(l)] => match Lit::new(l.clone()) {
Lit::Str(s) => Segment::Id(s),
_ => Segment::Other(l.span()),
},
[TokenTree::Group(g)] if g.delimiter() == Delimiter::Brace => Segment::Map(g.clone()),
[first, ..] => Segment::Other(first.span()),
[] => Segment::Other(Span::call_site()),
}
}
fn parse_map(map: &proc_macro2::Group) -> Result<Vec<Arg>, Form> {
for token in map.stream() {
if let TokenTree::Punct(p) = &token
&& p.as_char() == '#'
{
return Err(Form::Cfg(p.span()));
}
}
let parser = |input: ParseStream<'_>| -> syn::Result<Result<Vec<Arg>, Form>> {
let mut args = Vec::new();
while !input.is_empty() {
let key: Lit = input.parse()?;
input.parse::<Token![=>]>()?;
let value: Expr = input.parse()?;
let name = match &key {
Lit::Str(s) => s.value(),
other => return Ok(Err(Form::ArgumentName(other.span()))),
};
args.push(Arg { name, value });
if input.is_empty() {
break;
}
input.parse::<Token![,]>()?;
}
Ok(Ok(args))
};
match parser.parse2(map.stream()) {
Ok(result) => result,
Err(e) => Err(Form::Unparsed(e.span())),
}
}
pub(crate) fn constant(e: &Expr) -> bool {
match e {
Expr::Lit(_) | Expr::Path(_) => true,
Expr::Reference(r) => constant(&r.expr),
Expr::Paren(p) => constant(&p.expr),
Expr::Group(g) => constant(&g.expr),
Expr::Unary(u) => matches!(u.op, syn::UnOp::Neg(_)) && matches!(*u.expr, Expr::Lit(_)),
_ => false,
}
}
pub(crate) fn range(tokens: TokenStream) -> Option<std::ops::Range<usize>> {
let mut iter = tokens.into_iter();
let first = iter.next()?;
let last = iter.last().unwrap_or_else(|| first.clone());
Some(first.span().byte_range().start..last.span().byte_range().end)
}