oxc_diagnostics/
lib.rs

1//! Error data types and utilities for handling/reporting them.
2//!
3//! The main type in this module is [`OxcDiagnostic`], which is used by all other oxc tools to
4//! report problems. It implements [miette]'s [`Diagnostic`] trait, making it compatible with other
5//! tooling you may be using.
6//!
7//! ```rust
8//! use oxc_diagnostics::{OxcDiagnostic, Result};
9//! fn my_tool() -> Result<()> {
10//!     try_something().map_err(|e| OxcDiagnostic::error(e.to_string()))?;
11//!     Ok(())
12//! }
13//! ```
14//!
15//! See the [miette] documentation for more information on how to interact with diagnostics.
16//!
17//! ## Reporting
18//! If you are writing your own tools that may produce their own errors, you can use
19//! [`DiagnosticService`] to format and render them to a string or a stream. It can receive
20//! [`Error`]s over a multi-producer, single consumer
21//!
22//! ```
23//! use std::{sync::Arc, thread};
24//! use oxc_diagnostics::{DiagnosticService, Error, OxcDiagnostic};
25//!
26//! fn my_tool() -> Result<()> {
27//!     try_something().map_err(|e| OxcDiagnostic::error(e.to_string()))?;
28//!     Ok(())
29//! }
30//!
31//! let mut service = DiagnosticService::default();
32//! let mut sender = service.sender().clone();
33//!
34//! thread::spawn(move || {
35//!     let file_path_being_processed = PathBuf::from("file.txt");
36//!     let file_being_processed = Arc::new(NamedSource::new(file_path_being_processed.clone()));
37//!
38//!     for _ in 0..10 {
39//!         if let Err(diagnostic) = my_tool() {
40//!             let report = diagnostic.with_source_code(Arc::clone(&file_being_processed));
41//!             sender.send(Some(file_path_being_processed, vec![Error::new(e)]));
42//!         }
43//!         // send None to stop the service
44//!         sender.send(None);
45//!     }
46//! });
47//!
48//! service.run();
49//! ```
50
51mod service;
52
53use std::{
54    borrow::Cow,
55    fmt::{self, Display},
56    ops::{Deref, DerefMut},
57};
58
59pub mod reporter;
60
61pub use crate::service::{DiagnosticSender, DiagnosticService, DiagnosticTuple};
62
63pub type Error = miette::Error;
64pub type Severity = miette::Severity;
65
66pub type Result<T> = std::result::Result<T, OxcDiagnostic>;
67
68use miette::{Diagnostic, SourceCode};
69pub use miette::{GraphicalReportHandler, GraphicalTheme, LabeledSpan, NamedSource};
70
71/// Describes an error or warning that occurred.
72///
73/// Used by all oxc tools.
74#[derive(Debug, Clone, Eq, PartialEq)]
75#[must_use]
76pub struct OxcDiagnostic {
77    // `Box` the data to make `OxcDiagnostic` 8 bytes so that `Result` is small.
78    // This is required because rust does not performance return value optimization.
79    inner: Box<OxcDiagnosticInner>,
80}
81
82impl Deref for OxcDiagnostic {
83    type Target = Box<OxcDiagnosticInner>;
84
85    fn deref(&self) -> &Self::Target {
86        &self.inner
87    }
88}
89
90impl DerefMut for OxcDiagnostic {
91    fn deref_mut(&mut self) -> &mut Self::Target {
92        &mut self.inner
93    }
94}
95
96#[derive(Debug, Default, Clone, Eq, PartialEq)]
97pub struct OxcCode {
98    pub scope: Option<Cow<'static, str>>,
99    pub number: Option<Cow<'static, str>>,
100}
101
102impl OxcCode {
103    pub fn is_some(&self) -> bool {
104        self.scope.is_some() || self.number.is_some()
105    }
106}
107
108impl Display for OxcCode {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match (&self.scope, &self.number) {
111            (Some(scope), Some(number)) => write!(f, "{scope}({number})"),
112            (Some(scope), None) => scope.fmt(f),
113            (None, Some(number)) => number.fmt(f),
114            (None, None) => Ok(()),
115        }
116    }
117}
118
119#[derive(Debug, Clone, Eq, PartialEq)]
120pub struct OxcDiagnosticInner {
121    pub message: Cow<'static, str>,
122    pub labels: Option<Vec<LabeledSpan>>,
123    pub help: Option<Cow<'static, str>>,
124    pub severity: Severity,
125    pub code: OxcCode,
126    pub url: Option<Cow<'static, str>>,
127}
128
129impl Display for OxcDiagnostic {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
131        self.message.fmt(f)
132    }
133}
134
135impl std::error::Error for OxcDiagnostic {}
136
137impl Diagnostic for OxcDiagnostic {
138    /// The secondary help message.
139    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
140        self.help.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
141    }
142
143    /// The severity level of this diagnostic.
144    ///
145    /// Diagnostics with missing severity levels should be treated as [errors](Severity::Error).
146    fn severity(&self) -> Option<Severity> {
147        Some(self.severity)
148    }
149
150    /// Labels covering problematic portions of source code.
151    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
152        self.labels
153            .as_ref()
154            .map(|ls| ls.iter().cloned())
155            .map(Box::new)
156            .map(|b| b as Box<dyn Iterator<Item = LabeledSpan>>)
157    }
158
159    /// An error code uniquely identifying this diagnostic.
160    ///
161    /// Note that codes may be scoped, which will be rendered as `scope(code)`.
162    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
163        self.code.is_some().then(|| Box::new(&self.code) as Box<dyn Display>)
164    }
165
166    /// A URL that provides more information about the problem that occurred.
167    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
168        self.url.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
169    }
170}
171
172impl OxcDiagnostic {
173    /// Create new an error-level [`OxcDiagnostic`].
174    pub fn error<T: Into<Cow<'static, str>>>(message: T) -> Self {
175        Self {
176            inner: Box::new(OxcDiagnosticInner {
177                message: message.into(),
178                labels: None,
179                help: None,
180                severity: Severity::Error,
181                code: OxcCode::default(),
182                url: None,
183            }),
184        }
185    }
186
187    /// Create new a warning-level [`OxcDiagnostic`].
188    pub fn warn<T: Into<Cow<'static, str>>>(message: T) -> Self {
189        Self {
190            inner: Box::new(OxcDiagnosticInner {
191                message: message.into(),
192                labels: None,
193                help: None,
194                severity: Severity::Warning,
195                code: OxcCode::default(),
196                url: None,
197            }),
198        }
199    }
200
201    /// Add a scoped error code to this diagnostic.
202    ///
203    /// This is a shorthand for `with_error_code_scope(scope).with_error_code_num(number)`.
204    #[inline]
205    pub fn with_error_code<T: Into<Cow<'static, str>>, U: Into<Cow<'static, str>>>(
206        self,
207        scope: T,
208        number: U,
209    ) -> Self {
210        self.with_error_code_scope(scope).with_error_code_num(number)
211    }
212
213    /// Add an error code scope to this diagnostic.
214    ///
215    /// Use [`OxcDiagnostic::with_error_code`] to set both the scope and number at once.
216    #[inline]
217    pub fn with_error_code_scope<T: Into<Cow<'static, str>>>(mut self, code_scope: T) -> Self {
218        self.inner.code.scope = match self.inner.code.scope {
219            Some(scope) => Some(scope),
220            None => Some(code_scope.into()),
221        };
222        debug_assert!(
223            self.inner.code.scope.as_ref().is_some_and(|s| !s.is_empty()),
224            "Error code scopes cannot be empty"
225        );
226
227        self
228    }
229
230    /// Add an error code number to this diagnostic.
231    ///
232    /// Use [`OxcDiagnostic::with_error_code`] to set both the scope and number at once.
233    #[inline]
234    pub fn with_error_code_num<T: Into<Cow<'static, str>>>(mut self, code_num: T) -> Self {
235        self.inner.code.number = match self.inner.code.number {
236            Some(num) => Some(num),
237            None => Some(code_num.into()),
238        };
239        debug_assert!(
240            self.inner.code.number.as_ref().is_some_and(|n| !n.is_empty()),
241            "Error code numbers cannot be empty"
242        );
243
244        self
245    }
246
247    /// Set the severity level of this diagnostic.
248    ///
249    /// Use [`OxcDiagnostic::error`] or [`OxcDiagnostic::warn`] to create a diagnostic at the
250    /// severity you want.
251    pub fn with_severity(mut self, severity: Severity) -> Self {
252        self.inner.severity = severity;
253        self
254    }
255
256    /// Suggest a possible solution for a problem to the user.
257    ///
258    /// ## Example
259    /// ```
260    /// use std::path::PathBuf;
261    /// use oxc_diagnostics::OxcDiagnostic
262    ///
263    /// let config_file_path = Path::from("config.json");
264    /// if !config_file_path.exists() {
265    ///     return Err(OxcDiagnostic::error("No config file found")
266    ///         .with_help("Run my_tool --init to set up a new config file"));
267    /// }
268    /// ```
269    pub fn with_help<T: Into<Cow<'static, str>>>(mut self, help: T) -> Self {
270        self.inner.help = Some(help.into());
271        self
272    }
273
274    /// Set the label covering a problematic portion of source code.
275    ///
276    /// Existing labels will be removed. Use [`OxcDiagnostic::and_label`] append a label instead.
277    ///
278    /// You need to add some source code to this diagnostic (using
279    /// [`OxcDiagnostic::with_source_code`]) for this to actually be useful. Use
280    /// [`OxcDiagnostic::with_labels`] to add multiple labels all at once.
281    ///
282    /// Note that this pairs nicely with [`oxc_span::Span`], particularly the [`label`] method.
283    ///
284    /// [`oxc_span::Span`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html
285    /// [`label`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html#method.label
286    pub fn with_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
287        self.inner.labels = Some(vec![label.into()]);
288        self
289    }
290
291    /// Add multiple labels covering problematic portions of source code.
292    ///
293    /// Existing labels will be removed. Use [`OxcDiagnostic::and_labels`] to append labels
294    /// instead.
295    ///
296    /// You need to add some source code using [`OxcDiagnostic::with_source_code`] for this to
297    /// actually be useful. If you only have a single label, consider using
298    /// [`OxcDiagnostic::with_label`] instead.
299    ///
300    /// Note that this pairs nicely with [`oxc_span::Span`], particularly the [`label`] method.
301    ///
302    /// [`oxc_span::Span`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html
303    /// [`label`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html#method.label
304    pub fn with_labels<L: Into<LabeledSpan>, T: IntoIterator<Item = L>>(
305        mut self,
306        labels: T,
307    ) -> Self {
308        self.inner.labels = Some(labels.into_iter().map(Into::into).collect());
309        self
310    }
311
312    /// Add a label to this diagnostic without clobbering existing labels.
313    pub fn and_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
314        let mut labels = self.inner.labels.unwrap_or_default();
315        labels.push(label.into());
316        self.inner.labels = Some(labels);
317        self
318    }
319
320    /// Add multiple labels to this diagnostic without clobbering existing labels.
321    pub fn and_labels<L: Into<LabeledSpan>, T: IntoIterator<Item = L>>(
322        mut self,
323        labels: T,
324    ) -> Self {
325        let mut all_labels = self.inner.labels.unwrap_or_default();
326        all_labels.extend(labels.into_iter().map(Into::into));
327        self.inner.labels = Some(all_labels);
328        self
329    }
330
331    /// Add a URL that provides more information about this diagnostic.
332    pub fn with_url<S: Into<Cow<'static, str>>>(mut self, url: S) -> Self {
333        self.inner.url = Some(url.into());
334        self
335    }
336
337    /// Add source code to this diagnostic and convert it into an [`Error`].
338    ///
339    /// You should use a [`NamedSource`] if you have a file name as well as the source code.
340    pub fn with_source_code<T: SourceCode + Send + Sync + 'static>(self, code: T) -> Error {
341        Error::from(self).with_source_code(code)
342    }
343}