Skip to content
41 changes: 32 additions & 9 deletions impl/src/types/fmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use std::fmt::{self, Debug, Formatter};

use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{ToTokens, quote};
use syn::{Attribute, Data, Fields, Ident, LitStr, spanned::Spanned as _};
use syn::{
Attribute, Data, Fields, Ident, LitStr, Type, spanned::Spanned as _,
};

mod input;
use input::{StructFormatInput, VariantFormatInput};
Expand All @@ -21,8 +23,7 @@ pub(crate) enum TypeData {
variant_display_inputs: Vec<VariantData>,
},

// TODO: also use for structs with never (!) field
EmptyEnum,
EmptyType,
}

impl TypeData {
Expand All @@ -34,8 +35,18 @@ impl TypeData {
let default_display_attr = super::util::take_display_attr(attrs);

match input_data {
Data::Struct(_) => {
drop(input_data);
Data::Struct(data) => {
let has_never_type = data
.fields
.iter()
.any(|field| matches!(field.ty, Type::Never(_)));

drop(data);

if has_never_type {
drop(default_display_attr);
return Ok(Self::EmptyType);
}

let display_attr = default_display_attr
.ok_or_else(|| syn::Error::new(ident_span, "missing `display` attribute for struct with `#[derive(Error)]`"))?;
Expand All @@ -48,7 +59,7 @@ impl TypeData {
let variants = data.variants;
if variants.is_empty() {
drop(variants);
return Ok(Self::EmptyEnum);
return Ok(Self::EmptyType);
}

let variant_display_inputs =
Expand Down Expand Up @@ -159,10 +170,9 @@ impl ToTokens for TypeData {
});
}

Self::EmptyEnum => {
// TODO: fully qualify macro path
Self::EmptyType => {
tokens.extend(quote! {
unreachable!("attempted to format an empty enum")
::core::unreachable!("attempted to format an empty type")
});
}
}
Expand Down Expand Up @@ -239,6 +249,19 @@ mod tests {
use quote::quote;
use syn::DeriveInput;

#[test]
fn empty_struct_works_without_display_attr() {
let derive_input: ErrorStackDeriveInput =
syn::parse2(quote! { struct EmptyStructType(!); })
.expect("malformed test stream");

let output = quote! { #derive_input };
assert_eq!(
output.to_string(),
"# [allow (single_use_lifetimes)] impl :: core :: fmt :: Display for EmptyStructType { fn fmt (& self , f : & mut :: core :: fmt :: Formatter < '_ >) -> :: core :: fmt :: Result { :: core :: unreachable ! (\"attempted to format an empty type\") } } # [allow (single_use_lifetimes)] impl :: core :: error :: Error for EmptyStructType { }"
);
}

#[test]
fn struct_data_requires_display_attr() {
let mut derive_input: DeriveInput =
Expand Down
7 changes: 3 additions & 4 deletions impl/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,9 @@ impl ToTokens for ErrorStackDeriveInput {
.map(util::generic_reduced_to_ident)
.collect();

// TODO: move `other_attrs` down to avoid overwriting users' attrs
tokens.extend(quote! {
#(#other_attrs)*
#[allow(single_use_lifetimes)]
#(#other_attrs)*
impl #generics ::core::fmt::Display for #ident #type_generics
#where_clause
{
Expand All @@ -85,8 +84,8 @@ impl ToTokens for ErrorStackDeriveInput {
}
}

#(#other_attrs)*
#[allow(single_use_lifetimes)]
#(#other_attrs)*
impl #error_trait_generics ::core::error::Error for #ident #type_generics
#where_clause
{
Expand Down Expand Up @@ -118,7 +117,7 @@ mod tests {
let output = quote! { #input };
assert_eq!(
output.to_string(),
"# [test_attribute] # [test_attribute_2] # [allow (single_use_lifetimes)] impl :: core :: fmt :: Display for CustomType { fn fmt (& self , f : & mut :: core :: fmt :: Formatter < '_ >) -> :: core :: fmt :: Result { :: core :: write ! (f , \"custom type\" ,) } } # [test_attribute] # [test_attribute_2] # [allow (single_use_lifetimes)] impl :: core :: error :: Error for CustomType { }"
"# [allow (single_use_lifetimes)] # [test_attribute] # [test_attribute_2] impl :: core :: fmt :: Display for CustomType { fn fmt (& self , f : & mut :: core :: fmt :: Formatter < '_ >) -> :: core :: fmt :: Result { :: core :: write ! (f , \"custom type\" ,) } } # [allow (single_use_lifetimes)] # [test_attribute] # [test_attribute_2] impl :: core :: error :: Error for CustomType { }"
);
}

Expand Down
29 changes: 14 additions & 15 deletions tests/src/enum_variants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,25 @@ mod tests {
#[derive(Debug, Error)]
enum EnumType {
#[display("unit variant")]
UnitVariant,
Unit,
}

assert_eq!(EnumType::UnitVariant.to_string(), "unit variant");
assert_eq!(EnumType::Unit.to_string(), "unit variant");
}

#[test]
fn named_field_variant_works_without_interpolation() {
#[derive(Debug, Error)]
enum EnumType {
// TODO: rename all variants in all `EnumType`s
#[display("named field variant")]
NamedFieldVariant {
NamedFields {
_length: usize,
_is_ascii: bool,
_inner: String,
},
}

let test_val = EnumType::NamedFieldVariant {
let test_val = EnumType::NamedFields {
_length: 5,
_is_ascii: true,
_inner: String::from("hello"),
Expand All @@ -39,14 +38,14 @@ mod tests {
#[derive(Debug, Error)]
enum EnumType {
#[display("named field variant: {inner:?} has {length} characters")]
NamedFieldVariant {
NamedFields {
length: usize,
_is_ascii: bool,
inner: String,
},
}

let test_val = EnumType::NamedFieldVariant {
let test_val = EnumType::NamedFields {
length: 5,
_is_ascii: true,
inner: String::from("hello"),
Expand All @@ -64,14 +63,14 @@ mod tests {
#[display(
"named field variant: {inner:?} has {length} characters and is ascii={is_ascii}"
)]
NamedFieldVariant {
NamedFields {
length: usize,
is_ascii: bool,
inner: String,
},
}

let test_val = EnumType::NamedFieldVariant {
let test_val = EnumType::NamedFields {
length: 5,
is_ascii: true,
inner: String::from("hello"),
Expand All @@ -87,10 +86,10 @@ mod tests {
#[derive(Debug, Error)]
enum EnumType {
#[display("tuple variant")]
TupleVariant(isize, isize, isize),
Tuple(isize, isize, isize),
}

let test_val = EnumType::TupleVariant(5, 10, 15);
let test_val = EnumType::Tuple(5, 10, 15);
assert_eq!(test_val.to_string(), "tuple variant");
}

Expand All @@ -99,10 +98,10 @@ mod tests {
#[derive(Debug, Error)]
enum EnumType {
#[display("tuple variant: point with y value {1}")]
TupleVariant(isize, isize, isize),
Tuple(isize, isize, isize),
}

let test_val = EnumType::TupleVariant(5, 10, 15);
let test_val = EnumType::Tuple(5, 10, 15);
assert_eq!(
test_val.to_string(),
"tuple variant: point with y value 10"
Expand All @@ -116,10 +115,10 @@ mod tests {
#[display(
"tuple variant: point {2} units in front of the origin, and with x and y coords ({0}, {1})"
)]
TupleVariant(isize, isize, isize),
Tuple(isize, isize, isize),
}

let test_val = EnumType::TupleVariant(5, 10, 15);
let test_val = EnumType::Tuple(5, 10, 15);
assert_eq!(
test_val.to_string(),
"tuple variant: point 15 units in front of the origin, and with x and y coords (5, 10)"
Expand Down
2 changes: 1 addition & 1 deletion tests/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ mod tests {
#[test]
#[expect(
dead_code,
reason = "this test requires `EnumType::TupleVariant`'s fields to exist, even though they won't be read"
reason = "this test requires `EnumType::Tuple`'s fields to exist, even though they won't be read"
)]
fn enum_works_with_display_attr_default() {
#[derive(Debug, Error)]
Expand Down
2 changes: 1 addition & 1 deletion tests/src/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ mod tests {
assert_eq!(test_val.to_string(), "inner = 8");
}

// FIXME: #[expect(redundant_lifetimes)] on type fails to remove warning
// TODO: move #[expect(redundant_lifetimes)] to type when fixed
#[test]
#[expect(
redundant_lifetimes,
Expand Down