use std::collections::BTreeMap;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
use quote::quote;
use syn::{
Expr, Ident, LitStr, Token,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
struct PortableArgs {
format: LitStr,
args: Punctuated<Expr, Token![,]>,
}
impl Parse for PortableArgs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let format = input.parse()?;
let args = if input.is_empty() {
Punctuated::new()
} else {
input.parse::<Token![,]>()?;
Punctuated::parse_terminated(input)?
};
Ok(Self { format, args })
}
}
#[proc_macro]
pub fn expand_captures(input: TokenStream) -> TokenStream {
expand_captures_impl(input.into()).into()
}
fn expand_captures_impl(input: TokenStream2) -> TokenStream2 {
let Some((callback, level, original_args)) = unpack(input) else {
return quote!(compile_error!("invalid consortium-log macro expansion"));
};
let Ok(args) = syn::parse2::<PortableArgs>(original_args.clone()) else {
return quote!(#callback!(#level, #original_args));
};
if args.args.iter().any(|arg| matches!(arg, Expr::Assign(_))) {
return quote!(#callback!(#level, #original_args));
}
let mut captures = Vec::<Ident>::new();
let mut capture_indices = BTreeMap::<String, usize>::new();
let first_capture_index = args.args.len();
let rewritten = rewrite_format(
&args.format.value(),
args.format.span(),
first_capture_index,
&mut captures,
&mut capture_indices,
);
let format = LitStr::new(&rewritten, args.format.span());
let explicit_args: Vec<_> = args.args.into_iter().collect();
quote!(
#callback!(
#level,
#format
#(, #explicit_args)*
#(, #captures)*
)
)
}
fn unpack(input: TokenStream2) -> Option<(TokenStream2, TokenStream2, TokenStream2)> {
let mut tokens = input.into_iter();
let callback = group_contents(tokens.next()?)?;
let level = group_contents(tokens.next()?)?;
let args = group_contents(tokens.next()?)?;
tokens.next().is_none().then_some((callback, level, args))
}
fn group_contents(token: TokenTree) -> Option<TokenStream2> {
match token {
TokenTree::Group(group) => Some(group.stream()),
_ => None,
}
}
fn rewrite_format(
input: &str,
capture_span: Span,
first_capture_index: usize,
captures: &mut Vec<Ident>,
capture_indices: &mut BTreeMap<String, usize>,
) -> String {
let mut output = String::with_capacity(input.len());
let mut chars = input.char_indices().peekable();
while let Some((start, ch)) = chars.next() {
if ch != '{' {
output.push(ch);
continue;
}
if chars.peek().is_some_and(|(_, next)| *next == '{') {
output.push_str("{{");
chars.next();
continue;
}
let Some((end, _)) = chars.clone().find(|(_, candidate)| *candidate == '}') else {
output.push_str(&input[start..]);
break;
};
let content_start = start + ch.len_utf8();
let content = &input[content_start..end];
let name = content.split_once(':').map_or(content, |(name, _)| name);
let Ok(mut ident) = syn::parse_str::<Ident>(name) else {
output.push(ch);
continue;
};
ident.set_span(capture_span);
let index = *capture_indices.entry(name.to_owned()).or_insert_with(|| {
let index = first_capture_index + captures.len();
captures.push(ident);
index
});
output.push('{');
output.push_str(&index.to_string());
output.push_str(&content[name.len()..]);
output.push('}');
while chars.peek().is_some_and(|(position, _)| *position <= end) {
chars.next();
}
}
output
}
#[cfg(test)]
mod tests {
use super::rewrite_format;
use proc_macro2::Span;
use std::collections::BTreeMap;
fn rewrite(input: &str, explicit_args: usize) -> (String, Vec<String>) {
let mut captures = Vec::new();
let mut indices = BTreeMap::new();
let output = rewrite_format(
input,
Span::call_site(),
explicit_args,
&mut captures,
&mut indices,
);
(
output,
captures
.into_iter()
.map(|ident| ident.to_string())
.collect(),
)
}
#[test]
fn rewrites_implicit_captures_to_appended_indices() {
assert_eq!(
rewrite("{error}: value={value:?}; again={error}", 1),
(
"{1}: value={2:?}; again={1}".to_owned(),
vec!["error".to_owned(), "value".to_owned()]
)
);
}
#[test]
fn preserves_positional_parameters_and_escaped_braces() {
assert_eq!(
rewrite("{{name}} {} {0} {=u8}", 1),
("{{name}} {} {0} {=u8}".to_owned(), Vec::new())
);
}
}