use proc_macro2::{Spacing, TokenStream, TokenTree};
use syn::{Expr, ExprLit, Lit};
pub(crate) fn parse_attributes(
attrs: TokenStream,
) -> (Vec<Expr>, Vec<TokenStream>, Option<String>) {
let segments = segment_input(attrs);
let mut segments_stream: Vec<TokenStream> = segments
.iter()
.map(|x| x.iter().cloned().collect::<TokenStream>())
.collect();
let rewritten_segs: Vec<_> = segments.into_iter().map(rewrite).collect();
let mut conds: Vec<Expr> = vec![];
let last_segment_idx = rewritten_segs.len().saturating_sub(1);
for (idx, seg) in rewritten_segs.into_iter().enumerate() {
let expr = match syn::parse2::<Expr>(seg) {
Ok(val) => val,
Err(err) => Expr::Verbatim(err.to_compile_error()),
};
if idx != last_segment_idx && string_lit_value(&expr).is_some() {
let err =
syn::Error::new_spanned(expr, "contract description must be the last argument");
conds.push(Expr::Verbatim(err.to_compile_error()));
continue;
}
conds.push(expr);
}
let desc = conds.last().and_then(string_lit_value);
if desc.is_some() {
conds.pop();
segments_stream.pop();
}
for cond in &mut conds {
error_on_false_literal(cond);
}
(conds, segments_stream, desc)
}
fn string_lit_value(expr: &Expr) -> Option<String> {
match expr {
Expr::Lit(ExprLit {
lit: Lit::Str(str), ..
}) => Some(str.value()),
_ => None,
}
}
fn error_on_false_literal(expr: &mut Expr) {
let Expr::Lit(ExprLit {
lit: Lit::Bool(lit),
..
}) = expr
else {
return;
};
if lit.value {
return;
}
let err = syn::Error::new_spanned(&*expr, "contract predicate is always false");
*expr = Expr::Verbatim(err.into_compile_error());
}
fn rewrite(segments: Vec<TokenTree>) -> proc_macro2::TokenStream {
let mut lhs = vec![];
let mut rhs: Option<_> = None;
let mut span: Option<_> = None;
let mut idx = 0;
'segment: while let Some(tt) = segments.get(idx) {
match tt {
TokenTree::Group(group) => {
let stream: Vec<_> = group.stream().into_iter().collect();
let new_stream: TokenStream = rewrite(stream).into_iter().collect();
let mut new_group = proc_macro2::Group::new(group.delimiter(), new_stream);
new_group.set_span(group.span());
lhs.push(TokenTree::Group(new_group));
idx += 1;
}
TokenTree::Ident(_) => {
lhs.push(tt.clone());
idx += 1;
}
TokenTree::Literal(_) => {
lhs.push(tt.clone());
idx += 1;
}
TokenTree::Punct(_) => {
let punct = |idx: usize, c: char, s: Spacing| -> bool {
let tt = if let Some(val) = segments.get(idx) {
val
} else {
return false;
};
if let TokenTree::Punct(p) = tt {
p.as_char() == c && p.spacing() == s
} else {
false
}
};
if punct(idx, '-', Spacing::Joint) && punct(idx + 1, '>', Spacing::Alone) {
let rest = Vec::from(&segments[idx + 2..]);
let rhs_stream = rewrite(rest);
rhs = Some(rhs_stream);
span = Some(segments[idx + 1].span());
break 'segment;
} else {
'op: while let Some(tt) = segments.get(idx) {
match tt {
TokenTree::Punct(p) => {
if p.spacing() == Spacing::Alone {
lhs.push(tt.clone());
idx += 1;
break 'op;
} else {
lhs.push(tt.clone());
idx += 1;
}
}
_ => {
break 'op;
}
}
}
}
}
}
}
match (rhs, span) {
(None, None) => lhs.into_iter().collect(),
(None, Some(_)) => {
unreachable!("If there's a span there should be an implication")
}
(Some(_), None) => unreachable!("Invalid spans"),
(Some(rhs), Some(span)) => {
let lhs: TokenStream = lhs.into_iter().collect();
quote::quote_spanned! {
span =>
(!(#lhs) || #rhs)
}
}
}
}
fn segment_input(tts: TokenStream) -> Vec<Vec<TokenTree>> {
let mut groups = vec![];
let mut group = vec![];
for tt in tts {
match tt {
TokenTree::Punct(p) if p.as_char() == ',' && p.spacing() == Spacing::Alone => {
groups.push(group);
group = vec![];
}
t => group.push(t),
}
}
if !group.is_empty() {
groups.push(group);
}
groups
}