1use std::fmt::Display;
2
3use miette::{Diagnostic as MietteDiagnostic, LabeledSpan};
4use rspack_cacheable::cacheable;
5
6#[cacheable]
8#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
9pub enum Severity {
10 #[default]
11 Error,
12 Warning,
13}
14
15#[cacheable]
17#[derive(Debug, Clone, Default)]
18pub struct Label {
19 pub name: Option<String>,
21 pub offset: usize,
23 pub len: usize,
25}
26
27#[cacheable]
31#[derive(Debug, Clone, Default)]
32pub struct ErrorData {
33 pub severity: Severity,
35 pub message: String,
37 pub src: Option<String>,
39 pub labels: Option<Vec<Label>>,
43 pub help: Option<String>,
45 #[cacheable(omit_bounds)]
47 pub source_error: Option<Box<Error>>,
48 pub code: Option<String>,
52 pub details: Option<String>,
57 pub stack: Option<String>,
59 pub hide_stack: Option<bool>,
63}
64
65#[cacheable]
69#[derive(Debug, Clone, Default)]
70pub struct Error(Box<ErrorData>);
71
72impl std::ops::Deref for Error {
73 type Target = ErrorData;
74
75 fn deref(&self) -> &Self::Target {
76 &self.0
77 }
78}
79
80impl std::ops::DerefMut for Error {
81 fn deref_mut(&mut self) -> &mut Self::Target {
82 &mut self.0
83 }
84}
85
86impl Error {
87 #[allow(clippy::self_named_constructors)]
88 pub fn error(message: String) -> Self {
89 Self(Box::new(ErrorData {
90 message,
91 ..Default::default()
92 }))
93 }
94 pub fn warning(message: String) -> Self {
95 Self(Box::new(ErrorData {
96 severity: Severity::Warning,
97 message,
98 ..Default::default()
99 }))
100 }
101
102 pub fn from_string(
103 src: Option<String>,
104 start: usize,
105 end: usize,
106 title: String,
107 message: String,
108 ) -> Self {
109 let mut error = Error::error(format!("{title}: {message}"));
110 error.src = src;
111 error.labels = Some(vec![Label {
112 name: None,
113 offset: start,
114 len: end.saturating_sub(start),
115 }]);
116 error
117 }
118
119 pub fn from_error<T>(value: T) -> Self
120 where
121 T: std::error::Error,
122 {
123 let mut error = Error::error(value.to_string());
124 error.source_error = value.source().map(|e| Box::new(Error::from_error(e)));
125 error
126 }
127
128 pub fn is_error(&self) -> bool {
129 self.severity == Severity::Error
130 }
131 pub fn is_warn(&self) -> bool {
132 self.severity == Severity::Warning
133 }
134
135 pub fn wrap_err<D>(self, msg: D) -> Self
136 where
137 D: std::fmt::Display,
138 {
139 Self(Box::new(ErrorData {
140 message: msg.to_string(),
141 source_error: Some(Box::new(self)),
142 ..Default::default()
143 }))
144 }
145}
146
147impl Display for Error {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "{}", &self.message)
150 }
151}
152
153impl std::error::Error for Error {
154 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
155 self
156 .source_error
157 .as_ref()
158 .map(|e| e as &(dyn std::error::Error + 'static))
159 }
160}
161
162impl MietteDiagnostic for Error {
163 fn code(&self) -> Option<Box<dyn Display + '_>> {
164 self
165 .code
166 .as_ref()
167 .map(Box::new)
168 .map(|c| c as Box<dyn Display>)
169 }
170
171 fn severity(&self) -> Option<miette::Severity> {
172 match self.severity {
173 Severity::Error => Some(miette::Severity::Error),
174 Severity::Warning => Some(miette::Severity::Warning),
175 }
176 }
177
178 fn help(&self) -> Option<Box<dyn Display + '_>> {
179 self
180 .help
181 .as_ref()
182 .map(Box::new)
183 .map(|c| c as Box<dyn Display>)
184 }
185
186 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
187 self.src.as_ref().map(|s| s as &dyn miette::SourceCode)
188 }
189
190 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
191 let Some(labels) = &self.labels else {
192 return None;
193 };
194 Some(Box::new(labels.iter().map(|item| {
195 LabeledSpan::new(item.name.clone(), item.offset, item.len)
196 })))
197 }
198
199 fn diagnostic_source(&self) -> Option<&dyn MietteDiagnostic> {
200 self
201 .source_error
202 .as_ref()
203 .map(|s| &**s as &dyn MietteDiagnostic)
204 }
205}
206
207macro_rules! impl_from_error {
208 ($($t:ty),*) => {
209 $(
210 impl From<$t> for Error {
211 fn from(value: $t) -> Error {
212 Error::from_error(value)
213 }
214 }
215 ) *
216 }
217}
218
219impl_from_error! {
220 std::fmt::Error,
221 std::io::Error,
222 std::string::FromUtf8Error
223}
224
225impl<T> From<std::sync::mpsc::SendError<T>> for Error {
226 fn from(value: std::sync::mpsc::SendError<T>) -> Self {
227 Error::from_error(value)
228 }
229}
230
231impl From<anyhow::Error> for Error {
232 fn from(value: anyhow::Error) -> Self {
233 let mut error = Error::error(value.to_string());
234 error.source_error = value.source().map(|e| Box::new(Error::from_error(e)));
235 error
236 }
237}
238
239#[cfg(test)]
240mod test {
241 use super::{Error, ErrorData, Label};
242 use crate::{Renderer, Severity};
243 #[test]
244 fn should_error_display() {
245 let renderer = Renderer::new(false);
246 let sub_err = Error(Box::new(ErrorData {
247 severity: Severity::Warning,
248 message: "An unexpected keyword.".into(),
249 src: Some("const a = { const };\nconst b = { var };".into()),
250 labels: Some(vec![
251 Label {
252 name: Some("keyword 1".into()),
253 offset: 12,
254 len: 5,
255 },
256 Label {
257 name: Some("keyword 2".into()),
258 offset: 33,
259 len: 3,
260 },
261 ]),
262 help: Some("Maybe you should remove it.".into()),
263 source_error: None,
264 code: Some("ModuleAnalysisWarning".into()),
265 details: Some("detail info".into()),
266 stack: Some("stack info".into()),
267 hide_stack: None,
268 }));
269 let mid_err = Error(Box::new(ErrorData {
270 severity: Severity::Error,
271 message: "Can not parse current module.".into(),
272 src: Some("const a = { const };".into()),
273 labels: Some(vec![Label {
274 name: Some("parse failed".into()),
275 offset: 0,
276 len: 1,
277 }]),
278 help: Some("See follow info.".into()),
279 source_error: Some(Box::new(sub_err)),
280 code: Some("ModuleParseError".into()),
281 details: Some("detail info".into()),
282 stack: Some("stack info".into()),
283 hide_stack: None,
284 }));
285 let root_err = Error(Box::new(ErrorData {
286 severity: Severity::Error,
287 message: "Build Module Failed".into(),
288 src: None,
289 labels: None,
290 help: None,
291 source_error: Some(Box::new(mid_err)),
292 code: Some("ModuleBuildError".into()),
293 details: Some("detail info".into()),
294 stack: Some("stack info".into()),
295 hide_stack: None,
296 }));
297 let expect_display = r#"
298 × Build Module Failed
299 ├─▶ × Can not parse current module.
300 │ ╭────
301 │ 1 │ const a = { const };
302 │ · ┬
303 │ · ╰── parse failed
304 │ ╰────
305 │ help: See follow info.
306 │
307 ╰─▶ ⚠ An unexpected keyword.
308 ╭─[1:12]
309 1 │ const a = { const };
310 · ──┬──
311 · ╰── keyword 1
312 2 │ const b = { var };
313 · ─┬─
314 · ╰── keyword 2
315 ╰────
316 help: Maybe you should remove it.
317"#;
318 assert_eq!(
319 renderer.render(&root_err).unwrap().trim(),
320 expect_display.trim()
321 );
322 }
323}