Skip to content

Commit f8d6706

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

File tree

6 files changed

+121
-118
lines changed

6 files changed

+121
-118
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@
55
A error library that supports tracking the call-site location of errors. Also features an anyhow-style `AnyError`.
66

77
```rust
8-
use n0_error::{e, add_meta, Error, Result, StackResultExt, StdResultExt};
8+
use n0_error::{e, add_meta, StackError, Result, StackResultExt, StdResultExt};
99

1010
// Adds a `meta` field to all variants to track call-site error location.
1111
#[add_meta]
1212
// Derives the various impls for our error.
13-
#[derive(Error)]
13+
#[derive(StackError)]
1414
// Automatically create From impls from the error sources
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,
@@ -69,14 +69,14 @@ fn main() -> Result<()> {
6969
// In this case the meta field will be added as the last field.
7070

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

7676
#[add_meta]
77-
#[derive(Error)]
77+
#[derive(StackError)]
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: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,19 @@ fn print(err: impl StackError) {
6262
}
6363

6464
pub mod error {
65+
use n0_error::{StackError, add_meta};
6566
use std::io;
6667

67-
#[n0_error::add_meta]
68-
#[derive(n0_error::Error)]
68+
#[add_meta]
69+
#[derive(StackError)]
6970
#[error(from_sources)]
7071
pub enum OperationError {
7172
/// Failed to copy
7273
Copy { source: CopyError },
7374
}
7475

75-
#[n0_error::add_meta]
76-
#[derive(n0_error::Error)]
76+
#[add_meta]
77+
#[derive(StackError)]
7778
pub enum CopyError {
7879
/// Read error
7980
Read {
@@ -89,7 +90,7 @@ pub mod error {
8990
#[error(std_err)]
9091
source: io::Error,
9192
},
92-
#[display("Bad request - missing characters: {missing} {}", missing * 2)]
93+
#[error("Bad request - missing characters: {missing} {}", missing * 2)]
9394
BadRequest {
9495
missing: usize,
9596
},
@@ -101,8 +102,8 @@ pub mod error {
101102
Foo,
102103
}
103104

104-
#[n0_error::add_meta]
105-
#[derive(n0_error::Error)]
105+
#[add_meta]
106+
#[derive(StackError)]
106107
pub enum InvalidArgsError {
107108
/// Failed to parse arguments
108109
FailedToParse {},

n0-error-macros/src/lib.rs

Lines changed: 76 additions & 74 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(StackError, 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,16 +102,20 @@ 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
}
108-
_ => Err(err(&input, "#[derive(Error)] only supports enums or structs").into()),
109+
_ => Err(err(
110+
&input,
111+
"#[derive(StackError)] only supports enums or structs",
112+
)
113+
.into()),
109114
}
110115
}
111116

112117
struct SourceField<'a> {
113118
kind: SourceKind,
114-
transparent: bool,
115119
field: FieldInfo<'a>,
116120
}
117121

@@ -137,7 +141,7 @@ enum SourceKind {
137141
}
138142

139143
#[derive(Default, Clone, Copy, FromAttributes)]
140-
#[darling(default, attributes(error))]
144+
#[darling(default, attributes(error, stackerr))]
141145
struct TopAttrs {
142146
from_sources: bool,
143147
std_sources: bool,
@@ -154,18 +158,12 @@ struct FieldAttrs {
154158
meta: bool,
155159
}
156160

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
161+
// For each variant, capture doc comment text or #[error] attr
164162
struct VariantInfo<'a> {
165163
ident: Ident,
166164
fields: Vec<FieldInfo<'a>>,
167165
kind: Kind,
168-
display: Option<proc_macro2::TokenStream>,
166+
display: Display,
169167
source: Option<SourceField<'a>>,
170168
/// The field that is used for From<..> impls
171169
from: Option<FieldInfo<'a>>,
@@ -231,8 +229,10 @@ impl<'a> FieldIdent<'a> {
231229

232230
impl<'a> VariantInfo<'a> {
233231
fn transparent(&self) -> Option<&FieldInfo<'_>> {
234-
let source = self.source.as_ref()?;
235-
source.transparent.then_some(&source.field)
232+
match self.display {
233+
Display::Transparent => self.source.as_ref().map(|s| &s.field),
234+
_ => None,
235+
}
236236
}
237237

238238
fn field_binding_idents(&self) -> impl Iterator<Item = Ident> + '_ {
@@ -263,15 +263,7 @@ impl<'a> VariantInfo<'a> {
263263
attrs: &[Attribute],
264264
top: &TopAttrs,
265265
) -> 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-
// }
266+
let display = get_display(&attrs)?;
275267
let (kind, fields): (Kind, Vec<FieldInfo>) = match fields {
276268
Fields::Named(ref fields) => (
277269
Kind::Named,
@@ -308,15 +300,15 @@ impl<'a> VariantInfo<'a> {
308300
_ => false,
309301
}),
310302
Kind::Tuple => {
311-
if variant_attrs.transparent {
303+
if display.is_transparent() {
312304
fields.first()
313305
} else {
314306
None
315307
}
316308
}
317309
});
318310

319-
if variant_attrs.transparent && source_field.is_none() {
311+
if display.is_transparent() && source_field.is_none() {
320312
return Err(err(
321313
ident,
322314
"Variants with #[error(transparent)] require a source field",
@@ -333,11 +325,7 @@ impl<'a> VariantInfo<'a> {
333325
SourceKind::Stack
334326
};
335327
let field = (*field).clone();
336-
Some(SourceField {
337-
kind,
338-
transparent: variant_attrs.transparent,
339-
field,
340-
})
328+
Some(SourceField { kind, field })
341329
}
342330
};
343331

@@ -478,12 +466,12 @@ fn generate_enum_impls(
478466
});
479467

480468
let match_fmt_message_arms = variants.iter().map(|vi| match &vi.display {
481-
Some(expr) => {
469+
Display::Format(expr) => {
482470
let binds: Vec<Ident> = vi.field_binding_idents().collect();
483471
let pat = vi.spread_all(&binds);
484472
quote! { #pat => { #expr } }
485473
}
486-
None => {
474+
Display::Default | Display::Transparent => {
487475
let text = format!("{}::{}", enum_ident, vi.ident);
488476
let pat = vi.spread_empty();
489477
quote! { #pat => write!(f, #text) }
@@ -705,7 +693,7 @@ fn generate_struct_impl(
705693
quote! { write!(f, "{}", #expr) }
706694
} else {
707695
match &info.display {
708-
Some(expr) => {
696+
Display::Format(expr) => {
709697
let binds: Vec<Ident> = info.field_binding_idents().collect();
710698
match info.kind {
711699
Kind::Named => {
@@ -716,7 +704,7 @@ fn generate_struct_impl(
716704
}
717705
}
718706
}
719-
None => {
707+
Display::Default | Display::Transparent => {
720708
// Fallback to struct name
721709
let text = info.ident.to_string();
722710
quote! { write!(f, #text) }
@@ -868,47 +856,61 @@ fn generate_struct_impl(
868856
}
869857
}
870858

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-
}
859+
enum Display {
860+
Default,
861+
Transparent,
862+
Format(proc_macro2::TokenStream),
863+
}
864+
865+
impl Display {
866+
fn is_transparent(&self) -> bool {
867+
matches!(self, Self::Transparent)
909868
}
910869
}
911870

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

src/ext.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,16 +148,16 @@ impl<T> StackResultExt<T, NoneError> for Option<T> {
148148

149149
/// Error returned when converting [`Option`]s to an error.
150150
#[add_meta]
151-
#[derive(crate::Error)]
152-
#[display("Expected some, found none")]
151+
#[derive(crate::StackError)]
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]
157-
#[derive(crate::Error)]
157+
#[derive(crate::StackError)]
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/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
#![doc = include_str!("../README.md")]
99
#![deny(missing_docs)]
1010

11-
pub use n0_error_macros::{Error, add_meta};
11+
pub use n0_error_macros::{StackError, add_meta};
1212

1313
extern crate self as n0_error;
1414

0 commit comments

Comments
 (0)