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}