use alloc::{
string::{String, ToString},
vec::Vec,
};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{
Expr, Ident, LitStr, Token,
parse::{Parse, ParseStream},
punctuated::Punctuated,
};
use crate::{
expand::Expand,
field::{FieldId, Fields},
resolve::Resolve,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Format {
literal: LitStr,
arguments: Punctuated<Argument, Token![,]>,
}
impl Parse for Format {
#[inline]
fn parse(input: ParseStream) -> syn::Result<Self> {
let literal = input.parse()?;
let arguments = if input.peek(Token![,]) {
let _ = input.parse::<Token![,]>()?;
Punctuated::<Argument, Token![,]>::parse_terminated(input)?
} else {
Punctuated::new()
};
Ok(Self { literal, arguments })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Argument {
name: Option<Ident>,
expression: Expr,
}
impl Parse for Argument {
#[inline]
fn parse(input: ParseStream) -> syn::Result<Self> {
let name = if input.peek(Ident) && input.peek2(Token![=]) && !input.peek2(Token![==]) {
let name = input.parse()?;
let _ = input.parse::<Token![=]>()?;
Some(name)
} else {
None
};
let expression = input.parse()?;
Ok(Self { name, expression })
}
}
impl ToTokens for Argument {
#[inline]
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self { name, expression } = self;
match name {
Some(name) => quote::quote!(#name = #expression).to_tokens(tokens),
None => expression.to_tokens(tokens),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FormatTrait {
Display,
Debug,
LowerHex,
UpperHex,
Octal,
Binary,
LowerExp,
UpperExp,
Pointer,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FormatUse {
field: FieldId,
format_trait: FormatTrait,
}
impl FormatUse {
#[inline]
#[must_use]
pub const fn field(&self) -> FieldId {
let Self { field, .. } = self;
*field
}
#[inline]
#[must_use]
pub const fn format_trait(&self) -> FormatTrait {
let Self { format_trait, .. } = self;
*format_trait
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Resolved {
literal: LitStr,
arguments: syn::punctuated::Punctuated<Argument, syn::Token![,]>,
uses: Vec<FormatUse>,
fields: Vec<FieldId>,
static_message: bool,
}
impl Resolved {
fn resolve_format(raw: Format, fields: &Fields) -> syn::Result<Self> {
let explicit_names = Self::explicit_names(&raw.arguments);
let has_positional = raw.arguments.iter().any(|argument| Self::explicit_name(argument).is_none());
let value = raw.literal.value();
let mut read = value.as_str();
let mut output = String::with_capacity(value.len());
let mut uses = Vec::new();
let mut selected = Vec::new();
while let Some(open) = read.find('{') {
output.push_str(&read[..open]);
read = &read[open..];
if let Some(rest) = read.strip_prefix("{{") {
output.push_str("{{");
read = rest;
continue;
}
let Some(close) = read[1..].find('}') else {
output.push_str(read);
read = "";
break;
};
let close = close + 1;
let inside = &read[1..close];
let rewritten = Self::resolve_capture(
inside,
fields,
&explicit_names,
has_positional,
&mut uses,
&mut selected,
raw.literal.span(),
)?;
output.push('{');
output.push_str(&rewritten);
output.push('}');
read = &read[close + 1..];
}
output.push_str(read);
let static_message = raw.arguments.is_empty() && !value.contains('{') && !value.contains('}');
let format = LitStr::new(&output, raw.literal.span());
Ok(Self {
literal: format,
arguments: raw.arguments,
uses,
fields: selected,
static_message,
})
}
fn resolve_capture(
inside: &str,
fields: &Fields,
explicit_names: &[String],
has_positional: bool,
uses: &mut Vec<FormatUse>,
selected: &mut Vec<FieldId>,
span: proc_macro2::Span,
) -> syn::Result<String> {
let argument_end = inside.find(':').unwrap_or(inside.len());
let argument = &inside[..argument_end];
let spec = &inside[argument_end..];
let mut rewritten = String::with_capacity(inside.len() + 1);
match Self::capture_field(argument, fields, explicit_names, span)? {
Some(field) => {
if argument.as_bytes().first().is_some_and(u8::is_ascii_digit) && has_positional {
return Err(syn::Error::new(
span,
"ambiguous numeric field capture with explicit positional format arguments",
));
}
Self::push_field(selected, field);
uses.push(FormatUse {
field,
format_trait: Self::format_trait(spec),
});
match fields.name(field) {
Some(_) => rewritten.push_str(argument),
None => {
rewritten.push('_');
rewritten.push_str(argument);
}
}
}
None => rewritten.push_str(argument),
}
rewritten.push_str(&Self::resolve_dynamic_spec(spec, fields, selected, span)?);
Ok(rewritten)
}
fn capture_field(argument: &str, fields: &Fields, explicit_names: &[String], span: proc_macro2::Span) -> syn::Result<Option<FieldId>> {
if argument.is_empty() {
return Ok(None);
}
if argument.as_bytes().iter().all(u8::is_ascii_digit) {
let index = argument
.parse::<usize>()
.map_err(|_| syn::Error::new(span, "invalid positional format capture"))?;
return match index < fields.len() && fields.name(FieldId::from_index(index)).is_none() {
true => Ok(Some(FieldId::from_index(index))),
false => Ok(None),
};
}
if explicit_names.iter().any(|name| name == argument) {
return Ok(None);
}
let ident = syn::parse_str::<syn::Ident>(argument).map_err(|_| syn::Error::new(span, "invalid named format capture"))?;
fields.named(&ident).map(Some)
}
fn resolve_dynamic_spec(spec: &str, fields: &Fields, selected: &mut Vec<FieldId>, span: proc_macro2::Span) -> syn::Result<String> {
let mut output = String::with_capacity(spec.len());
let bytes = spec.as_bytes();
let mut index = 0;
while index < bytes.len() {
let start = index;
let is_word = bytes[index].is_ascii_alphanumeric() || bytes[index] == b'_';
if !is_word {
output.push(bytes[index] as char);
index += 1;
continue;
}
while index < bytes.len() && (bytes[index].is_ascii_alphanumeric() || bytes[index] == b'_') {
index += 1;
}
let token = &spec[start..index];
let dynamic = bytes.get(index) == Some(&b'$');
if !dynamic {
output.push_str(token);
continue;
}
if token.as_bytes().iter().all(u8::is_ascii_digit) {
let field_index = token
.parse::<usize>()
.map_err(|_| syn::Error::new(span, "invalid dynamic format field index"))?;
if field_index < fields.len() && fields.name(FieldId::from_index(field_index)).is_none() {
let field = FieldId::from_index(field_index);
Self::push_field(selected, field);
output.push('_');
}
} else if let Ok(ident) = syn::parse_str::<syn::Ident>(token)
&& let Ok(field) = fields.named(&ident)
{
Self::push_field(selected, field);
}
output.push_str(token);
}
Ok(output)
}
fn format_trait(spec: &str) -> FormatTrait {
match spec.chars().next_back() {
Some('?') => FormatTrait::Debug,
Some('o') => FormatTrait::Octal,
Some('x') => FormatTrait::LowerHex,
Some('X') => FormatTrait::UpperHex,
Some('p') => FormatTrait::Pointer,
Some('b') => FormatTrait::Binary,
Some('e') => FormatTrait::LowerExp,
Some('E') => FormatTrait::UpperExp,
_ => FormatTrait::Display,
}
}
fn explicit_names(arguments: &syn::punctuated::Punctuated<Argument, syn::Token![,]>) -> Vec<String> {
arguments
.iter()
.filter_map(|argument| argument.name.as_ref())
.map(ToString::to_string)
.collect()
}
const fn explicit_name(argument: &Argument) -> Option<&syn::Ident> {
argument.name.as_ref()
}
fn push_field(fields: &mut Vec<FieldId>, field: FieldId) {
if !fields.contains(&field) {
fields.push(field);
}
}
}
impl Resolved {
#[inline]
#[must_use]
pub const fn literal(&self) -> &LitStr {
let Self { literal, .. } = self;
literal
}
#[inline]
#[must_use]
pub const fn arguments(&self) -> &Punctuated<Argument, Token![,]> {
let Self { arguments, .. } = self;
arguments
}
#[inline]
#[must_use]
pub fn uses(&self) -> &[FormatUse] {
let Self { uses, .. } = self;
uses
}
#[inline]
#[must_use]
pub fn fields(&self) -> &[FieldId] {
let Self { fields, .. } = self;
fields
}
#[inline]
#[must_use]
pub const fn is_static(&self) -> bool {
let Self { static_message, .. } = self;
*static_message
}
}
impl Resolve for Format {
type Context = Fields;
type Output = Resolved;
#[inline]
fn resolve(self, fields: &Self::Context) -> syn::Result<Self::Output> {
Resolved::resolve_format(self, fields)
}
}
#[derive(Clone, Debug)]
pub struct ExpandContext(TokenStream);
impl ExpandContext {
#[inline]
#[must_use]
pub const fn new(root: TokenStream) -> Self {
Self(root)
}
}
impl Expand for &Resolved {
type Context = ExpandContext;
#[inline]
fn expand_with(self, context: Self::Context) -> syn::Result<TokenStream> {
let ExpandContext(root) = context;
let literal = self.literal();
let arguments = self.arguments();
if self.is_static() {
Ok(quote! { f.write_str(#literal) })
} else {
Ok(quote! { #root::write!(f, #literal, #arguments) })
}
}
}