diff --git a/Cargo.lock b/Cargo.lock index 0ad225fa..ad4c4a18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,6 +16,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", + "rustversion", "syn", "unicode-xid", ] @@ -47,6 +48,12 @@ dependencies = [ "semver", ] +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + [[package]] name = "semver" version = "1.0.4" diff --git a/Cargo.toml b/Cargo.toml index 8b760717..63b5aab6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ unicode-xid = { version = "0.2.2", optional = true } [build-dependencies] rustc_version = { version = "0.4", optional = true } +[dev-dependencies] +rustversion = "1.0" + [badges] github = { repository = "JelteF/derive_more", workflow = "CI" } diff --git a/doc/display.md b/doc/display.md index eab878aa..1dcda404 100644 --- a/doc/display.md +++ b/doc/display.md @@ -28,6 +28,14 @@ The variables available in the arguments is `self` and each member of the varian with members of tuple structs being named with a leading underscore and their index, i.e. `_0`, `_1`, `_2`, etc. +Although [captured identifiers in format strings are supported since 1.58 +Rust](https://blog.rust-lang.org/2022/01/13/Rust-1.58.0.html#captured-identifiers-in-format-strings), +we support this feature on earlier versions of Rust too. This means that +`#[display(fmt = "Prefix: {field}")]` is completely valid on MSRV. + +> __NOTE:__ Underscored named parameters like `#[display(fmt = "Prefix: {_0}")]` +> [are supported only since 1.41 Rust](https://github.com/rust-lang/rust/pull/66847). + ## Other formatting traits The syntax does not change, but the name of the attribute is the snake case version of the trait. @@ -110,11 +118,11 @@ use std::path::PathBuf; struct MyInt(i32); #[derive(DebugCustom)] -#[debug(fmt = "MyIntDbg(as hex: {:x}, as dec: {})", _0, _0)] +#[debug(fmt = "MyIntDbg(as hex: {_0:x}, as dec: {_0})")] struct MyIntDbg(i32); #[derive(Display)] -#[display(fmt = "({}, {})", x, y)] +#[display(fmt = "({x}, {y})")] struct Point2D { x: i32, y: i32, diff --git a/src/display.rs b/src/display.rs index 5a03ce26..a6b9708e 100644 --- a/src/display.rs +++ b/src/display.rs @@ -359,6 +359,7 @@ impl<'a, 'b> State<'a, 'b> { == "fmt" => { let expected_affix_usage = "outer `enum` `fmt` is an affix spec that expects no args and at most 1 placeholder for inner variant display"; + let placeholders = Placeholder::parse_fmt_string(&fmt.value()); if outer_enum { if list.nested.iter().skip(1).count() != 0 { return Err(Error::new( @@ -366,36 +367,18 @@ impl<'a, 'b> State<'a, 'b> { expected_affix_usage, )); } - // TODO: Check for a single `Display` group? - let fmt_string = match &list.nested[0] { - syn::NestedMeta::Meta(syn::Meta::NameValue( - syn::MetaNameValue { - path, - lit: syn::Lit::Str(s), - .. - }, - )) if path - .segments + if placeholders.len() > 1 + || placeholders .first() - .expect("path shouldn't be empty") - .ident - == "fmt" => - { - s.value() - } - // This one has been checked already in get_meta_fmt() method. - _ => unreachable!(), - }; - - let num_placeholders = - Placeholder::parse_fmt_string(&fmt_string).len(); - if num_placeholders > 1 { + .map(|p| p.arg != Parameter::Positional(0)) + .unwrap_or_default() + { return Err(Error::new( list.nested[1].span(), expected_affix_usage, )); } - if num_placeholders == 1 { + if placeholders.len() == 1 { return Ok((quote_spanned!(fmt.span()=> #fmt), true)); } } @@ -421,8 +404,28 @@ impl<'a, 'b> State<'a, 'b> { Ok(quote_spanned!(list.span()=> #args #arg,)) })?; + let interpolated_args = placeholders + .into_iter() + .flat_map(|p| { + let map_argument = |arg| match arg { + Parameter::Named(i) => Some(i), + Parameter::Positional(_) => None, + }; + map_argument(p.arg) + .into_iter() + .chain(p.width.and_then(map_argument)) + .chain(p.precision.and_then(map_argument)) + }) + .collect::>() + .into_iter() + .map(|ident| { + let ident = syn::Ident::new(&ident, fmt.span()); + quote! { #ident = #ident, } + }) + .collect::(); + Ok(( - quote_spanned!(meta.span()=> write!(_derive_more_display_formatter, #fmt, #args)), + quote_spanned!(meta.span()=> write!(_derive_more_display_formatter, #fmt, #args #interpolated_args)), false, )) } @@ -665,10 +668,7 @@ impl<'a, 'b> State<'a, 'b> { _ => unreachable!(), }) .collect(); - if fmt_args.is_empty() { - return HashMap::default(); - } - let fmt_string = match &list.nested[0] { + let (fmt_string, fmt_span) = match &list.nested[0] { syn::NestedMeta::Meta(syn::Meta::NameValue(syn::MetaNameValue { path, lit: syn::Lit::Str(s), @@ -680,7 +680,7 @@ impl<'a, 'b> State<'a, 'b> { .ident == "fmt" => { - s.value() + (s.value(), s.span()) } // This one has been checked already in get_meta_fmt() method. _ => unreachable!(), @@ -689,7 +689,11 @@ impl<'a, 'b> State<'a, 'b> { Placeholder::parse_fmt_string(&fmt_string).into_iter().fold( HashMap::default(), |mut bounds, pl| { - if let Some(arg) = fmt_args.get(&pl.position) { + let arg = match pl.arg { + Parameter::Positional(i) => fmt_args.get(&i).cloned(), + Parameter::Named(i) => Some(syn::Ident::new(&i, fmt_span).into()), + }; + if let Some(arg) = &arg { if fields_type_params.contains_key(arg) { bounds .entry(fields_type_params[arg].clone()) @@ -733,11 +737,47 @@ impl<'a, 'b> State<'a, 'b> { } } +/// [Parameter][1] used in [`Placeholder`]. +/// +/// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-parameters +#[derive(Debug, Eq, PartialEq)] +enum Parameter { + /// [Positional parameter][1]. + /// + /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#positional-parameters + Positional(usize), + + /// [Named parameter][1]. + /// + /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#named-parameters + Named(String), +} + +impl<'a> From> for Parameter { + fn from(arg: parsing::Argument<'a>) -> Self { + match arg { + parsing::Argument::Integer(i) => Parameter::Positional(i), + parsing::Argument::Identifier(i) => Parameter::Named(i.to_owned()), + } + } +} + /// Representation of formatting placeholder. #[derive(Debug, PartialEq)] struct Placeholder { - /// Position of formatting argument to be used for this placeholder. - position: usize, + /// Formatting argument (either named or positional) to be used by this placeholder. + arg: Parameter, + + /// [Width parameter][1], if present. + /// + /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#width + width: Option, + + /// [Precision parameter][1], if present. + /// + /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#precision + precision: Option, + /// Name of [`std::fmt`] trait to be used for rendering this placeholder. trait_name: &'static str, } @@ -754,19 +794,25 @@ impl Placeholder { format.arg, format.spec.map(|s| s.ty).unwrap_or(parsing::Type::Display), ); - let position = maybe_arg - .and_then(|arg| match arg { - parsing::Argument::Integer(i) => Some(i), - parsing::Argument::Identifier(_) => None, - }) - .unwrap_or_else(|| { - // Assign "the next argument". - // https://doc.rust-lang.org/stable/std/fmt/index.html#positional-parameters - n += 1; - n - 1 - }); + let position = maybe_arg.map(Into::into).unwrap_or_else(|| { + // Assign "the next argument". + // https://doc.rust-lang.org/stable/std/fmt/index.html#positional-parameters + n += 1; + Parameter::Positional(n - 1) + }); + Placeholder { - position, + arg: position, + width: format.spec.and_then(|s| match s.width { + Some(parsing::Count::Parameter(arg)) => Some(arg.into()), + _ => None, + }), + precision: format.spec.and_then(|s| match s.precision { + Some(parsing::Precision::Count(parsing::Count::Parameter( + arg, + ))) => Some(arg.into()), + _ => None, + }), trait_name: ty.trait_name(), } }) @@ -780,32 +826,44 @@ mod placeholder_parse_fmt_string_spec { #[test] fn indicates_position_and_trait_name_for_each_fmt_placeholder() { - let fmt_string = "{},{:?},{{}},{{{1:0$}}}-{2:.1$x}{0:#?}{:width$}"; + let fmt_string = "{},{:?},{{}},{{{1:0$}}}-{2:.1$x}{par:#?}{:width$}"; assert_eq!( Placeholder::parse_fmt_string(&fmt_string), vec![ Placeholder { - position: 0, + arg: Parameter::Positional(0), + width: None, + precision: None, trait_name: "Display", }, Placeholder { - position: 1, + arg: Parameter::Positional(1), + width: None, + precision: None, trait_name: "Debug", }, Placeholder { - position: 1, + arg: Parameter::Positional(1), + width: Some(Parameter::Positional(0)), + precision: None, trait_name: "Display", }, Placeholder { - position: 2, + arg: Parameter::Positional(2), + width: None, + precision: Some(Parameter::Positional(1)), trait_name: "LowerHex", }, Placeholder { - position: 0, + arg: Parameter::Named("par".to_owned()), + width: None, + precision: None, trait_name: "Debug", }, Placeholder { - position: 2, + arg: Parameter::Positional(2), + width: Some(Parameter::Named("width".to_owned())), + precision: None, trait_name: "Display", }, ], diff --git a/tests/display.rs b/tests/display.rs index fae596b8..e778afc8 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -41,7 +41,7 @@ impl PositiveOrNegative { } #[derive(Display)] -#[display(fmt = "{}", message)] +#[display(fmt = "{message}")] struct Error { message: &'static str, backtrace: (), @@ -109,7 +109,7 @@ struct Generic(T); #[display(fmt = "Here's a prefix for {} and a suffix")] enum Affix { A(u32), - #[display(fmt = "{} -- {}", wat, stuff)] + #[display(fmt = "{wat} -- {}", stuff)] B { wat: String, stuff: bool, @@ -171,6 +171,39 @@ mod generic { assert_eq!(NamedGenericStruct { field: 1 }.to_string(), "Generic 1"); } + #[derive(Display)] + #[display(fmt = "Generic {field}")] + struct InterpolatedNamedGenericStruct { + field: T, + } + #[test] + fn interpolated_named_generic_struct() { + assert_eq!( + InterpolatedNamedGenericStruct { field: 1 }.to_string(), + "Generic 1", + ); + } + + #[derive(Display)] + #[display(fmt = "Generic {field:<>width$.prec$} {field}")] + struct InterpolatedNamedGenericStructWidthPrecision { + field: T, + width: usize, + prec: usize, + } + #[test] + fn interpolated_named_generic_struct_width_precision() { + assert_eq!( + InterpolatedNamedGenericStructWidthPrecision { + field: 1.2345, + width: 9, + prec: 2, + } + .to_string(), + "Generic <<<<<1.23 1.2345", + ); + } + #[derive(Display)] struct AutoNamedGenericStruct { field: T, @@ -188,6 +221,16 @@ mod generic { assert_eq!(UnnamedGenericStruct(2).to_string(), "Generic 2"); } + #[rustversion::since(1.41)] // https://github.com/rust-lang/rust/pull/66847 + #[derive(Display)] + #[display(fmt = "Generic {_0}")] + struct InterpolatedUnnamedGenericStruct(T); + #[rustversion::since(1.41)] // https://github.com/rust-lang/rust/pull/66847 + #[test] + fn interpolated_unnamed_generic_struct() { + assert_eq!(InterpolatedUnnamedGenericStruct(2).to_string(), "Generic 2"); + } + #[derive(Display)] struct AutoUnnamedGenericStruct(T); #[test] @@ -208,6 +251,27 @@ mod generic { assert_eq!(GenericEnum::B::(2).to_string(), "Gen::B 2"); } + #[rustversion::since(1.41)] // https://github.com/rust-lang/rust/pull/66847 + #[derive(Display)] + enum InterpolatedGenericEnum { + #[display(fmt = "Gen::A {field}")] + A { field: A }, + #[display(fmt = "Gen::B {_0}")] + B(B), + } + #[rustversion::since(1.41)] // https://github.com/rust-lang/rust/pull/66847 + #[test] + fn interpolated_generic_enum() { + assert_eq!( + InterpolatedGenericEnum::A::<_, u8> { field: 1 }.to_string(), + "Gen::A 1", + ); + assert_eq!( + InterpolatedGenericEnum::B::(2).to_string(), + "Gen::B 2", + ); + } + #[derive(Display)] enum AutoGenericEnum { A { field: A }, @@ -231,6 +295,18 @@ mod generic { assert_eq!(s.to_string(), "8 255 <-> 10 0xff <-> 8 FF"); } + #[derive(Display)] + #[display(fmt = "{} {b} <-> {0:o} {1:#x} <-> {0:?} {1:X?}", a, b)] + struct InterpolatedMultiTraitNamedGenericStruct { + a: A, + b: B, + } + #[test] + fn interpolated_multi_trait_named_generic_struct() { + let s = InterpolatedMultiTraitNamedGenericStruct { a: 8u8, b: 255 }; + assert_eq!(s.to_string(), "8 255 <-> 10 0xff <-> 8 FF"); + } + #[derive(Display)] #[display(fmt = "{} {} {{}} {0:o} {1:#x} - {0:>4?} {1:^4X?}", "_0", "_1")] struct MultiTraitUnnamedGenericStruct(A, B); @@ -240,6 +316,17 @@ mod generic { assert_eq!(s.to_string(), "8 255 {} 10 0xff - 8 FF "); } + #[rustversion::since(1.41)] // https://github.com/rust-lang/rust/pull/66847 + #[derive(Display)] + #[display(fmt = "{} {_1} {{}} {0:o} {1:#x} - {0:>4?} {1:^4X?}", "_0", "_1")] + struct InterpolatedMultiTraitUnnamedGenericStruct(A, B); + #[rustversion::since(1.41)] // https://github.com/rust-lang/rust/pull/66847 + #[test] + fn interpolated_multi_trait_unnamed_generic_struct() { + let s = InterpolatedMultiTraitUnnamedGenericStruct(8u8, 255); + assert_eq!(s.to_string(), "8 255 {} 10 0xff - 8 FF "); + } + #[derive(Display)] #[display(fmt = "{}", "3 * 4")] struct UnusedGenericStruct(T); @@ -384,6 +471,17 @@ mod generic { assert_eq!(s.to_string(), "10 20"); } + #[rustversion::since(1.41)] // https://github.com/rust-lang/rust/pull/66847 + #[test] + fn underscored_simple() { + #[derive(Display)] + #[display(fmt = "{_0} {_1}")] + struct Struct(T1, T2); + + let s = Struct(10, 20); + assert_eq!(s.to_string(), "10 20"); + } + #[test] fn redundant() { #[derive(Display)] @@ -395,6 +493,18 @@ mod generic { assert_eq!(s.to_string(), "10 20"); } + #[rustversion::since(1.41)] // https://github.com/rust-lang/rust/pull/66847 + #[test] + fn underscored_redundant() { + #[derive(Display)] + #[display(bound = "T1: ::core::fmt::Display, T2: ::core::fmt::Display")] + #[display(fmt = "{_0} {_1}")] + struct Struct(T1, T2); + + let s = Struct(10, 20); + assert_eq!(s.to_string(), "10 20"); + } + #[test] fn complex() { trait Trait1 { @@ -425,5 +535,37 @@ mod generic { let s = Struct(10, 20); assert_eq!(s.to_string(), "WHAT 10 EVER 20"); } + + #[rustversion::since(1.41)] // https://github.com/rust-lang/rust/pull/66847 + #[test] + fn underscored_complex() { + trait Trait1 { + fn function1(&self) -> &'static str; + } + + trait Trait2 { + fn function2(&self) -> &'static str; + } + + impl Trait1 for i32 { + fn function1(&self) -> &'static str { + "WHAT" + } + } + + impl Trait2 for i32 { + fn function2(&self) -> &'static str { + "EVER" + } + } + + #[derive(Display)] + #[display(bound = "T1: Trait1 + Trait2, T2: Trait1 + Trait2")] + #[display(fmt = "{} {_0} {} {_1}", "_0.function1()", "_1.function2()")] + struct Struct(T1, T2); + + let s = Struct(10, 20); + assert_eq!(s.to_string(), "WHAT 10 EVER 20"); + } } }