Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support _variant in outer level enum formatting for Display #377

Merged
merged 15 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
strategy:
fail-fast: false
matrix:
msrv: ["1.65.0"]
msrv: ["1.70.0"]
os:
- ubuntu
- macOS
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <github-tech@jeltef.nl>"]
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down
2 changes: 1 addition & 1 deletion clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
JelteF marked this conversation as resolved.
Show resolved Hide resolved

# Ensures consistent bracing for macro calls in the codebase.
# Extends default settings:
Expand Down
2 changes: 1 addition & 1 deletion impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <github-tech@jeltef.nl>"]
license = "MIT"
Expand Down
22 changes: 19 additions & 3 deletions impl/doc/display.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -175,6 +181,7 @@ struct Point2D {
}

#[derive(Display)]
#[display("Enum E: {_variant}")]
enum E {
Uint(u32),
#[display("I am B {:b}", i)]
Expand All @@ -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 {
Expand Down Expand Up @@ -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");
Expand Down
150 changes: 127 additions & 23 deletions impl/src/fmt/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -80,6 +80,7 @@ fn expand_struct(
(attrs, ident, trait_ident, _): ExpansionCtx<'_>,
) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
let s = Expansion {
shared_format: None,
attrs,
fields: &s.fields,
trait_ident,
Expand Down Expand Up @@ -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<syn::WherePredicate>, 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| {
Expand All @@ -138,6 +135,7 @@ fn expand_enum(
}

let v = Expansion {
shared_format: shared_attrs.fmt.as_ref(),
attrs: &attrs,
fields: &variant.fields,
trait_ident,
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -226,6 +227,81 @@ impl<'a> Expansion<'a> {
/// [`Display::fmt()`]: fmt::Display::fmt()
/// [`FmtAttribute`]: super::FmtAttribute
fn generate_body(&self) -> syn::Result<TokenStream> {
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<TokenStream> {
match &self.attrs.fmt {
Some(fmt) => {
Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() {
Expand Down Expand Up @@ -267,27 +343,55 @@ impl<'a> Expansion<'a> {

/// Generates trait bounds for a struct or an enum variant.
fn generate_bounds(&self) -> Vec<syn::WherePredicate> {
let mut bounds: Vec<syn::WherePredicate> =
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
}
}

Expand Down
4 changes: 2 additions & 2 deletions impl/src/fmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,9 @@ impl Placeholder {
/// Parses [`Placeholder`]s from the provided formatting string.
fn parse_fmt_string(s: &str) -> Vec<Self> {
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,
Expand Down
Loading