Skip to content

Commit 00defd8

Browse files
committed
refactor: replace #[display] with #[error]
1 parent ce4cfe0 commit 00defd8

File tree

5 files changed

+87
-89
lines changed

5 files changed

+87
-89
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ use n0_error::{e, add_meta, Error, Result, StackResultExt, StdResultExt};
1515
#[error(from_sources)]
1616
enum MyError {
1717
// A custom validation error
18-
#[display("bad input: {count}")]
18+
#[error("bad input: {count}")]
1919
BadInput { count: usize },
2020
// Wrap a std::io::Error as a source (std error)
21-
#[display("IO error")]
21+
#[error("IO error")]
2222
Io {
2323
#[error(std_err)]
2424
source: std::io::Error,
@@ -70,13 +70,13 @@ fn main() -> Result<()> {
7070

7171
#[add_meta]
7272
#[derive(Error)]
73-
#[display("tuple fail ({_0})")]
73+
#[error("tuple fail ({_0})")]
7474
struct TupleStruct(u32);
7575

7676
#[add_meta]
7777
#[derive(Error)]
7878
enum TupleEnum {
79-
#[display("io failed")]
79+
#[error("io failed")]
8080
Io(#[error(source, std_err)] std::io::Error),
8181
}
8282

examples/simple.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ pub mod error {
8989
#[error(std_err)]
9090
source: io::Error,
9191
},
92-
#[display("Bad request - missing characters: {missing} {}", missing * 2)]
92+
#[error("Bad request - missing characters: {missing} {}", missing * 2)]
9393
BadRequest {
9494
missing: usize,
9595
},

n0-error-macros/src/lib.rs

Lines changed: 70 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,13 @@ fn add_meta_field(fields: &mut Fields) -> Result<(), syn::Error> {
7575
/// - `#[error(from_sources)]`: Creates `From` impls for the `source` types of all variants. Will fail to compile if multiple sources have the same type.
7676
/// - `#[error(std_sources)]`: Defaults all sources to be std errors instead of stack errors.
7777
/// - on enum variants and structs:
78-
/// - `#[display("..")]`: Sets the display formatting. You can refer to named fields by their names, and to tuple fields by `_0`, `_1` etc.
78+
/// - `#[error("format {field}: {}", a + b)]`: Sets the display formatting. You can refer to named fields by their names, and to tuple fields by `_0`, `_1` etc.
7979
/// - `#[error(transparent)]`: Directly forwards the display implementation to the error source, and omits the outer error in the source chain when reporting errors.
8080
/// - on fields:
8181
/// - `#[error(source)]`: Sets a field as the source of this error. If a field is named `source` this is applied implicitly and not needed.
8282
/// - `#[error(from)]`: Creates a `From` impl for the field's type to the error type.
8383
/// - `#[error(std_err)]`: Marks the error as a `std` error. Without this attribute, errors are expected to implement `StackError`. Only applicable to source fields.
84-
#[proc_macro_derive(Error, attributes(display, error))]
84+
#[proc_macro_derive(Error, attributes(error))]
8585
pub fn derive_error(input: TokenStream) -> TokenStream {
8686
let input = parse_macro_input!(input as syn::DeriveInput);
8787
match derive_error_inner(input) {
@@ -91,9 +91,9 @@ pub fn derive_error(input: TokenStream) -> TokenStream {
9191
}
9292

9393
fn derive_error_inner(input: DeriveInput) -> Result<proc_macro2::TokenStream, darling::Error> {
94-
let top = TopAttrs::from_attributes(&input.attrs)?;
9594
match &input.data {
9695
syn::Data::Enum(item_enum) => {
96+
let top = TopAttrs::from_attributes(&input.attrs)?;
9797
let infos = item_enum
9898
.variants
9999
.iter()
@@ -102,6 +102,7 @@ fn derive_error_inner(input: DeriveInput) -> Result<proc_macro2::TokenStream, da
102102
Ok(generate_enum_impls(&input.ident, &input.generics, infos))
103103
}
104104
syn::Data::Struct(item) => {
105+
let top = TopAttrs::default();
105106
let info = VariantInfo::parse(&input.ident, &item.fields, &input.attrs, &top)?;
106107
Ok(generate_struct_impl(&input.ident, &input.generics, info))
107108
}
@@ -111,7 +112,6 @@ fn derive_error_inner(input: DeriveInput) -> Result<proc_macro2::TokenStream, da
111112

112113
struct SourceField<'a> {
113114
kind: SourceKind,
114-
transparent: bool,
115115
field: FieldInfo<'a>,
116116
}
117117

@@ -154,18 +154,12 @@ struct FieldAttrs {
154154
meta: bool,
155155
}
156156

157-
#[derive(Default, Clone, Copy, FromAttributes)]
158-
#[darling(default, attributes(error))]
159-
struct VariantAttrs {
160-
transparent: bool,
161-
}
162-
163-
// For each variant, capture doc comment text or #[display] attr
157+
// For each variant, capture doc comment text or #[error] attr
164158
struct VariantInfo<'a> {
165159
ident: Ident,
166160
fields: Vec<FieldInfo<'a>>,
167161
kind: Kind,
168-
display: Option<proc_macro2::TokenStream>,
162+
display: Display,
169163
source: Option<SourceField<'a>>,
170164
/// The field that is used for From<..> impls
171165
from: Option<FieldInfo<'a>>,
@@ -231,8 +225,10 @@ impl<'a> FieldIdent<'a> {
231225

232226
impl<'a> VariantInfo<'a> {
233227
fn transparent(&self) -> Option<&FieldInfo<'_>> {
234-
let source = self.source.as_ref()?;
235-
source.transparent.then_some(&source.field)
228+
match self.display {
229+
Display::Transparent => self.source.as_ref().map(|s| &s.field),
230+
_ => None,
231+
}
236232
}
237233

238234
fn field_binding_idents(&self) -> impl Iterator<Item = Ident> + '_ {
@@ -263,15 +259,7 @@ impl<'a> VariantInfo<'a> {
263259
attrs: &[Attribute],
264260
top: &TopAttrs,
265261
) -> Result<VariantInfo<'a>, syn::Error> {
266-
let variant_attrs = VariantAttrs::from_attributes(attrs)?;
267-
let display = get_doc_or_display(&attrs)?;
268-
// TODO: enable this but only for #[display] not for doc comments
269-
// if display.is_some() && variant_attrs.transparent {
270-
// return Err(err(
271-
// ident,
272-
// "#[display] and #[error(transparent)] are mutually exclusive",
273-
// ));
274-
// }
262+
let display = get_display(&attrs)?;
275263
let (kind, fields): (Kind, Vec<FieldInfo>) = match fields {
276264
Fields::Named(ref fields) => (
277265
Kind::Named,
@@ -308,15 +296,15 @@ impl<'a> VariantInfo<'a> {
308296
_ => false,
309297
}),
310298
Kind::Tuple => {
311-
if variant_attrs.transparent {
299+
if display.is_transparent() {
312300
fields.first()
313301
} else {
314302
None
315303
}
316304
}
317305
});
318306

319-
if variant_attrs.transparent && source_field.is_none() {
307+
if display.is_transparent() && source_field.is_none() {
320308
return Err(err(
321309
ident,
322310
"Variants with #[error(transparent)] require a source field",
@@ -333,11 +321,7 @@ impl<'a> VariantInfo<'a> {
333321
SourceKind::Stack
334322
};
335323
let field = (*field).clone();
336-
Some(SourceField {
337-
kind,
338-
transparent: variant_attrs.transparent,
339-
field,
340-
})
324+
Some(SourceField { kind, field })
341325
}
342326
};
343327

@@ -478,12 +462,12 @@ fn generate_enum_impls(
478462
});
479463

480464
let match_fmt_message_arms = variants.iter().map(|vi| match &vi.display {
481-
Some(expr) => {
465+
Display::Format(expr) => {
482466
let binds: Vec<Ident> = vi.field_binding_idents().collect();
483467
let pat = vi.spread_all(&binds);
484468
quote! { #pat => { #expr } }
485469
}
486-
None => {
470+
Display::Default | Display::Transparent => {
487471
let text = format!("{}::{}", enum_ident, vi.ident);
488472
let pat = vi.spread_empty();
489473
quote! { #pat => write!(f, #text) }
@@ -705,7 +689,7 @@ fn generate_struct_impl(
705689
quote! { write!(f, "{}", #expr) }
706690
} else {
707691
match &info.display {
708-
Some(expr) => {
692+
Display::Format(expr) => {
709693
let binds: Vec<Ident> = info.field_binding_idents().collect();
710694
match info.kind {
711695
Kind::Named => {
@@ -716,7 +700,7 @@ fn generate_struct_impl(
716700
}
717701
}
718702
}
719-
None => {
703+
Display::Default | Display::Transparent => {
720704
// Fallback to struct name
721705
let text = info.ident.to_string();
722706
quote! { write!(f, #text) }
@@ -868,47 +852,61 @@ fn generate_struct_impl(
868852
}
869853
}
870854

871-
fn get_doc_or_display(attrs: &[Attribute]) -> Result<Option<proc_macro2::TokenStream>, syn::Error> {
872-
// Prefer #[display("...")]
873-
if let Some(attr) = attrs.iter().find(|a| a.path().is_ident("display")) {
874-
// Accept format!-style args: #[display("text {}", arg1, arg2, ...)]
875-
let args = attr.parse_args_with(Punctuated::<Expr, syn::Token![,]>::parse_terminated)?;
876-
if args.is_empty() {
877-
Err(err(
878-
attr,
879-
"#[display(..)] requires at least a format string",
880-
))
881-
} else {
882-
let mut it = args.into_iter();
883-
let fmt = it.next().unwrap();
884-
let rest: Vec<_> = it.collect();
885-
Ok(Some(quote! { write!(f, #fmt #(, #rest)* ) }))
886-
}
887-
} else {
888-
// Otherwise collect doc lines: #[doc = "..."]
889-
let docs: Vec<String> = attrs
890-
.iter()
891-
.filter(|a| a.path().is_ident("doc"))
892-
.filter_map(|attr| {
893-
let s = attr.meta.require_name_value().ok()?;
894-
match &s.value {
895-
syn::Expr::Lit(syn::ExprLit {
896-
lit: syn::Lit::Str(s),
897-
..
898-
}) => Some(s.value().trim().to_string()),
899-
_ => None,
900-
}
901-
})
902-
.collect();
903-
if docs.is_empty() {
904-
Ok(None)
905-
} else {
906-
let doc = docs.join("\n");
907-
Ok(Some(quote! { write!(f, #doc) }))
908-
}
855+
enum Display {
856+
Default,
857+
Transparent,
858+
Format(proc_macro2::TokenStream),
859+
}
860+
861+
impl Display {
862+
fn is_transparent(&self) -> bool {
863+
matches!(self, Self::Transparent)
909864
}
910865
}
911866

867+
fn get_display(attrs: &[Attribute]) -> Result<Display, syn::Error> {
868+
// Only consider #[error(...)]
869+
let Some(attr) = attrs.iter().find(|a| a.path().is_ident("error")) else {
870+
return Ok(Display::Default);
871+
};
872+
873+
// syn 2: parse args inside the attribute's parentheses
874+
let args: Punctuated<Expr, syn::Token![,]> =
875+
attr.parse_args_with(Punctuated::<Expr, syn::Token![,]>::parse_terminated)?;
876+
877+
if args.is_empty() {
878+
return Err(err(
879+
attr,
880+
"#[error(..)] requires arguments: a format string or `transparent`",
881+
));
882+
}
883+
884+
// #[error(transparent)]
885+
if args.len() == 1 {
886+
if let Expr::Path(p) = &args[0] {
887+
if p.path.is_ident("transparent") {
888+
return Ok(Display::Transparent);
889+
}
890+
}
891+
}
892+
893+
// #[error("...", args...)]
894+
let mut it = args.into_iter();
895+
let first = it.next().unwrap();
896+
897+
let fmt_lit = match first {
898+
Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => s,
899+
other => {
900+
return Err(err(
901+
other,
902+
"first argument to #[error(\"...\")] must be a string literal, or use #[error(transparent)]",
903+
))
904+
}
905+
};
906+
907+
let rest: Vec<Expr> = it.collect();
908+
Ok(Display::Format(quote! { write!(f, #fmt_lit #(, #rest)* ) }))
909+
}
912910
fn err(ident: impl ToTokens, err: impl ToString) -> syn::Error {
913911
syn::Error::new_spanned(ident, err.to_string())
914912
}

src/ext.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,15 @@ impl<T> StackResultExt<T, NoneError> for Option<T> {
149149
/// Error returned when converting [`Option`]s to an error.
150150
#[add_meta]
151151
#[derive(crate::Error)]
152-
#[display("Expected some, found none")]
152+
#[error("Expected some, found none")]
153153
pub struct NoneError {}
154154

155155
/// A simple string error, providing a message and optionally a source.
156156
#[add_meta]
157157
#[derive(crate::Error)]
158158
pub(crate) enum FromString {
159-
#[display("{message}")]
159+
#[error("{message}")]
160160
WithSource { message: String, source: AnyError },
161-
#[display("{message}")]
161+
#[error("{message}")]
162162
WithoutSource { message: String },
163163
}

src/tests.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ fn test_anyhow_compat() -> Result {
1919
#[add_meta]
2020
#[derive(Error)]
2121
enum MyError {
22-
#[display("A failure")]
22+
#[error("A failure")]
2323
A {},
2424
}
2525
#[test]
@@ -232,7 +232,7 @@ fn test_ensure() {
232232
struct SomeError;
233233

234234
#[derive(n0_error::Error)]
235-
#[display("fail ({code})")]
235+
#[error("fail ({code})")]
236236
struct SomeErrorFields {
237237
code: u32,
238238
}
@@ -269,7 +269,7 @@ struct SomeErrorLoc;
269269

270270
#[add_meta]
271271
#[derive(n0_error::Error)]
272-
#[display("fail ({code})")]
272+
#[error("fail ({code})")]
273273
struct SomeErrorLocFields {
274274
code: u32,
275275
}
@@ -363,7 +363,7 @@ fn test_any() {
363363

364364
#[add_meta]
365365
#[derive(n0_error::Error)]
366-
#[display("tuple fail ({_0})")]
366+
#[error("tuple fail ({_0})")]
367367
struct TupleStruct(u32);
368368

369369
#[test]
@@ -378,7 +378,7 @@ fn test_tuple_struct_basic() {
378378
#[derive(n0_error::Error)]
379379
#[error(from_sources)]
380380
enum TupleEnum {
381-
#[display("io failed")]
381+
#[error("io failed")]
382382
Io(#[error(source, std_err)] io::Error),
383383
#[error(transparent)]
384384
Transparent(MyError),
@@ -415,7 +415,7 @@ pub fn test_skip_transparent_errors() {
415415
#[derive(Error)]
416416
#[error(from_sources)]
417417
enum ErrorA {
418-
#[display("failure at b")]
418+
#[error("failure at b")]
419419
ErrorB { source: ErrorB },
420420
}
421421

@@ -425,7 +425,7 @@ pub fn test_skip_transparent_errors() {
425425
enum ErrorB {
426426
#[error(transparent)]
427427
IoTransparent(io::Error),
428-
#[display("io error")]
428+
#[error("io error")]
429429
Io { source: io::Error },
430430
}
431431

@@ -487,7 +487,7 @@ pub fn test_skip_transparent_errors() {
487487
fn test_generics() {
488488
#[add_meta]
489489
#[derive(Error)]
490-
#[display("failed at {}", list.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", "))]
490+
#[error("failed at {}", list.iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", "))]
491491
struct GenericError<E: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static> {
492492
list: Vec<E>,
493493
}
@@ -499,7 +499,7 @@ fn test_generics() {
499499
Bar {
500500
list: Vec<E>,
501501
},
502-
#[display("failed at {other}")]
502+
#[error("failed at {other}")]
503503
Baz {
504504
other: Box<E>,
505505
},

0 commit comments

Comments
 (0)