onlyerror/
lib.rs

1//! `no_std`-compatible derive macro for error handling.
2//!
3//! `onlyerror` is comparable in feature set to the venerable [`thiserror`] crate with two major
4//! differences:
5//!
6//! 1. The feature subset is highly restricted.
7//! 2. Generally much faster compile times.
8//!
9//! For more on compile times, see the [`myn` benchmarks].
10//!
11//! # Example
12//!
13//! ```
14//! use onlyerror::Error;
15//!
16//! #[derive(Debug, Error)]
17//! pub enum HttpClientError {
18//!     /// I/O error.
19//!     Io(#[from] std::io::Error),
20//!
21//!     /// Login error.
22//!     #[error("Login error. Server message: `{0}`.")]
23//!     LoginError(String),
24//!
25//!     /// Invalid header.
26//!     #[error("Invalid header (expected {expected:?}, found {found:?}).")]
27//!     InvalidHeader {
28//!         expected: String,
29//!         found: String,
30//!     },
31//!
32//!     /// Unknown.
33//!     Unknown,
34//! }
35//! ```
36//!
37//! # DSL reference
38//!
39//! The macro has a DSL modeled after `thiserror`, so it should feel familiar to anyone who has used
40//! it.
41//!
42//! - The macro derives an implementation for the `Error` trait.
43//! - `Display` is derived using the `#[error("...")]` attributes with a fallback to doc comments.
44//! - `From` is derived for each `#[from]` or `#[source]` attribute.
45//!
46//! Error messages in `#[error("...")]` can reference enum variant fields by name (for struct-like
47//! variants) or by number (for tuple-like variants) using the [`std::fmt`] machinery.
48//!
49//! It is recommended to use `#[error("...")]` when you need interpolation, otherwise use doc
50//! comments. Doing this will keep implementation details out of your documentation while making
51//! the error variants self-documenting.
52//!
53//! # Limitations
54//!
55//! - Only `enum` types are supported by the [`Error`] macro.
56//! - Only inline string interpolations are supported by the derived `Display` impl.
57//! - Either all variants must be given an error message, or `#[no_display]` attribute must be set
58//!   to enum with hand-written `Display` implementation
59//! - `From` impls are only derived for `#[from]` and `#[source]` attributes, not implicitly for any
60//!   field names.
61//! - `Backtrace` is not supported.
62//! - `#[error(transparent)]` is not supported.
63//!
64//! # Cargo features
65//!
66//! - `std` (default): use the [`std::error`] module.
67//!
68//! To use `onlyerror` in a `no_std` environment, disable default features in your Cargo manifest.
69//!
70//! As of writing, you must add `#![feature(error_in_core)]` to the top-level `lib.rs` or `main.rs`
71//! file to enable the [`core::error`] module. This feature flag is only available on nightly
72//! compilers.
73//!
74//! [`Error`]: derive@Error
75//! [`myn` benchmarks]: https://github.com/parasyte/myn/blob/main/benchmarks.md
76//! [`thiserror`]: https://docs.rs/thiserror
77
78#![forbid(unsafe_code)]
79#![deny(clippy::all)]
80#![deny(clippy::pedantic)]
81#![allow(clippy::let_underscore_untyped)]
82
83use crate::parser::{Error, ErrorSource, VariantType};
84use myn::utils::spanned_error;
85use proc_macro::{Span, TokenStream};
86use std::{fmt::Write as _, rc::Rc, str::FromStr as _};
87
88mod parser;
89
90#[allow(clippy::too_many_lines)]
91#[proc_macro_derive(Error, attributes(error, from, source, no_display))]
92pub fn derive_error(input: TokenStream) -> TokenStream {
93    let ast = match Error::parse(input) {
94        Ok(ast) => ast,
95        Err(err) => return err,
96    };
97
98    #[cfg(feature = "std")]
99    let std_crate = "std";
100    #[cfg(not(feature = "std"))]
101    let std_crate = "core";
102
103    let name = &ast.name;
104    let error_matches = ast
105        .variants
106        .iter()
107        .filter_map(|v| match &v.source {
108            ErrorSource::From(index) | ErrorSource::Source(index) => {
109                let name = &v.name;
110
111                Some(match &v.ty {
112                    VariantType::Unit => format!("Self::{name} => None,"),
113                    VariantType::Tuple => {
114                        let index_num: usize = index.parse().unwrap_or_default();
115                        let fields = (0..v.fields.len())
116                            .map(|i| if i == index_num { "field," } else { "_," })
117                            .collect::<String>();
118
119                        format!("Self::{name}({fields}) => Some(field),")
120                    }
121                    VariantType::Struct => {
122                        format!("Self::{name} {{ {index}, ..}} => Some({index}),")
123                    }
124                })
125            }
126            ErrorSource::None => None,
127        })
128        .collect::<String>();
129
130    let display_impl = if ast.no_display {
131        String::new()
132    } else {
133        let display = ast.variants.iter().map(|v| {
134            let name = &v.name;
135            let display = &v.display;
136
137            if display.is_empty() {
138                return Err(name);
139            }
140
141            let display_fields =
142                v.display_fields
143                    .iter()
144                    .fold(String::new(), |mut fields, field| {
145                        let _ = write!(fields, "{field},");
146                        fields
147                    });
148
149            Ok(match &v.ty {
150                VariantType::Unit => format!("Self::{name} => write!(f, {display:?}),"),
151                VariantType::Tuple => {
152                    let fields = (0..v.fields.len()).fold(String::new(), |mut fields, i| {
153                        if v.display_fields.contains(&Rc::from(format!("field_{i}"))) {
154                            let _ = write!(fields, "field_{i},");
155                        } else {
156                            let _ = fields.write_str("_,");
157                        }
158                        fields
159                    });
160                    format!("Self::{name}({fields}) => write!(f, {display:?}, {display_fields}),")
161                }
162                VariantType::Struct => {
163                    format!(
164                        "Self::{name} {{ {display_fields} .. }} => \
165                        write!(f, {display:?}, {display_fields}),"
166                    )
167                }
168            })
169        });
170        let mut display_matches = String::new();
171        for res in display {
172            match res {
173                Err(name) => {
174                    return spanned_error("Required error message is missing", name.span());
175                }
176                Ok(msg) => display_matches.push_str(&msg),
177            }
178        }
179        let display_matches = if display_matches.is_empty() {
180            String::from("Ok(())")
181        } else {
182            format!("match self {{ {display_matches} }}")
183        };
184
185        format!(
186            r#"impl ::{std_crate}::fmt::Display for {name} {{
187                fn fmt(&self, f: &mut ::{std_crate}::fmt::Formatter<'_>) ->
188                    ::{std_crate}::result::Result<(), ::{std_crate}::fmt::Error>
189                {{
190                    {display_matches}
191                }}
192            }}"#
193        )
194    };
195
196    let from_impls = ast
197        .variants
198        .into_iter()
199        .filter_map(|v| match v.source {
200            ErrorSource::From(index) => {
201                let variant_name = v.name;
202                let from_ty = &v.fields[&index];
203                let body = if v.ty == VariantType::Tuple {
204                    format!(r#"Self::{variant_name}(value)"#)
205                } else {
206                    format!(r#"Self::{variant_name} {{ {index}: value }}"#)
207                };
208
209                Some(format!(
210                    r#"impl ::{std_crate}::convert::From<{from_ty}> for {name} {{
211                        fn from(value: {from_ty}) -> Self {{
212                            {body}
213                        }}
214                    }}"#
215                ))
216            }
217            _ => None,
218        })
219        .collect::<String>();
220
221    let code = TokenStream::from_str(&format!(
222        r#"
223            impl ::{std_crate}::error::Error for {name} {{
224                fn source(&self) -> Option<&(dyn ::{std_crate}::error::Error + 'static)> {{
225                    match self {{
226                        {error_matches}
227                        _ => None,
228                    }}
229                }}
230            }}
231
232            {display_impl}
233            {from_impls}
234        "#
235    ));
236
237    match code {
238        Ok(stream) => stream,
239        Err(err) => spanned_error(err.to_string(), Span::call_site()),
240    }
241}