rspack_error/
error.rs

1use std::fmt::Display;
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<String>,
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    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.
306307  ╰─▶   ⚠ 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}