Skip to main content

rspack_error/
error.rs

1use std::{fmt::Display, sync::Arc};
2
3use miette::{Diagnostic as MietteDiagnostic, LabeledSpan};
4use rspack_cacheable::cacheable;
5
6/// Error severity. Defaults to [`Severity::Error`].
7#[cacheable]
8#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
9pub enum Severity {
10  #[default]
11  Error,
12  Warning,
13}
14
15/// Label for source code.
16#[cacheable]
17#[derive(Debug, Clone, Default)]
18pub struct Label {
19  /// Label name.
20  pub name: Option<String>,
21  /// Source code offset.
22  pub offset: usize,
23  /// Length of impact.
24  pub len: usize,
25}
26
27/// Core error type.
28///
29/// See the test case for specific usage.
30#[cacheable]
31#[derive(Debug, Clone, Default)]
32pub struct ErrorData {
33  /// Error severity.
34  pub severity: Severity,
35  /// Message.
36  pub message: String,
37  /// Source code.
38  pub src: Option<Arc<str>>,
39  /// Labels displayed in source code.
40  ///
41  /// The source code block will be displayed only if both source code and labels exist.
42  pub labels: Option<Vec<Label>>,
43  /// Help text.
44  pub help: Option<String>,
45  /// Source error.
46  #[cacheable(omit_bounds)]
47  pub source_error: Option<Box<Error>>,
48  /// Error Code.
49  ///
50  /// This field is used to distinguish error types and will not be used for error display.
51  pub code: Option<String>,
52  /// Detail info.
53  ///
54  /// This field is used to save extra info when hide stack and will not be used for error display.
55  /// TODO: remove this field and hide stack, just use stack field.
56  pub details: Option<String>,
57  /// Error stack.
58  pub stack: Option<String>,
59  /// Whether to hide the stack.
60  ///
61  /// TODO: replace Option<bool> to bool.
62  pub hide_stack: Option<bool>,
63}
64
65/// ErrorData wrapper type.
66///
67/// Wrap ErrorData to avoid result_large_err.
68#[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.
319320  ╰─▶   ⚠ 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    // Force color support to prove the renderer respects its own theme
332    with_override(true, || {
333      assert_eq!(
334        renderer.render(&root_err).unwrap().trim(),
335        expect_display.trim()
336      );
337    });
338  }
339}