use crate::ast::Field; use crate::attr::Display; use proc_macro2::TokenTree; use quote::{format_ident, quote_spanned}; use std::collections::HashSet as Set; use syn::ext::IdentExt; use syn::parse::{ParseStream, Parser}; use syn::{Ident, Index, LitStr, Member, Result, Token}; impl Display<'_> { // Transform `"error {var}"` to `"error {}", var`. pub fn expand_shorthand(&mut self, fields: &[Field]) { let raw_args = self.args.clone(); let mut named_args = explicit_named_args.parse2(raw_args).unwrap(); let fields: Set = fields.iter().map(|f| f.member.clone()).collect(); let span = self.fmt.span(); let fmt = self.fmt.value(); let mut read = fmt.as_str(); let mut out = String::new(); let mut args = self.args.clone(); let mut has_bonus_display = false; let mut has_trailing_comma = false; if let Some(TokenTree::Punct(punct)) = args.clone().into_iter().last() { if punct.as_char() == ',' { has_trailing_comma = true; } } while let Some(brace) = read.find('{') { out += &read[..brace + 1]; read = &read[brace + 1..]; if read.starts_with('{') { out.push('{'); read = &read[1..]; continue; } let next = match read.chars().next() { Some(next) => next, None => return, }; let member = match next { '0'..='9' => { let int = take_int(&mut read); let member = match int.parse::() { Ok(index) => Member::Unnamed(Index { index, span }), Err(_) => return, }; if !fields.contains(&member) { out += ∫ continue; } member } 'a'..='z' | 'A'..='Z' | '_' => { let ident = take_ident(&mut read); Member::Named(Ident::new(&ident, span)) } _ => continue, }; let local = match &member { Member::Unnamed(index) => format_ident!("_{}", index), Member::Named(ident) => ident.clone(), }; let mut formatvar = local.clone(); if formatvar.to_string().starts_with('_') { // Work around leading underscore being rejected by 1.40 and // older compilers. https://github.com/rust-lang/rust/pull/66847 formatvar = format_ident!("field_{}", formatvar); } out += &formatvar.to_string(); if !named_args.insert(formatvar.clone()) { // Already specified in the format argument list. continue; } if !has_trailing_comma { args.extend(quote_spanned!(span=> ,)); } args.extend(quote_spanned!(span=> #formatvar = #local)); if read.starts_with('}') && fields.contains(&member) { has_bonus_display = true; args.extend(quote_spanned!(span=> .as_display())); } has_trailing_comma = false; } out += read; self.fmt = LitStr::new(&out, self.fmt.span()); self.args = args; self.has_bonus_display = has_bonus_display; } } fn explicit_named_args(input: ParseStream) -> Result> { let mut named_args = Set::new(); while !input.is_empty() { if input.peek(Token![,]) && input.peek2(Ident::peek_any) && input.peek3(Token![=]) { input.parse::()?; let ident: Ident = input.parse()?; input.parse::()?; named_args.insert(ident); } else { input.parse::()?; } } Ok(named_args) } fn take_int(read: &mut &str) -> String { let mut int = String::new(); for (i, ch) in read.char_indices() { match ch { '0'..='9' => int.push(ch), _ => { *read = &read[i..]; break; } } } int } fn take_ident(read: &mut &str) -> String { let mut ident = String::new(); for (i, ch) in read.char_indices() { match ch { 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => ident.push(ch), _ => { *read = &read[i..]; break; } } } ident }