From b8779400986c6dc56ac1955e47556abbce27cfe7 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Tue, 2 Jul 2024 09:59:20 +0200 Subject: [PATCH] Support _variant in outer level enum formatting for Display --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 4 +- Cargo.toml | 2 +- README.md | 4 +- clippy.toml | 2 +- impl/Cargo.toml | 2 +- impl/doc/display.md | 22 +- impl/src/fmt/display.rs | 150 ++- impl/src/fmt/mod.rs | 4 +- impl/src/fmt/parsing.rs | 1005 ++++++++--------- .../shared_format_positional_placeholders.rs | 19 + ...ared_format_positional_placeholders.stderr | 17 + .../display/shared_format_unclosed_brace.rs | 7 + .../shared_format_unclosed_brace.stderr | 9 + .../display/shared_format_variant_spec.rs | 7 + .../display/shared_format_variant_spec.stderr | 5 + tests/display.rs | 125 ++ 17 files changed, 845 insertions(+), 541 deletions(-) create mode 100644 tests/compile_fail/display/shared_format_positional_placeholders.rs create mode 100644 tests/compile_fail/display/shared_format_positional_placeholders.stderr create mode 100644 tests/compile_fail/display/shared_format_unclosed_brace.rs create mode 100644 tests/compile_fail/display/shared_format_unclosed_brace.stderr create mode 100644 tests/compile_fail/display/shared_format_variant_spec.rs create mode 100644 tests/compile_fail/display/shared_format_variant_spec.stderr diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 657f25c0..13439b8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: strategy: fail-fast: false matrix: - msrv: ["1.65.0"] + msrv: ["1.70.0"] os: - ubuntu - macOS diff --git a/CHANGELOG.md b/CHANGELOG.md index a9805322..95d036f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Breaking changes -- The minimum supported Rust version (MSRV) is now Rust 1.65. +- The minimum supported Rust version (MSRV) is now Rust 1.70. - Add the `std` feature which should be disabled in `no_std` environments. - All Cargo features, except `std`, are now disabled by default. The `full` feature can be used to get the old behavior of supporting all possible @@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). [#294](https://github.com/JelteF/derive_more/pull/294)) - The `as_mut` feature is removed, and the `AsMut` derive is now gated by the `as_ref` feature. ([#295](https://github.com/JelteF/derive_more/pull/295)) +- A top level `#[display("...")]` attribute on an enum now requires the usage + of `{_variant}` to include the variant instead of including it at `{}`. ([#377](https://github.com/JelteF/derive_more/pull/377)) ### Added diff --git a/Cargo.toml b/Cargo.toml index 420f60b3..c734182e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "derive_more" version = "1.0.0-beta.6" edition = "2021" -rust-version = "1.65.0" +rust-version = "1.70.0" description = "Adds #[derive(x)] macros for more traits" authors = ["Jelte Fennema "] license = "MIT" diff --git a/README.md b/README.md index 383f773d..4ce210fc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Latest Version](https://img.shields.io/crates/v/derive_more.svg)](https://crates.io/crates/derive_more) [![Rust Documentation](https://docs.rs/derive_more/badge.svg)](https://docs.rs/derive_more) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/JelteF/derive_more/master/LICENSE) -[![Rust 1.65+](https://img.shields.io/badge/rustc-1.65+-lightgray.svg)](https://blog.rust-lang.org/2022/11/03/Rust-1.65.0.html) +[![Rust 1.70+](https://img.shields.io/badge/rustc-1.70+-lightgray.svg)](https://blog.rust-lang.org/2022/11/03/Rust-1.70.0.html) [![Unsafe Forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance) Rust has lots of builtin traits that are implemented for its basic types, such @@ -215,7 +215,7 @@ extern crate derive_more; ## [MSRV] policy -This library requires Rust 1.65 or higher. +This library requires Rust 1.70 or higher. Changing [MSRV] (minimum supported Rust version) of this crate is treated as a **minor version change** in terms of [Semantic Versioning]. - So, if [MSRV] changes are **NOT concerning** for your project, just use the default [caret requirement]: diff --git a/clippy.toml b/clippy.toml index 0c697c31..ca697fd7 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,7 +2,7 @@ # See full lints list at: # https://rust-lang.github.io/rust-clippy/master/index.html -msrv = "1.65.0" +msrv = "1.70.0" # Ensures consistent bracing for macro calls in the codebase. # Extends default settings: diff --git a/impl/Cargo.toml b/impl/Cargo.toml index 8252fdfb..dc6bcf53 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -2,7 +2,7 @@ name = "derive_more-impl" version = "1.0.0-beta.6" edition = "2021" -rust-version = "1.65.0" +rust-version = "1.70.0" description = "Internal implementation of `derive_more` crate" authors = ["Jelte Fennema "] license = "MIT" diff --git a/impl/doc/display.md b/impl/doc/display.md index 5c242bdf..6a7139e6 100644 --- a/impl/doc/display.md +++ b/impl/doc/display.md @@ -26,6 +26,12 @@ 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. +For enums you can also specify a shared format on the enum itself instead of +the variant. This format is used for each of the variants, and can be +customized per variant by including the special `{_variant}` placeholder in +this shared format, which is then replaced by the format string that's provided +on the variant. + ### Other formatting traits @@ -175,6 +181,7 @@ struct Point2D { } #[derive(Display)] +#[display("Enum E: {_variant}")] enum E { Uint(u32), #[display("I am B {:b}", i)] @@ -185,6 +192,13 @@ enum E { Path(PathBuf), } +#[derive(Display)] +#[display("Enum E2: {_0:?}")] +enum E2 { + Uint(u32), + String(&'static str, &'static str), +} + #[derive(Display)] #[display("Hello there!")] union U { @@ -223,9 +237,11 @@ impl PositiveOrNegative { assert_eq!(MyInt(-2).to_string(), "-2"); assert_eq!(Point2D { x: 3, y: 4 }.to_string(), "(3, 4)"); -assert_eq!(E::Uint(2).to_string(), "2"); -assert_eq!(E::Binary { i: -2 }.to_string(), "I am B 11111110"); -assert_eq!(E::Path("abc".into()).to_string(), "I am C abc"); +assert_eq!(E::Uint(2).to_string(), "Enum E: 2"); +assert_eq!(E::Binary { i: -2 }.to_string(), "Enum E: I am B 11111110"); +assert_eq!(E::Path("abc".into()).to_string(), "Enum E: I am C abc"); +assert_eq!(E2::Uint(2).to_string(), "Enum E2: 2"); +assert_eq!(E2::String("shown", "ignored").to_string(), "Enum E2: \"shown\""); assert_eq!(U { i: 2 }.to_string(), "Hello there!"); assert_eq!(format!("{:o}", S), "7"); assert_eq!(format!("{:X}", UH), "UpperHex"); diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs index e41dcbd1..f70c99c5 100644 --- a/impl/src/fmt/display.rs +++ b/impl/src/fmt/display.rs @@ -9,7 +9,7 @@ use syn::{parse_quote, spanned::Spanned as _}; use crate::utils::{attr::ParseMultiple as _, Spanning}; -use super::{trait_name_to_attribute_name, ContainerAttributes}; +use super::{parsing, trait_name_to_attribute_name, ContainerAttributes, FmtAttribute}; /// Expands a [`fmt::Display`]-like derive macro. /// @@ -80,6 +80,7 @@ fn expand_struct( (attrs, ident, trait_ident, _): ExpansionCtx<'_>, ) -> syn::Result<(Vec, TokenStream)> { let s = Expansion { + shared_format: None, attrs, fields: &s.fields, trait_ident, @@ -110,12 +111,8 @@ fn expand_struct( /// Expands a [`fmt`]-like derive macro for the provided enum. fn expand_enum( e: &syn::DataEnum, - (attrs, _, trait_ident, attr_name): ExpansionCtx<'_>, + (shared_attrs, _, trait_ident, attr_name): ExpansionCtx<'_>, ) -> syn::Result<(Vec, TokenStream)> { - if attrs.fmt.is_some() { - todo!("https://github.com/JelteF/derive_more/issues/142"); - } - let (bounds, match_arms) = e.variants.iter().try_fold( (Vec::new(), TokenStream::new()), |(mut bounds, mut arms), variant| { @@ -138,6 +135,7 @@ fn expand_enum( } let v = Expansion { + shared_format: shared_attrs.fmt.as_ref(), attrs: &attrs, fields: &variant.fields, trait_ident, @@ -198,6 +196,9 @@ fn expand_union( /// [`Display::fmt()`]: fmt::Display::fmt() #[derive(Debug)] struct Expansion<'a> { + /// Format shared between all variants of an enum. + shared_format: Option<&'a FmtAttribute>, + /// Derive macro [`ContainerAttributes`]. attrs: &'a ContainerAttributes, @@ -226,6 +227,81 @@ impl<'a> Expansion<'a> { /// [`Display::fmt()`]: fmt::Display::fmt() /// [`FmtAttribute`]: super::FmtAttribute fn generate_body(&self) -> syn::Result { + if self.shared_format.is_none() { + return self.generate_body_impl(); + } + let shared_format = self.shared_format.as_ref().unwrap(); + if !shared_format.args.is_empty() { + return Err(syn::Error::new( + shared_format.args.span(), + "shared format string does not support positional placeholders, use named placeholders instead", + )); + } + let mut tokens = TokenStream::new(); + let mut maybe_body = None; + let mut current_format = String::new(); + let fmt_string = shared_format.lit.value(); + let maybe_format_string = parsing::format_string(&fmt_string); + let Some(format_string) = maybe_format_string else { + // If we could not parse the format string, we just use the original string so + // we get a nice error message. We also panic as a safety precaution in case our + // parsing fails to parse something that write! allows. + return Ok(quote! { + derive_more::core::write!(__derive_more_f, #shared_format); + unreachable!("derive_more could not parse shared format string, but rust could: {:?}", #fmt_string); + }); + }; + for part in format_string.elements { + match part { + parsing::MaybeFormat::Text(s) => { + current_format.push_str(s); + } + parsing::MaybeFormat::Format { raw, format } => { + if format.arg == Some(parsing::Argument::Identifier("_variant")) { + if format.spec.is_some() { + return Err(syn::Error::new( + shared_format.span(), + "shared format _variant placeholder cannot contain format specifiers", + )); + } + if !current_format.is_empty() { + tokens.extend(quote! { derive_more::core::write!(__derive_more_f, #current_format)?; }); + current_format.clear(); + } + if maybe_body.is_none() { + maybe_body = Some(self.generate_body_impl()?); + } + let body = maybe_body.as_ref().unwrap(); + tokens.extend(quote! { #body?; }); + } else { + if format.arg.is_none() + || matches!(format.arg, Some(parsing::Argument::Integer(_))) + { + return Err(syn::Error::new( + shared_format.span(), + "shared format string cannot contain positional placeholders, use named placeholders instead", + )); + } + current_format.push_str(raw); + } + } + }; + } + if !current_format.is_empty() { + tokens.extend( + quote! { derive_more::core::write!(__derive_more_f, #current_format) }, + ) + } else { + tokens.extend(quote! { Ok(()) }); + } + Ok(tokens) + } + + /// Generates [`Display::fmt()`] implementation for a struct or an enum variant + /// without considering `shared_format`. + /// + /// [`Display::fmt()`]: fmt::Display::fmt() + fn generate_body_impl(&self) -> syn::Result { match &self.attrs.fmt { Some(fmt) => { Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() { @@ -267,27 +343,55 @@ impl<'a> Expansion<'a> { /// Generates trait bounds for a struct or an enum variant. fn generate_bounds(&self) -> Vec { + let mut bounds: Vec = + if let Some(shared_format) = self.shared_format { + let shared_bounds = shared_format + .bounded_types(self.fields) + .map(|(ty, trait_name)| { + let trait_ident = format_ident!("{trait_name}"); + + parse_quote! { #ty: derive_more::core::fmt::#trait_ident } + }) + .chain(self.attrs.bounds.0.clone()) + .collect(); + // If it doesn't contain _variant we don't need to add any other bounds + if !parsing::format_string_formats(&shared_format.lit.value()) + .into_iter() + .flatten() + .any(|f| f.arg == Some(parsing::Argument::Identifier("_variant"))) + { + return shared_bounds; + } + shared_bounds + } else { + Vec::new() + }; + let Some(fmt) = &self.attrs.fmt else { - return self - .fields - .iter() - .next() - .map(|f| { - let ty = &f.ty; - let trait_ident = &self.trait_ident; - vec![parse_quote! { #ty: derive_more::core::fmt::#trait_ident }] - }) - .unwrap_or_default(); + bounds.extend( + self.fields + .iter() + .next() + .map(|f| { + let ty = &f.ty; + let trait_ident = &self.trait_ident; + vec![parse_quote! { #ty: derive_more::core::fmt::#trait_ident }] + }) + .unwrap_or_default(), + ); + return bounds; }; - fmt.bounded_types(self.fields) - .map(|(ty, trait_name)| { - let trait_ident = format_ident!("{trait_name}"); + bounds.extend( + fmt.bounded_types(self.fields) + .map(|(ty, trait_name)| { + let trait_ident = format_ident!("{trait_name}"); - parse_quote! { #ty: derive_more::core::fmt::#trait_ident } - }) - .chain(self.attrs.bounds.0.clone()) - .collect() + parse_quote! { #ty: derive_more::core::fmt::#trait_ident } + }) + .chain(self.attrs.bounds.0.clone()), + ); + bounds } } diff --git a/impl/src/fmt/mod.rs b/impl/src/fmt/mod.rs index 324e574c..b28a773f 100644 --- a/impl/src/fmt/mod.rs +++ b/impl/src/fmt/mod.rs @@ -372,9 +372,9 @@ impl Placeholder { /// Parses [`Placeholder`]s from the provided formatting string. fn parse_fmt_string(s: &str) -> Vec { let mut n = 0; - parsing::format_string(s) + parsing::format_string_formats(s) .into_iter() - .flat_map(|f| f.formats) + .flatten() .map(|format| { let (maybe_arg, ty) = ( format.arg, diff --git a/impl/src/fmt/parsing.rs b/impl/src/fmt/parsing.rs index 8a5d7527..74e053fa 100644 --- a/impl/src/fmt/parsing.rs +++ b/impl/src/fmt/parsing.rs @@ -9,7 +9,7 @@ use unicode_xid::UnicodeXID as XID; /// Output of the [`format_string`] parser. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct FormatString<'a> { - pub(crate) formats: Vec>, + pub(crate) elements: Vec>, } /// Output of the [`format`] parser. @@ -150,7 +150,11 @@ type Fill = char; type Width<'a> = Count<'a>; /// Output of the [`maybe_format`] parser. -type MaybeFormat<'a> = Option>; +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) enum MaybeFormat<'a> { + Format { raw: &'a str, format: Format<'a> }, + Text(&'a str), +} /// Output of the [`identifier`] parser. type Identifier<'a> = &'a str; @@ -188,23 +192,32 @@ type LeftToParse<'a> = &'a str; /// parsers). /// /// [0]: std::fmt#syntax -pub(crate) fn format_string(input: &str) -> Option> { - let (mut input, _) = optional_result(text)(input); - - let formats = iter::repeat(()) +pub(crate) fn format_string(mut input: &str) -> Option> { + let elements = iter::repeat(()) .scan(&mut input, |input, _| { - let (curr, format) = - alt(&mut [&mut maybe_format, &mut map(text, |(i, _)| (i, None))])( - input, - )?; + let (curr, format) = alt(&mut [ + &mut maybe_format, + &mut map(text, |(i, x)| (i, MaybeFormat::Text(x))), + ])(input)?; **input = curr; Some(format) }) - .flatten() .collect(); - // Should consume all tokens for a successful parse. - input.is_empty().then_some(FormatString { formats }) + input.is_empty().then_some(FormatString { elements }) +} + +// Same as `format_string` but returns only the `Format` parts of the string. +pub(crate) fn format_string_formats(input: &str) -> Option> { + format_string(input).map(|f| { + f.elements + .into_iter() + .filter_map(|e| match e { + MaybeFormat::Format { format, .. } => Some(format), + _ => None, + }) + .collect() + }) } /// Parses a `maybe_format` as defined in the [grammar spec][0]. @@ -227,9 +240,12 @@ pub(crate) fn format_string(input: &str) -> Option> { /// [0]: std::fmt#syntax fn maybe_format(input: &str) -> Option<(LeftToParse<'_>, MaybeFormat<'_>)> { alt(&mut [ - &mut map(str("{{"), |i| (i, None)), - &mut map(str("}}"), |i| (i, None)), - &mut map(format, |(i, format)| (i, Some(format))), + &mut map(str("{{"), |i| (i, MaybeFormat::Text("{{"))), + &mut map(str("}}"), |i| (i, MaybeFormat::Text("}}"))), + &mut map(format, |(i, format)| { + let raw = &input[..input.len() - i.len()]; + (i, MaybeFormat::Format { raw, format }) + }), ])(input) } @@ -720,592 +736,569 @@ mod tests { #[test] fn text() { - assert_eq!(format_string(""), Some(FormatString { formats: vec![] })); - assert_eq!( - format_string("test"), - Some(FormatString { formats: vec![] }), - ); - assert_eq!( - format_string("Минск"), - Some(FormatString { formats: vec![] }), - ); - assert_eq!(format_string("🦀"), Some(FormatString { formats: vec![] })); + assert_eq!(format_string_formats(""), Some(vec![])); + assert_eq!(format_string_formats("test"), Some(vec![]),); + assert_eq!(format_string_formats("Минск"), Some(vec![]),); + assert_eq!(format_string_formats("🦀"), Some(vec![])); } #[test] fn argument() { assert_eq!( - format_string("{}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: None, - }], - }), + format_string_formats("{}"), + Some(vec![Format { + arg: None, + spec: None, + }],), ); assert_eq!( - format_string("{0}"), - Some(FormatString { - formats: vec![Format { - arg: Some(Argument::Integer(0)), - spec: None, - }], - }), + format_string_formats("{0}"), + Some(vec![Format { + arg: Some(Argument::Integer(0)), + spec: None, + }],), ); assert_eq!( - format_string("{par}"), - Some(FormatString { - formats: vec![Format { - arg: Some(Argument::Identifier("par")), - spec: None, - }], - }), + format_string_formats("{par}"), + Some(vec![Format { + arg: Some(Argument::Identifier("par")), + spec: None, + }],), ); assert_eq!( - format_string("{Минск}"), - Some(FormatString { - formats: vec![Format { - arg: Some(Argument::Identifier("Минск")), - spec: None, - }], - }), + format_string_formats("{Минск}"), + Some(vec![Format { + arg: Some(Argument::Identifier("Минск")), + spec: None, + }],), ); } #[test] fn spec() { assert_eq!( - format_string("{:}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:^}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((None, Align::Center)), - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:^}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((None, Align::Center)), + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:-<}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some('-'), Align::Left)), - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:-<}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some('-'), Align::Left)), + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{: <}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some(' '), Align::Left)), - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{: <}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some(' '), Align::Left)), + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:^<}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some('^'), Align::Left)), - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:^<}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some('^'), Align::Left)), + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:+}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: Some(Sign::Plus), - alternate: None, - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:+}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: Some(Sign::Plus), + alternate: None, + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:^<-}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some('^'), Align::Left)), - sign: Some(Sign::Minus), - alternate: None, - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:^<-}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some('^'), Align::Left)), + sign: Some(Sign::Minus), + alternate: None, + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:#}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: Some(Alternate), - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:#}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: Some(Alternate), + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:+#}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: Some(Sign::Plus), - alternate: Some(Alternate), - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:+#}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: Some(Sign::Plus), + alternate: Some(Alternate), + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:-<#}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some('-'), Align::Left)), - sign: None, - alternate: Some(Alternate), - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:-<#}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some('-'), Align::Left)), + sign: None, + alternate: Some(Alternate), + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:^<-#}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some('^'), Align::Left)), - sign: Some(Sign::Minus), - alternate: Some(Alternate), - zero_padding: None, - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:^<-#}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some('^'), Align::Left)), + sign: Some(Sign::Minus), + alternate: Some(Alternate), + zero_padding: None, + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:0}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: Some(ZeroPadding), - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:0}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: Some(ZeroPadding), + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:#0}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: Some(Alternate), - zero_padding: Some(ZeroPadding), - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:#0}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: Some(Alternate), + zero_padding: Some(ZeroPadding), + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:-0}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: Some(Sign::Minus), - alternate: None, - zero_padding: Some(ZeroPadding), - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:-0}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: Some(Sign::Minus), + alternate: None, + zero_padding: Some(ZeroPadding), + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:^<0}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some('^'), Align::Left)), - sign: None, - alternate: None, - zero_padding: Some(ZeroPadding), - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:^<0}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some('^'), Align::Left)), + sign: None, + alternate: None, + zero_padding: Some(ZeroPadding), + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:^<+#0}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some('^'), Align::Left)), - sign: Some(Sign::Plus), - alternate: Some(Alternate), - zero_padding: Some(ZeroPadding), - width: None, - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:^<+#0}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some('^'), Align::Left)), + sign: Some(Sign::Plus), + alternate: Some(Alternate), + zero_padding: Some(ZeroPadding), + width: None, + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:1}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: None, - width: Some(Count::Integer(1)), - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:1}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: Some(Count::Integer(1)), + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:1$}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: None, - width: Some(Count::Parameter(Argument::Integer(1))), - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:1$}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: Some(Count::Parameter(Argument::Integer(1))), + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:par$}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: None, - width: Some(Count::Parameter(Argument::Identifier("par"))), - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:par$}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: Some(Count::Parameter(Argument::Identifier("par"))), + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:-^-#0Минск$}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some('-'), Align::Center)), - sign: Some(Sign::Minus), - alternate: Some(Alternate), - zero_padding: Some(ZeroPadding), - width: Some(Count::Parameter(Argument::Identifier("Минск"))), - precision: None, - ty: Type::Display, - }), - }], - }), + format_string_formats("{:-^-#0Минск$}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some('-'), Align::Center)), + sign: Some(Sign::Minus), + alternate: Some(Alternate), + zero_padding: Some(ZeroPadding), + width: Some(Count::Parameter(Argument::Identifier("Минск"))), + precision: None, + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:.*}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: Some(Precision::Star), - ty: Type::Display, - }), - }], - }), + format_string_formats("{:.*}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: Some(Precision::Star), + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:.0}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: Some(Precision::Count(Count::Integer(0))), - ty: Type::Display, - }), - }], - }), + format_string_formats("{:.0}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: Some(Precision::Count(Count::Integer(0))), + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:.0$}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: Some(Precision::Count(Count::Parameter( - Argument::Integer(0), - ))), - ty: Type::Display, - }), - }], - }), + format_string_formats("{:.0$}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: Some(Precision::Count(Count::Parameter( + Argument::Integer(0), + ))), + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:.par$}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: Some(Precision::Count(Count::Parameter( - Argument::Identifier("par"), - ))), - ty: Type::Display, - }), - }], - }), + format_string_formats("{:.par$}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: Some(Precision::Count(Count::Parameter( + Argument::Identifier("par"), + ))), + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{: >+#2$.par$}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: Some((Some(' '), Align::Right)), - sign: Some(Sign::Plus), - alternate: Some(Alternate), - zero_padding: None, - width: Some(Count::Parameter(Argument::Integer(2))), - precision: Some(Precision::Count(Count::Parameter( - Argument::Identifier("par"), - ))), - ty: Type::Display, - }), - }], - }), + format_string_formats("{: >+#2$.par$}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some(' '), Align::Right)), + sign: Some(Sign::Plus), + alternate: Some(Alternate), + zero_padding: None, + width: Some(Count::Parameter(Argument::Integer(2))), + precision: Some(Precision::Count(Count::Parameter( + Argument::Identifier("par"), + ))), + ty: Type::Display, + }), + }],), ); assert_eq!( - format_string("{:x?}"), - Some(FormatString { - formats: vec![Format { - arg: None, - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: None, - zero_padding: None, - width: None, - precision: None, - ty: Type::LowerDebug, - }), - }], - }), + format_string_formats("{:x?}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: None, + ty: Type::LowerDebug, + }), + }],), ); assert_eq!( - format_string("{:E}"), - Some(FormatString { - formats: vec![Format { - arg: None, + format_string_formats("{:E}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: None, + zero_padding: None, + width: None, + precision: None, + ty: Type::UpperExp, + }), + }],), + ); + assert_eq!( + format_string_formats("{: >+#par$.par$X?}"), + Some(vec![Format { + arg: None, + spec: Some(FormatSpec { + align: Some((Some(' '), Align::Right)), + sign: Some(Sign::Plus), + alternate: Some(Alternate), + zero_padding: None, + width: Some(Count::Parameter(Argument::Identifier("par"))), + precision: Some(Precision::Count(Count::Parameter( + Argument::Identifier("par"), + ))), + ty: Type::UpperDebug, + }), + }],), + ); + } + + #[test] + fn full_format() { + assert_eq!( + format_string_formats("prefix{{{0:#?}postfix{par:-^par$.a$}}}"), + Some(vec![ + Format { + arg: Some(Argument::Integer(0)), spec: Some(FormatSpec { align: None, sign: None, - alternate: None, + alternate: Some(Alternate), zero_padding: None, width: None, precision: None, - ty: Type::UpperExp, + ty: Type::Debug, }), - }], - }), - ); - assert_eq!( - format_string("{: >+#par$.par$X?}"), - Some(FormatString { - formats: vec![Format { - arg: None, + }, + Format { + arg: Some(Argument::Identifier("par")), spec: Some(FormatSpec { - align: Some((Some(' '), Align::Right)), - sign: Some(Sign::Plus), - alternate: Some(Alternate), + align: Some((Some('-'), Align::Center)), + sign: None, + alternate: None, zero_padding: None, width: Some(Count::Parameter(Argument::Identifier("par"))), precision: Some(Precision::Count(Count::Parameter( - Argument::Identifier("par"), + Argument::Identifier("a"), ))), - ty: Type::UpperDebug, + ty: Type::Display, }), - }], - }), + }, + ],), ); } #[test] - fn full() { + fn full_parts() { assert_eq!( format_string("prefix{{{0:#?}postfix{par:-^par$.a$}}}"), Some(FormatString { - formats: vec![ - Format { - arg: Some(Argument::Integer(0)), - spec: Some(FormatSpec { - align: None, - sign: None, - alternate: Some(Alternate), - zero_padding: None, - width: None, - precision: None, - ty: Type::Debug, - }), + elements: vec![ + MaybeFormat::Text("prefix"), + MaybeFormat::Text("{{"), + MaybeFormat::Format { + raw: "{0:#?}", + format: Format { + arg: Some(Argument::Integer(0)), + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: Some(Alternate), + zero_padding: None, + width: None, + precision: None, + ty: Type::Debug, + }), + } }, - Format { - arg: Some(Argument::Identifier("par")), - spec: Some(FormatSpec { - align: Some((Some('-'), Align::Center)), - sign: None, - alternate: None, - zero_padding: None, - width: Some(Count::Parameter(Argument::Identifier("par"))), - precision: Some(Precision::Count(Count::Parameter( - Argument::Identifier("a"), - ))), - ty: Type::Display, - }), + MaybeFormat::Text("postfix"), + MaybeFormat::Format { + raw: "{par:-^par$.a$}", + format: Format { + arg: Some(Argument::Identifier("par")), + spec: Some(FormatSpec { + align: Some((Some('-'), Align::Center)), + sign: None, + alternate: None, + zero_padding: None, + width: Some(Count::Parameter(Argument::Identifier( + "par" + ))), + precision: Some(Precision::Count(Count::Parameter( + Argument::Identifier("a"), + ))), + ty: Type::Display, + }), + } }, - ], + MaybeFormat::Text("}}"), + ] }), ); } #[test] fn error() { - assert_eq!(format_string("{"), None); - assert_eq!(format_string("}"), None); - assert_eq!(format_string("{{}"), None); - assert_eq!(format_string("{:x?"), None); - assert_eq!(format_string("{:.}"), None); - assert_eq!(format_string("{:q}"), None); - assert_eq!(format_string("{:par}"), None); - assert_eq!(format_string("{⚙️}"), None); + assert_eq!(format_string_formats("{"), None); + assert_eq!(format_string_formats("}"), None); + assert_eq!(format_string_formats("{{}"), None); + assert_eq!(format_string_formats("{:x?"), None); + assert_eq!(format_string_formats("{:.}"), None); + assert_eq!(format_string_formats("{:q}"), None); + assert_eq!(format_string_formats("{:par}"), None); + assert_eq!(format_string_formats("{⚙️}"), None); } } diff --git a/tests/compile_fail/display/shared_format_positional_placeholders.rs b/tests/compile_fail/display/shared_format_positional_placeholders.rs new file mode 100644 index 00000000..8d2a0ff2 --- /dev/null +++ b/tests/compile_fail/display/shared_format_positional_placeholders.rs @@ -0,0 +1,19 @@ +#[derive(derive_more::Display)] +#[display("Stuff({})")] +enum Foo { + A, +} + +#[derive(derive_more::Display)] +#[display("Stuff({0})")] +enum Foo2 { + A, +} + +#[derive(derive_more::Display)] +#[display("Stuff()", _0, _2)] +enum Foo3 { + A, +} + +fn main() {} diff --git a/tests/compile_fail/display/shared_format_positional_placeholders.stderr b/tests/compile_fail/display/shared_format_positional_placeholders.stderr new file mode 100644 index 00000000..d9f2d523 --- /dev/null +++ b/tests/compile_fail/display/shared_format_positional_placeholders.stderr @@ -0,0 +1,17 @@ +error: shared format string cannot contain positional placeholders, use named placeholders instead + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:2:11 + | +2 | #[display("Stuff({})")] + | ^^^^^^^^^^^ + +error: shared format string cannot contain positional placeholders, use named placeholders instead + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:8:11 + | +8 | #[display("Stuff({0})")] + | ^^^^^^^^^^^^ + +error: shared format string does not support positional placeholders, use named placeholders instead + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:14:22 + | +14 | #[display("Stuff()", _0, _2)] + | ^^ diff --git a/tests/compile_fail/display/shared_format_unclosed_brace.rs b/tests/compile_fail/display/shared_format_unclosed_brace.rs new file mode 100644 index 00000000..47134e25 --- /dev/null +++ b/tests/compile_fail/display/shared_format_unclosed_brace.rs @@ -0,0 +1,7 @@ +#[derive(derive_more::Display)] +#[display("Stuff({)")] +enum Foo { + A, +} + +fn main() {} diff --git a/tests/compile_fail/display/shared_format_unclosed_brace.stderr b/tests/compile_fail/display/shared_format_unclosed_brace.stderr new file mode 100644 index 00000000..4d4861f3 --- /dev/null +++ b/tests/compile_fail/display/shared_format_unclosed_brace.stderr @@ -0,0 +1,9 @@ +error: invalid format string: expected `'}'`, found `')'` + --> tests/compile_fail/display/shared_format_unclosed_brace.rs:2:19 + | +2 | #[display("Stuff({)")] + | -^ expected `'}'` in format string + | | + | because of this opening brace + | + = note: if you intended to print `{`, you can escape it using `{{` diff --git a/tests/compile_fail/display/shared_format_variant_spec.rs b/tests/compile_fail/display/shared_format_variant_spec.rs new file mode 100644 index 00000000..9ad647ad --- /dev/null +++ b/tests/compile_fail/display/shared_format_variant_spec.rs @@ -0,0 +1,7 @@ +#[derive(derive_more::Display)] +#[display("Stuff({_variant:?})")] +enum Foo { + A, +} + +fn main() {} diff --git a/tests/compile_fail/display/shared_format_variant_spec.stderr b/tests/compile_fail/display/shared_format_variant_spec.stderr new file mode 100644 index 00000000..d95676fc --- /dev/null +++ b/tests/compile_fail/display/shared_format_variant_spec.stderr @@ -0,0 +1,5 @@ +error: shared format _variant placeholder cannot contain format specifiers + --> tests/compile_fail/display/shared_format_variant_spec.rs:2:11 + | +2 | #[display("Stuff({_variant:?})")] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/display.rs b/tests/display.rs index 1e7ddfc1..ebc0012e 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -1283,6 +1283,131 @@ mod enums { } } } + + mod shared_format { + use super::*; + mod single { + use super::*; + + #[derive(Display)] + #[display("Variant: {_variant}")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + } + + #[test] + fn assert() { + assert_eq!(Enum::A(1).to_string(), "Variant: A 1"); + assert_eq!(Enum::B { field: 2 }.to_string(), "Variant: B 2",); + assert_eq!(Enum::C.to_string(), "Variant: C",); + } + } + + mod multiple { + use super::*; + + #[derive(Display)] + #[display("{_variant} Variant: {_variant} {_variant}")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + } + + #[test] + fn assert() { + assert_eq!(Enum::A(1).to_string(), "A 1 Variant: A 1 A 1"); + assert_eq!( + Enum::B { field: 2 }.to_string(), + "B 2 Variant: B 2 B 2", + ); + assert_eq!(Enum::C.to_string(), "C Variant: C C",); + } + } + + mod none { + use super::*; + + /// Make sure that variant specific bounds are not added if _variant is + /// not used. + struct NoDisplay; + + #[derive(Display)] + #[display("Variant")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + D(T), + } + + #[test] + fn assert() { + assert_eq!(Enum::::A(1).to_string(), "Variant"); + assert_eq!( + Enum::::B { field: 2 }.to_string(), + "Variant", + ); + assert_eq!(Enum::::C.to_string(), "Variant",); + assert_eq!(Enum::::D(NoDisplay).to_string(), "Variant",); + } + } + + mod use_field { + use super::*; + + #[derive(Display)] + #[display("Variant {_0}")] + enum Enum { + A(i32), + B(&'static str), + C(T), + } + + #[test] + fn assert() { + assert_eq!(Enum::::A(1).to_string(), "Variant 1"); + assert_eq!(Enum::::B("abc").to_string(), "Variant abc",); + assert_eq!(Enum::::C(9).to_string(), "Variant 9",); + } + } + + mod use_field_and_variant { + use super::*; + + #[derive(Display)] + #[display("Variant {_variant} {_0}")] + enum Enum { + #[display("A")] + A(i32), + #[display("B")] + B(&'static str), + #[display("C")] + C(T), + } + + #[test] + fn assert() { + assert_eq!(Enum::::A(1).to_string(), "Variant A 1"); + assert_eq!(Enum::::B("abc").to_string(), "Variant B abc",); + assert_eq!(Enum::::C(9).to_string(), "Variant C 9",); + } + } + } } }