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