diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index abf57e7..a57307c 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -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}; @@ -21,8 +23,7 @@ pub(crate) enum TypeData { variant_display_inputs: Vec, }, - // TODO: also use for structs with never (!) field - EmptyEnum, + EmptyType, } impl TypeData { @@ -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)]`"))?; @@ -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 = @@ -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") }); } } @@ -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 = diff --git a/impl/src/types/mod.rs b/impl/src/types/mod.rs index 07e7b99..172dec0 100644 --- a/impl/src/types/mod.rs +++ b/impl/src/types/mod.rs @@ -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 { @@ -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 { @@ -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 { }" ); } diff --git a/tests/src/enum_variants.rs b/tests/src/enum_variants.rs index dac46ec..e74253a 100644 --- a/tests/src/enum_variants.rs +++ b/tests/src/enum_variants.rs @@ -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"), @@ -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"), @@ -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"), @@ -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"); } @@ -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" @@ -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)" diff --git a/tests/src/enums.rs b/tests/src/enums.rs index 32dc1a2..ef6d216 100644 --- a/tests/src/enums.rs +++ b/tests/src/enums.rs @@ -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)] diff --git a/tests/src/structs.rs b/tests/src/structs.rs index 9f45926..3fc2f9f 100644 --- a/tests/src/structs.rs +++ b/tests/src/structs.rs @@ -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,