1use std::{fmt::Display, sync::Arc};
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<Arc<str>>,
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 Self::from_shared_source(src.map(Into::into), start, end, title, message)
110 }
111
112 pub fn from_shared_source(
113 src: Option<Arc<str>>,
114 start: usize,
115 end: usize,
116 title: String,
117 message: String,
118 ) -> Self {
119 let mut error = Error::error(format!("{title}: {message}"));
120 error.src = src;
121 error.labels = Some(vec![Label {
122 name: None,
123 offset: start,
124 len: end.saturating_sub(start),
125 }]);
126 error
127 }
128
129 pub fn from_error<T>(value: T) -> Self
130 where
131 T: std::error::Error,
132 {
133 let mut error = Error::error(value.to_string());
134 error.source_error = value.source().map(|e| Box::new(Error::from_error(e)));
135 error
136 }
137
138 pub fn is_error(&self) -> bool {
139 self.severity == Severity::Error
140 }
141 pub fn is_warn(&self) -> bool {
142 self.severity == Severity::Warning
143 }
144
145 pub fn wrap_err<D>(self, msg: D) -> Self
146 where
147 D: std::fmt::Display,
148 {
149 Self(Box::new(ErrorData {
150 message: msg.to_string(),
151 source_error: Some(Box::new(self)),
152 ..Default::default()
153 }))
154 }
155}
156
157impl Display for Error {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 write!(f, "{}", &self.message)
160 }
161}
162
163impl std::error::Error for Error {
164 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
165 self
166 .source_error
167 .as_ref()
168 .map(|e| e as &(dyn std::error::Error + 'static))
169 }
170}
171
172impl MietteDiagnostic for Error {
173 fn code(&self) -> Option<Box<dyn Display + '_>> {
174 self
175 .code
176 .as_ref()
177 .map(Box::new)
178 .map(|c| c as Box<dyn Display>)
179 }
180
181 fn severity(&self) -> Option<miette::Severity> {
182 match self.severity {
183 Severity::Error => Some(miette::Severity::Error),
184 Severity::Warning => Some(miette::Severity::Warning),
185 }
186 }
187
188 fn help(&self) -> Option<Box<dyn Display + '_>> {
189 self
190 .help
191 .as_ref()
192 .map(Box::new)
193 .map(|c| c as Box<dyn Display>)
194 }
195
196 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
197 self.src.as_ref().map(|s| s as &dyn miette::SourceCode)
198 }
199
200 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
201 let Some(labels) = &self.labels else {
202 return None;
203 };
204 Some(Box::new(labels.iter().map(|item| {
205 LabeledSpan::new(item.name.clone(), item.offset, item.len)
206 })))
207 }
208
209 fn diagnostic_source(&self) -> Option<&dyn MietteDiagnostic> {
210 self
211 .source_error
212 .as_ref()
213 .map(|s| &**s as &dyn MietteDiagnostic)
214 }
215}
216
217macro_rules! impl_from_error {
218 ($($t:ty),*) => {
219 $(
220 impl From<$t> for Error {
221 fn from(value: $t) -> Error {
222 Error::from_error(value)
223 }
224 }
225 ) *
226 }
227}
228
229impl_from_error! {
230 std::fmt::Error,
231 std::io::Error,
232 std::string::FromUtf8Error
233}
234
235impl<T> From<std::sync::mpsc::SendError<T>> for Error {
236 fn from(value: std::sync::mpsc::SendError<T>) -> Self {
237 Error::from_error(value)
238 }
239}
240
241impl From<anyhow::Error> for Error {
242 fn from(value: anyhow::Error) -> Self {
243 let mut error = Error::error(value.to_string());
244 error.source_error = value.source().map(|e| Box::new(Error::from_error(e)));
245 error
246 }
247}
248
249#[cfg(test)]
250mod test {
251 use owo_colors::with_override;
252
253 use super::{Error, ErrorData, Label};
254 use crate::{Renderer, Severity};
255
256 #[test]
257 fn should_error_display() {
258 let renderer = Renderer::new(false);
259 let sub_err = Error(Box::new(ErrorData {
260 severity: Severity::Warning,
261 message: "An unexpected keyword.".into(),
262 src: Some("const a = { const };\nconst b = { var };".into()),
263 labels: Some(vec![
264 Label {
265 name: Some("keyword 1".into()),
266 offset: 12,
267 len: 5,
268 },
269 Label {
270 name: Some("keyword 2".into()),
271 offset: 33,
272 len: 3,
273 },
274 ]),
275 help: Some("Maybe you should remove it.".into()),
276 source_error: None,
277 code: Some("ModuleAnalysisWarning".into()),
278 details: Some("detail info".into()),
279 stack: Some("stack info".into()),
280 hide_stack: None,
281 }));
282 let mid_err = Error(Box::new(ErrorData {
283 severity: Severity::Error,
284 message: "Can not parse current module.".into(),
285 src: Some("const a = { const };".into()),
286 labels: Some(vec![Label {
287 name: Some("parse failed".into()),
288 offset: 0,
289 len: 1,
290 }]),
291 help: Some("See follow info.".into()),
292 source_error: Some(Box::new(sub_err)),
293 code: Some("ModuleParseError".into()),
294 details: Some("detail info".into()),
295 stack: Some("stack info".into()),
296 hide_stack: None,
297 }));
298 let root_err = Error(Box::new(ErrorData {
299 severity: Severity::Error,
300 message: "Build Module Failed".into(),
301 src: None,
302 labels: None,
303 help: None,
304 source_error: Some(Box::new(mid_err)),
305 code: Some("ModuleBuildError".into()),
306 details: Some("detail info".into()),
307 stack: Some("stack info".into()),
308 hide_stack: None,
309 }));
310 let expect_display = r#"
311 × Build Module Failed
312 ├─▶ × Can not parse current module.
313 │ ╭────
314 │ 1 │ const a = { const };
315 │ · ┬
316 │ · ╰── parse failed
317 │ ╰────
318 │ help: See follow info.
319 │
320 ╰─▶ ⚠ An unexpected keyword.
321 ╭─[1:12]
322 1 │ const a = { const };
323 · ──┬──
324 · ╰── keyword 1
325 2 │ const b = { var };
326 · ─┬─
327 · ╰── keyword 2
328 ╰────
329 help: Maybe you should remove it.
330"#;
331 with_override(true, || {
333 assert_eq!(
334 renderer.render(&root_err).unwrap().trim(),
335 expect_display.trim()
336 );
337 });
338 }
339}