Skip to content

Commit

Permalink
Support _variant in outer level enum formatting for Display
Browse files Browse the repository at this point in the history
  • Loading branch information
JelteF committed Jul 3, 2024
1 parent 79e7fec commit b2aff5b
Show file tree
Hide file tree
Showing 16 changed files with 427 additions and 34 deletions.
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"

# 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
148 changes: 125 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,79 @@ 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_parts = parsing::format_parts(&fmt_string);
let Some(parts) = maybe_parts else {
// If we could not parse the format string, we just use the original string so
// we get a nice error message. We also assert false so we get a compile
// error.
return Ok(quote! {
derive_more::core::write!(__derive_more_f, #shared_format);
panic!("derive_more could not parse shared format string, but rust could: {:?}", #fmt_string);
});
};
for part in parts {
match part {
parsing::FormatPart::Text(s) => {
current_format.push_str(s);
}
parsing::FormatPart::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 placehold 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 +341,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(&shared_format.lit.value())
.into_iter()
.flat_map(|f| f.formats)
.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
Loading

0 comments on commit b2aff5b

Please sign in to comment.