1use std::{fmt::Display, str::FromStr as _};
2
3use proc_macro2::{Ident, Span, TokenStream};
4use quote::{quote, quote_spanned};
5use syn::{
6 parse::Parser as _, punctuated::Punctuated, spanned::Spanned as _, Error, Result,
7};
8
9use crate::utils;
10use utils::{HashMap, HashSet};
11
12/// Provides the hook to expand `#[derive(Display)]` into an implementation of `From`
13pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> Result<TokenStream> {
14 let trait_name = trait_name.trim_end_matches("Custom");
15 let trait_ident = syn::Ident::new(trait_name, Span::call_site());
16 let trait_path = &quote!(::core::fmt::#trait_ident);
17 let trait_attr = trait_name_to_attribute_name(trait_name);
18 let type_params = input
19 .generics
20 .type_params()
21 .map(|t| t.ident.clone())
22 .collect();
23
24 let ParseResult {
25 arms,
26 bounds,
27 requires_helper,
28 } = State {
29 trait_path,
30 trait_attr,
31 input,
32 type_params,
33 }
34 .get_match_arms_and_extra_bounds()?;
35
36 let generics = if !bounds.is_empty() {
37 let bounds: Vec<_> = bounds
38 .into_iter()
39 .map(|(ty, trait_names)| {
40 let bounds: Vec<_> = trait_names
41 .into_iter()
42 .map(|bound| quote!(#bound))
43 .collect();
44 quote!(#ty: #(#bounds)+*)
45 })
46 .collect();
47 let where_clause = quote_spanned!(input.span()=> where #(#bounds),*);
48 utils::add_extra_where_clauses(&input.generics, where_clause)
49 } else {
50 input.generics.clone()
51 };
52 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
53 let name = &input.ident;
54
55 let helper_struct = if requires_helper {
56 display_as_helper_struct()
57 } else {
58 TokenStream::new()
59 };
60
61 Ok(quote! {
62 impl #impl_generics #trait_path for #name #ty_generics #where_clause
63 {
64 #[allow(unused_variables)]
65 #[inline]
66 fn fmt(&self, _derive_more_display_formatter: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
67 #helper_struct
68
69 match self {
70 #arms
71 _ => Ok(()) // This is needed for empty enums
72 }
73 }
74 }
75 })
76}
77
78fn trait_name_to_attribute_name(trait_name: &str) -> &'static str {
79 match trait_name {
80 "Display" => "display",
81 "Binary" => "binary",
82 "Octal" => "octal",
83 "LowerHex" => "lower_hex",
84 "UpperHex" => "upper_hex",
85 "LowerExp" => "lower_exp",
86 "UpperExp" => "upper_exp",
87 "Pointer" => "pointer",
88 "Debug" => "debug",
89 _ => unimplemented!(),
90 }
91}
92
93fn attribute_name_to_trait_name(attribute_name: &str) -> &'static str {
94 match attribute_name {
95 "display" => "Display",
96 "binary" => "Binary",
97 "octal" => "Octal",
98 "lower_hex" => "LowerHex",
99 "upper_hex" => "UpperHex",
100 "lower_exp" => "LowerExp",
101 "upper_exp" => "UpperExp",
102 "pointer" => "Pointer",
103 _ => unreachable!(),
104 }
105}
106
107fn trait_name_to_trait_bound(trait_name: &str) -> syn::TraitBound {
108 let path_segments_iterator: impl Iterator = vecIntoIter<&str>!["core", "fmt", trait_name]
109 .into_iter()
110 .map(|segment: &str| syn::PathSegment::from(Ident::new(string:segment, Span::call_site())));
111
112 syn::TraitBound {
113 lifetimes: None,
114 modifier: syn::TraitBoundModifier::None,
115 paren_token: None,
116 path: syn::Path {
117 leading_colon: Some(syn::Token![::](spans:Span::call_site())),
118 segments: path_segments_iterator.collect(),
119 },
120 }
121}
122
123/// Create a helper struct that is required by some `Display` impls.
124///
125/// The struct is necessary in cases where `Display` is derived for an enum
126/// with an outer `#[display(fmt = "...")]` attribute and if that outer
127/// format-string contains a single placeholder. In that case, we have to
128/// format twice:
129///
130/// - we need to format each variant according to its own, optional
131/// format-string,
132/// - we then need to insert this formatted variant into the outer
133/// format-string.
134///
135/// This helper struct solves this as follows:
136/// - formatting the whole object inserts the helper struct into the outer
137/// format string,
138/// - upon being formatted, the helper struct calls an inner closure to produce
139/// its formatted result,
140/// - the closure in turn uses the inner, optional format-string to produce its
141/// result. If there is no inner format-string, it falls back to plain
142/// `$trait::fmt()`.
143fn display_as_helper_struct() -> TokenStream {
144 quote! {
145 struct _derive_more_DisplayAs<F>(F)
146 where
147 F: ::core::ops::Fn(&mut ::core::fmt::Formatter) -> ::core::fmt::Result;
148
149 const _derive_more_DisplayAs_impl: () = {
150 impl<F> ::core::fmt::Display for _derive_more_DisplayAs<F>
151 where
152 F: ::core::ops::Fn(&mut ::core::fmt::Formatter) -> ::core::fmt::Result
153 {
154 fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
155 (self.0)(f)
156 }
157 }
158 };
159 }
160}
161
162/// Result type of `State::get_match_arms_and_extra_bounds()`.
163#[derive(Default)]
164struct ParseResult {
165 /// The match arms destructuring `self`.
166 arms: TokenStream,
167 /// Any trait bounds that may be required.
168 bounds: HashMap<syn::Type, HashSet<syn::TraitBound>>,
169 /// `true` if the Display impl requires the `DisplayAs` helper struct.
170 requires_helper: bool,
171}
172
173struct State<'a, 'b> {
174 trait_path: &'b TokenStream,
175 trait_attr: &'static str,
176 input: &'a syn::DeriveInput,
177 type_params: HashSet<Ident>,
178}
179
180impl<'a, 'b> State<'a, 'b> {
181 fn get_proper_fmt_syntax(&self) -> impl Display {
182 format!(
183 r#"Proper syntax: #[{}(fmt = "My format", "arg1", "arg2")]"#,
184 self.trait_attr
185 )
186 }
187 fn get_proper_bound_syntax(&self) -> impl Display {
188 format!(
189 "Proper syntax: #[{}(bound = \"T, U: Trait1 + Trait2, V: Trait3\")]",
190 self.trait_attr
191 )
192 }
193
194 fn get_matcher(&self, fields: &syn::Fields) -> TokenStream {
195 match fields {
196 syn::Fields::Unit => TokenStream::new(),
197 syn::Fields::Unnamed(fields) => {
198 let fields: TokenStream = (0..fields.unnamed.len())
199 .map(|n| {
200 let i = Ident::new(&format!("_{}", n), Span::call_site());
201 quote!(#i,)
202 })
203 .collect();
204 quote!((#fields))
205 }
206 syn::Fields::Named(fields) => {
207 let fields: TokenStream = fields
208 .named
209 .iter()
210 .map(|f| {
211 let i = f.ident.as_ref().unwrap();
212 quote!(#i,)
213 })
214 .collect();
215 quote!({#fields})
216 }
217 }
218 }
219 fn find_meta(
220 &self,
221 attrs: &[syn::Attribute],
222 meta_key: &str,
223 ) -> Result<Option<syn::Meta>> {
224 let mut metas = Vec::new();
225 for meta in attrs.iter().filter_map(|attr| attr.parse_meta().ok()) {
226 let meta_list = match &meta {
227 syn::Meta::List(meta) => meta,
228 _ => continue,
229 };
230
231 if !meta_list.path.is_ident(self.trait_attr) {
232 continue;
233 }
234
235 use syn::{Meta, NestedMeta};
236 let meta_nv = match meta_list.nested.first() {
237 Some(NestedMeta::Meta(Meta::NameValue(meta_nv))) => meta_nv,
238 _ => {
239 // If the given attribute is not MetaNameValue, it most likely implies that the
240 // user is writing an incorrect format. For example:
241 // - `#[display()]`
242 // - `#[display("foo")]`
243 // - `#[display(foo)]`
244 return Err(Error::new(
245 meta.span(),
246 format!(
247 r#"The format for this attribute cannot be parsed. Correct format: `#[{}({} = "...")]`"#,
248 self.trait_attr, meta_key
249 ),
250 ));
251 }
252 };
253
254 if meta_nv.path.is_ident(meta_key) {
255 metas.push(meta);
256 }
257 }
258
259 let mut iter = metas.into_iter();
260 let meta = iter.next();
261 if iter.next().is_none() {
262 Ok(meta)
263 } else {
264 Err(Error::new(meta.span(), "Too many attributes specified"))
265 }
266 }
267 fn parse_meta_bounds(
268 &self,
269 bounds: &syn::LitStr,
270 ) -> Result<HashMap<syn::Type, HashSet<syn::TraitBound>>> {
271 let span = bounds.span();
272
273 let input = bounds.value();
274 let tokens = TokenStream::from_str(&input)?;
275 let parser = Punctuated::<syn::GenericParam, syn::Token![,]>::parse_terminated;
276
277 let generic_params = parser
278 .parse2(tokens)
279 .map_err(|error| Error::new(span, error.to_string()))?;
280
281 if generic_params.is_empty() {
282 return Err(Error::new(span, "No bounds specified"));
283 }
284
285 let mut bounds = HashMap::default();
286
287 for generic_param in generic_params {
288 let type_param = match generic_param {
289 syn::GenericParam::Type(type_param) => type_param,
290 _ => return Err(Error::new(span, "Only trait bounds allowed")),
291 };
292
293 if !self.type_params.contains(&type_param.ident) {
294 return Err(Error::new(
295 span,
296 "Unknown generic type argument specified",
297 ));
298 } else if !type_param.attrs.is_empty() {
299 return Err(Error::new(span, "Attributes aren't allowed"));
300 } else if type_param.eq_token.is_some() || type_param.default.is_some() {
301 return Err(Error::new(span, "Default type parameters aren't allowed"));
302 }
303
304 let ident = type_param.ident.to_string();
305
306 let ty = syn::Type::Path(syn::TypePath {
307 qself: None,
308 path: type_param.ident.into(),
309 });
310 let bounds = bounds.entry(ty).or_insert_with(HashSet::default);
311
312 for bound in type_param.bounds {
313 let bound = match bound {
314 syn::TypeParamBound::Trait(bound) => bound,
315 _ => return Err(Error::new(span, "Only trait bounds allowed")),
316 };
317
318 if bound.lifetimes.is_some() {
319 return Err(Error::new(
320 span,
321 "Higher-rank trait bounds aren't allowed",
322 ));
323 }
324
325 bounds.insert(bound);
326 }
327
328 if bounds.is_empty() {
329 return Err(Error::new(
330 span,
331 format!("No bounds specified for type parameter {}", ident),
332 ));
333 }
334 }
335
336 Ok(bounds)
337 }
338 fn parse_meta_fmt(
339 &self,
340 meta: &syn::Meta,
341 outer_enum: bool,
342 ) -> Result<(TokenStream, bool)> {
343 let list = match meta {
344 syn::Meta::List(list) => list,
345 _ => {
346 return Err(Error::new(meta.span(), self.get_proper_fmt_syntax()));
347 }
348 };
349
350 match &list.nested[0] {
351 syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue {
352 path,
353 lit: syn::Lit::Str(fmt),
354 ..
355 })) => match path {
356 op if op.segments.first().expect("path shouldn't be empty").ident
357 == "fmt" =>
358 {
359 let expected_affix_usage = "outer `enum` `fmt` is an affix spec that expects no args and at most 1 placeholder for inner variant display";
360 if outer_enum {
361 if list.nested.iter().skip(1).count() != 0 {
362 return Err(Error::new(
363 list.nested[1].span(),
364 expected_affix_usage,
365 ));
366 }
367 // TODO: Check for a single `Display` group?
368 let fmt_string = match &list.nested[0] {
369 syn::NestedMeta::Meta(syn::Meta::NameValue(
370 syn::MetaNameValue {
371 path,
372 lit: syn::Lit::Str(s),
373 ..
374 },
375 )) if path
376 .segments
377 .first()
378 .expect("path shouldn't be empty")
379 .ident
380 == "fmt" =>
381 {
382 s.value()
383 }
384 // This one has been checked already in get_meta_fmt() method.
385 _ => unreachable!(),
386 };
387
388 let num_placeholders =
389 Placeholder::parse_fmt_string(&fmt_string).len();
390 if num_placeholders > 1 {
391 return Err(Error::new(
392 list.nested[1].span(),
393 expected_affix_usage,
394 ));
395 }
396 if num_placeholders == 1 {
397 return Ok((quote_spanned!(fmt.span()=> #fmt), true));
398 }
399 }
400 let args = list
401 .nested
402 .iter()
403 .skip(1) // skip fmt = "..."
404 .try_fold(TokenStream::new(), |args, arg| {
405 let arg = match arg {
406 syn::NestedMeta::Lit(syn::Lit::Str(s)) => s,
407 syn::NestedMeta::Meta(syn::Meta::Path(i)) => {
408 return Ok(quote_spanned!(list.span()=> #args #i,));
409 }
410 _ => {
411 return Err(Error::new(
412 arg.span(),
413 self.get_proper_fmt_syntax(),
414 ))
415 }
416 };
417 let arg: TokenStream =
418 arg.parse().map_err(|e| Error::new(arg.span(), e))?;
419 Ok(quote_spanned!(list.span()=> #args #arg,))
420 })?;
421
422 Ok((
423 quote_spanned!(meta.span()=> write!(_derive_more_display_formatter, #fmt, #args)),
424 false,
425 ))
426 }
427 _ => Err(Error::new(
428 list.nested[0].span(),
429 self.get_proper_fmt_syntax(),
430 )),
431 },
432 _ => Err(Error::new(
433 list.nested[0].span(),
434 self.get_proper_fmt_syntax(),
435 )),
436 }
437 }
438 fn infer_fmt(&self, fields: &syn::Fields, name: &Ident) -> Result<TokenStream> {
439 let fields = match fields {
440 syn::Fields::Unit => {
441 return Ok(quote!(
442 _derive_more_display_formatter.write_str(stringify!(#name))
443 ))
444 }
445 syn::Fields::Named(fields) => &fields.named,
446 syn::Fields::Unnamed(fields) => &fields.unnamed,
447 };
448 if fields.is_empty() {
449 return Ok(quote!(
450 _derive_more_display_formatter.write_str(stringify!(#name))
451 ));
452 } else if fields.len() > 1 {
453 return Err(Error::new(
454 fields.span(),
455 "Cannot automatically infer format for types with more than 1 field",
456 ));
457 }
458
459 let trait_path = self.trait_path;
460 if let Some(ident) = &fields.iter().next().as_ref().unwrap().ident {
461 Ok(quote!(#trait_path::fmt(#ident, _derive_more_display_formatter)))
462 } else {
463 Ok(quote!(#trait_path::fmt(_0, _derive_more_display_formatter)))
464 }
465 }
466 fn get_match_arms_and_extra_bounds(&self) -> Result<ParseResult> {
467 let result: Result<_> = match &self.input.data {
468 syn::Data::Enum(e) => {
469 match self
470 .find_meta(&self.input.attrs, "fmt")
471 .and_then(|m| m.map(|m| self.parse_meta_fmt(&m, true)).transpose())?
472 {
473 // #[display(fmt = "no placeholder")] on whole enum.
474 Some((fmt, false)) => {
475 e.variants.iter().try_for_each(|v| {
476 if let Some(meta) = self.find_meta(&v.attrs, "fmt")? {
477 Err(Error::new(
478 meta.span(),
479 "`fmt` cannot be used on variant when the whole enum has a format string without a placeholder, maybe you want to add a placeholder?",
480 ))
481 } else {
482 Ok(())
483 }
484 })?;
485
486 Ok(ParseResult {
487 arms: quote_spanned!(self.input.span()=> _ => #fmt,),
488 bounds: HashMap::default(),
489 requires_helper: false,
490 })
491 }
492 // #[display(fmt = "one placeholder: {}")] on whole enum.
493 Some((outer_fmt, true)) => {
494 let fmt: Result<TokenStream> = e.variants.iter().try_fold(TokenStream::new(), |arms, v| {
495 let matcher = self.get_matcher(&v.fields);
496 let fmt = if let Some(meta) = self.find_meta(&v.attrs, "fmt")? {
497 self.parse_meta_fmt(&meta, false)?.0
498 } else {
499 self.infer_fmt(&v.fields, &v.ident)?
500 };
501 let name = &self.input.ident;
502 let v_name = &v.ident;
503 Ok(quote_spanned!(fmt.span()=> #arms #name::#v_name #matcher => write!(
504 _derive_more_display_formatter,
505 #outer_fmt,
506 _derive_more_DisplayAs(|_derive_more_display_formatter| #fmt)
507 ),))
508 });
509 let fmt = fmt?;
510 Ok(ParseResult {
511 arms: quote_spanned!(self.input.span()=> #fmt),
512 bounds: HashMap::default(),
513 requires_helper: true,
514 })
515 }
516 // No format attribute on whole enum.
517 None => e.variants.iter().try_fold(ParseResult::default(), |result, v| {
518 let ParseResult{ arms, mut bounds, requires_helper } = result;
519 let matcher = self.get_matcher(&v.fields);
520 let name = &self.input.ident;
521 let v_name = &v.ident;
522 let fmt: TokenStream;
523 let these_bounds: HashMap<_, _>;
524
525 if let Some(meta) = self.find_meta(&v.attrs, "fmt")? {
526 fmt = self.parse_meta_fmt(&meta, false)?.0;
527 these_bounds = self.get_used_type_params_bounds(&v.fields, &meta);
528 } else {
529 fmt = self.infer_fmt(&v.fields, v_name)?;
530 these_bounds = self.infer_type_params_bounds(&v.fields);
531 };
532 these_bounds.into_iter().for_each(|(ty, trait_names)| {
533 bounds.entry(ty).or_default().extend(trait_names)
534 });
535 let arms = quote_spanned!(self.input.span()=> #arms #name::#v_name #matcher => #fmt,);
536
537 Ok(ParseResult{ arms, bounds, requires_helper })
538 }),
539 }
540 }
541 syn::Data::Struct(s) => {
542 let matcher = self.get_matcher(&s.fields);
543 let name = &self.input.ident;
544 let fmt: TokenStream;
545 let bounds: HashMap<_, _>;
546
547 if let Some(meta) = self.find_meta(&self.input.attrs, "fmt")? {
548 fmt = self.parse_meta_fmt(&meta, false)?.0;
549 bounds = self.get_used_type_params_bounds(&s.fields, &meta);
550 } else {
551 fmt = self.infer_fmt(&s.fields, name)?;
552 bounds = self.infer_type_params_bounds(&s.fields);
553 }
554
555 Ok(ParseResult {
556 arms: quote_spanned!(self.input.span()=> #name #matcher => #fmt,),
557 bounds,
558 requires_helper: false,
559 })
560 }
561 syn::Data::Union(_) => {
562 let meta =
563 self.find_meta(&self.input.attrs, "fmt")?.ok_or_else(|| {
564 Error::new(
565 self.input.span(),
566 "Cannot automatically infer format for unions",
567 )
568 })?;
569 let fmt = self.parse_meta_fmt(&meta, false)?.0;
570
571 Ok(ParseResult {
572 arms: quote_spanned!(self.input.span()=> _ => #fmt,),
573 bounds: HashMap::default(),
574 requires_helper: false,
575 })
576 }
577 };
578
579 let mut result = result?;
580
581 let meta = match self.find_meta(&self.input.attrs, "bound")? {
582 Some(meta) => meta,
583 _ => return Ok(result),
584 };
585
586 let span = meta.span();
587
588 let meta = match meta {
589 syn::Meta::List(meta) => meta.nested,
590 _ => return Err(Error::new(span, self.get_proper_bound_syntax())),
591 };
592
593 if meta.len() != 1 {
594 return Err(Error::new(span, self.get_proper_bound_syntax()));
595 }
596
597 let meta = match &meta[0] {
598 syn::NestedMeta::Meta(syn::Meta::NameValue(meta)) => meta,
599 _ => return Err(Error::new(span, self.get_proper_bound_syntax())),
600 };
601
602 let extra_bounds = match &meta.lit {
603 syn::Lit::Str(extra_bounds) => extra_bounds,
604 _ => return Err(Error::new(span, self.get_proper_bound_syntax())),
605 };
606
607 let extra_bounds = self.parse_meta_bounds(extra_bounds)?;
608
609 extra_bounds.into_iter().for_each(|(ty, trait_names)| {
610 result.bounds.entry(ty).or_default().extend(trait_names)
611 });
612
613 Ok(result)
614 }
615 fn get_used_type_params_bounds(
616 &self,
617 fields: &syn::Fields,
618 meta: &syn::Meta,
619 ) -> HashMap<syn::Type, HashSet<syn::TraitBound>> {
620 if self.type_params.is_empty() {
621 return HashMap::default();
622 }
623
624 let fields_type_params: HashMap<syn::Path, _> = fields
625 .iter()
626 .enumerate()
627 .filter_map(|(i, field)| {
628 utils::get_if_type_parameter_used_in_type(&self.type_params, &field.ty)
629 .map(|ty| {
630 (
631 field
632 .ident
633 .clone()
634 .unwrap_or_else(|| {
635 Ident::new(&format!("_{}", i), Span::call_site())
636 })
637 .into(),
638 ty,
639 )
640 })
641 })
642 .collect();
643 if fields_type_params.is_empty() {
644 return HashMap::default();
645 }
646
647 let list = match meta {
648 syn::Meta::List(list) => list,
649 // This one has been checked already in get_meta_fmt() method.
650 _ => unreachable!(),
651 };
652 let fmt_args: HashMap<_, _> = list
653 .nested
654 .iter()
655 .skip(1) // skip fmt = "..."
656 .enumerate()
657 .filter_map(|(i, arg)| match arg {
658 syn::NestedMeta::Lit(syn::Lit::Str(ref s)) => {
659 syn::parse_str(&s.value()).ok().map(|id| (i, id))
660 }
661 syn::NestedMeta::Meta(syn::Meta::Path(ref id)) => Some((i, id.clone())),
662 // This one has been checked already in get_meta_fmt() method.
663 _ => unreachable!(),
664 })
665 .collect();
666 if fmt_args.is_empty() {
667 return HashMap::default();
668 }
669 let fmt_string = match &list.nested[0] {
670 syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue {
671 path,
672 lit: syn::Lit::Str(s),
673 ..
674 })) if path
675 .segments
676 .first()
677 .expect("path shouldn't be empty")
678 .ident
679 == "fmt" =>
680 {
681 s.value()
682 }
683 // This one has been checked already in get_meta_fmt() method.
684 _ => unreachable!(),
685 };
686
687 Placeholder::parse_fmt_string(&fmt_string).into_iter().fold(
688 HashMap::default(),
689 |mut bounds, pl| {
690 if let Some(arg) = fmt_args.get(&pl.position) {
691 if fields_type_params.contains_key(arg) {
692 bounds
693 .entry(fields_type_params[arg].clone())
694 .or_insert_with(HashSet::default)
695 .insert(trait_name_to_trait_bound(pl.trait_name));
696 }
697 }
698 bounds
699 },
700 )
701 }
702 fn infer_type_params_bounds(
703 &self,
704 fields: &syn::Fields,
705 ) -> HashMap<syn::Type, HashSet<syn::TraitBound>> {
706 if self.type_params.is_empty() {
707 return HashMap::default();
708 }
709 if let syn::Fields::Unit = fields {
710 return HashMap::default();
711 }
712 // infer_fmt() uses only first field.
713 fields
714 .iter()
715 .take(1)
716 .filter_map(|field| {
717 utils::get_if_type_parameter_used_in_type(&self.type_params, &field.ty)
718 .map(|ty| {
719 (
720 ty,
721 [trait_name_to_trait_bound(attribute_name_to_trait_name(
722 self.trait_attr,
723 ))]
724 .iter()
725 .cloned()
726 .collect(),
727 )
728 })
729 })
730 .collect()
731 }
732}
733
734/// Representation of formatting placeholder.
735#[derive(Debug, PartialEq)]
736struct Placeholder {
737 /// Position of formatting argument to be used for this placeholder.
738 position: usize,
739 /// Name of [`std::fmt`] trait to be used for rendering this placeholder.
740 trait_name: &'static str,
741}
742
743impl Placeholder {
744 /// Parses [`Placeholder`]s from a given formatting string.
745 fn parse_fmt_string(s: &str) -> Vec<Placeholder> {
746 let mut n = 0;
747 crate::parsing::all_placeholders(s)
748 .into_iter()
749 .flatten()
750 .map(|m| {
751 let (maybe_arg, maybe_typ) = crate::parsing::format(m).unwrap();
752 let position = maybe_arg.unwrap_or_else(|| {
753 // Assign "the next argument".
754 // https://doc.rust-lang.org/stable/std/fmt/index.html#positional-parameters
755 n += 1;
756 n - 1
757 });
758 let typ = maybe_typ.unwrap_or_default();
759 let trait_name = match typ {
760 "" => "Display",
761 "?" | "x?" | "X?" => "Debug",
762 "o" => "Octal",
763 "x" => "LowerHex",
764 "X" => "UpperHex",
765 "p" => "Pointer",
766 "b" => "Binary",
767 "e" => "LowerExp",
768 "E" => "UpperExp",
769 _ => unreachable!(),
770 };
771 Placeholder {
772 position,
773 trait_name,
774 }
775 })
776 .collect()
777 }
778}
779
780#[cfg(test)]
781mod regex_maybe_placeholder_spec {
782
783 #[test]
784 fn parses_placeholders_and_omits_escaped() {
785 let fmt_string = "{}, {:?}, {{}}, {{{1:0$}}}";
786 let placeholders: Vec<_> = crate::parsing::all_placeholders(&fmt_string)
787 .into_iter()
788 .flatten()
789 .collect();
790 assert_eq!(placeholders, vec!["{}", "{:?}", "{1:0$}"]);
791 }
792}
793
794#[cfg(test)]
795mod regex_placeholder_format_spec {
796
797 #[test]
798 fn detects_type() {
799 for (p, expected) in vec![
800 ("{}", ""),
801 ("{:?}", "?"),
802 ("{:x?}", "x?"),
803 ("{:X?}", "X?"),
804 ("{:o}", "o"),
805 ("{:x}", "x"),
806 ("{:X}", "X"),
807 ("{:p}", "p"),
808 ("{:b}", "b"),
809 ("{:e}", "e"),
810 ("{:E}", "E"),
811 ("{:.*}", ""),
812 ("{8}", ""),
813 ("{:04}", ""),
814 ("{1:0$}", ""),
815 ("{:width$}", ""),
816 ("{9:>8.*}", ""),
817 ("{2:.1$x}", "x"),
818 ] {
819 let typ = crate::parsing::format(p).unwrap().1.unwrap_or_default();
820 assert_eq!(typ, expected);
821 }
822 }
823
824 #[test]
825 fn detects_arg() {
826 for (p, expected) in vec![
827 ("{}", ""),
828 ("{0:?}", "0"),
829 ("{12:x?}", "12"),
830 ("{3:X?}", "3"),
831 ("{5:o}", "5"),
832 ("{6:x}", "6"),
833 ("{:X}", ""),
834 ("{8}", "8"),
835 ("{:04}", ""),
836 ("{1:0$}", "1"),
837 ("{:width$}", ""),
838 ("{9:>8.*}", "9"),
839 ("{2:.1$x}", "2"),
840 ] {
841 let arg = crate::parsing::format(p)
842 .unwrap()
843 .0
844 .map(|s| s.to_string())
845 .unwrap_or_default();
846 assert_eq!(arg, String::from(expected));
847 }
848 }
849}
850
851#[cfg(test)]
852mod placeholder_parse_fmt_string_spec {
853 use super::*;
854
855 #[test]
856 fn indicates_position_and_trait_name_for_each_fmt_placeholder() {
857 let fmt_string = "{},{:?},{{}},{{{1:0$}}}-{2:.1$x}{0:#?}{:width$}";
858 assert_eq!(
859 Placeholder::parse_fmt_string(&fmt_string),
860 vec![
861 Placeholder {
862 position: 0,
863 trait_name: "Display",
864 },
865 Placeholder {
866 position: 1,
867 trait_name: "Debug",
868 },
869 Placeholder {
870 position: 1,
871 trait_name: "Display",
872 },
873 Placeholder {
874 position: 2,
875 trait_name: "LowerHex",
876 },
877 Placeholder {
878 position: 0,
879 trait_name: "Debug",
880 },
881 Placeholder {
882 position: 2,
883 trait_name: "Display",
884 },
885 ],
886 )
887 }
888}
889