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}