Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/oxc_diagnostics/src/graphic_reporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,36 @@ use crate::graphical_theme::GraphicalTheme;

#[derive(Debug, Clone)]
pub struct GraphicalReportHandler {
/// How to render links.
///
/// Default: [`LinkStyle::Link`]
pub(crate) links: LinkStyle,
/// Terminal width to wrap at.
///
/// Default: `400`
pub(crate) termwidth: usize,
/// How to style reports
pub(crate) theme: GraphicalTheme,
pub(crate) footer: Option<String>,
/// Number of source lines to render before/after the line(s) covered by errors.
///
/// Default: `1`
pub(crate) context_lines: usize,
/// Tab print width
///
/// Default: `4`
pub(crate) tab_width: usize,
/// Unused.
pub(crate) with_cause_chain: bool,
/// Whether to wrap lines to fit the width.
///
/// Default: `true`
pub(crate) wrap_lines: bool,
/// Whether to break words during wrapping.
///
/// When `false`, line breaks will happen before the first word that would overflow `termwidth`.
///
/// Default: `true`
pub(crate) break_words: bool,
pub(crate) word_separator: Option<textwrap::WordSeparator>,
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
Expand Down
3 changes: 3 additions & 0 deletions crates/oxc_diagnostics/src/graphical_theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ and the

You can create your own custom graphical theme using this type, or you can use
one of the predefined ones using the methods below.

When created by [`Default::default`], themes are automatically selected based on the `NO_COLOR`
environment variable and whether the process is running in a terminal.
*/
#[derive(Debug, Clone)]
pub struct GraphicalTheme {
Expand Down
122 changes: 120 additions & 2 deletions crates/oxc_diagnostics/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,52 @@
//! Diagnostics Wrapper
//! Exports `miette`
//! Error data types and utilities for handling/reporting them.
//!
//! The main type in this module is [`OxcDiagnostic`], which is used by all other oxc tools to
//! report problems. It implements [miette]'s [`Diagnostic`] trait, making it compatible with other
//! tooling you may be using.
//!
//! ```rust
//! use oxc_diagnostics::{OxcDiagnostic, Result};
//! fn my_tool() -> Result<()> {
//! try_something().map_err(|e| OxcDiagnostic::error(e.to_string()))?;
//! Ok(())
//! }
//! ```
//!
//! See the [miette] documentation for more information on how to interact with diagnostics.
//!
//! ## Reporting
//! If you are writing your own tools that may produce their own errors, you can use
//! [`DiagnosticService`] to format and render them to a string or a stream. It can receive
//! [`Error`]s over a multi-producer, single consumer
//!
//! ```
//! use std::{sync::Arc, thread};
//! use oxc_diagnostics::{DiagnosticService, Error, OxcDiagnostic};
//!
//! fn my_tool() -> Result<()> {
//! try_something().map_err(|e| OxcDiagnostic::error(e.to_string()))?;
//! Ok(())
//! }
//!
//! let mut service = DiagnosticService::default();
//! let mut sender = service.sender().clone();
//!
//! thread::spawn(move || {
//! let file_path_being_processed = PathBuf::from("file.txt");
//! let file_being_processed = Arc::new(NamedSource::new(file_path_being_processed.clone()));
//!
//! for _ in 0..10 {
//! if let Err(diagnostic) = my_tool() {
//! let report = diagnostic.with_source_code(Arc::clone(&file_being_processed));
//! sender.send(Some(file_path_being_processed, vec![Error::new(e)]));
//! }
//! // send None to stop the service
//! sender.send(None);
//! }
//! });
//!
//! service.run();
//! ```

mod graphic_reporter;
mod graphical_theme;
Expand All @@ -26,6 +73,9 @@ pub type Result<T> = std::result::Result<T, OxcDiagnostic>;
use miette::{Diagnostic, SourceCode};
pub use miette::{LabeledSpan, NamedSource};

/// Describes an error or warning that occurred.
///
/// Used by all oxc tools.
#[derive(Debug, Clone)]
#[must_use]
pub struct OxcDiagnostic {
Expand Down Expand Up @@ -89,14 +139,19 @@ impl fmt::Display for OxcDiagnostic {
impl std::error::Error for OxcDiagnostic {}

impl Diagnostic for OxcDiagnostic {
/// The secondary help message.
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.help.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
}

/// The severity level of this diagnostic.
///
/// Diagnostics with missing severity levels should be treated as [errors](Severity::Error).
fn severity(&self) -> Option<Severity> {
Some(self.severity)
}

/// Labels covering problematic portions of source code.
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
self.labels
.as_ref()
Expand All @@ -105,16 +160,21 @@ impl Diagnostic for OxcDiagnostic {
.map(|b| b as Box<dyn Iterator<Item = LabeledSpan>>)
}

/// An error code uniquely identifying this diagnostic.
///
/// Note that codes may be scoped, which will be rendered as `scope(code)`.
fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.code.is_some().then(|| Box::new(&self.code) as Box<dyn Display>)
}

/// A URL that provides more information about the problem that occurred.
fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.url.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
}
}

impl OxcDiagnostic {
/// Create new an error-level [`OxcDiagnostic`].
pub fn error<T: Into<Cow<'static, str>>>(message: T) -> Self {
Self {
inner: Box::new(OxcDiagnosticInner {
Expand All @@ -128,6 +188,7 @@ impl OxcDiagnostic {
}
}

/// Create new a warning-level [`OxcDiagnostic`].
pub fn warn<T: Into<Cow<'static, str>>>(message: T) -> Self {
Self {
inner: Box::new(OxcDiagnosticInner {
Expand All @@ -141,6 +202,9 @@ impl OxcDiagnostic {
}
}

/// Add a scoped error code to this diagnostic.
///
/// This is a shorthand for `with_error_code_scope(scope).with_error_code_num(number)`.
#[inline]
pub fn with_error_code<T: Into<Cow<'static, str>>, U: Into<Cow<'static, str>>>(
self,
Expand All @@ -150,6 +214,9 @@ impl OxcDiagnostic {
self.with_error_code_scope(scope).with_error_code_num(number)
}

/// Add an error code scope to this diagnostic.
///
/// Use [`OxcDiagnostic::with_error_code`] to set both the scope and number at once.
#[inline]
pub fn with_error_code_scope<T: Into<Cow<'static, str>>>(mut self, code_scope: T) -> Self {
self.inner.code.scope = match self.inner.code.scope {
Expand All @@ -164,6 +231,9 @@ impl OxcDiagnostic {
self
}

/// Add an error code number to this diagnostic.
///
/// Use [`OxcDiagnostic::with_error_code`] to set both the scope and number at once.
#[inline]
pub fn with_error_code_num<T: Into<Cow<'static, str>>>(mut self, code_num: T) -> Self {
self.inner.code.number = match self.inner.code.number {
Expand All @@ -178,21 +248,63 @@ impl OxcDiagnostic {
self
}

/// Set the severity level of this diagnostic.
///
/// Use [`OxcDiagnostic::error`] or [`OxcDiagnostic::warn`] to create a diagnostic at the
/// severity you want.
pub fn with_severity(mut self, severity: Severity) -> Self {
self.inner.severity = severity;
self
}

/// Suggest a possible solution for a problem to the user.
///
/// ## Example
/// ```
/// use std::path::PathBuf;
/// use oxc_diagnostics::OxcDiagnostic
///
/// let config_file_path = Path::from("config.json");
/// if !config_file_path.exists() {
/// return Err(OxcDiagnostic::error("No config file found")
/// .with_help("Run my_tool --init to set up a new config file"));
/// }
/// ```
pub fn with_help<T: Into<Cow<'static, str>>>(mut self, help: T) -> Self {
self.inner.help = Some(help.into());
self
}

/// Set the label covering a problematic portion of source code.
///
/// Existing labels will be removed. Use [`OxcDiagnostic::and_label`] append a label instead.
///
/// You need to add some source code to this diagnostic (using
/// [`OxcDiagnostic::with_source_code`]) for this to actually be useful. Use
/// [`OxcDiagnostic::with_labels`] to add multiple labels all at once.
///
/// Note that this pairs nicely with [`oxc_span::Span`], particularly the [`label`] method.
///
/// [`oxc_span::Span`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html
/// [`label`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html#method.label
pub fn with_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
self.inner.labels = Some(vec![label.into()]);
self
}

/// Add multiple labels covering problematic portions of source code.
///
/// Existing labels will be removed. Use [`OxcDiagnostic::and_labels`] to append labels
/// instead.
///
/// You need to add some source code using [`OxcDiagnostic::with_source_code`] for this to
/// actually be useful. If you only have a single label, consider using
/// [`OxcDiagnostic::with_label`] instead.
///
/// Note that this pairs nicely with [`oxc_span::Span`], particularly the [`label`] method.
///
/// [`oxc_span::Span`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html
/// [`label`]: https://docs.rs/oxc_span/latest/oxc_span/struct.Span.html#method.label
pub fn with_labels<L: Into<LabeledSpan>, T: IntoIterator<Item = L>>(
mut self,
labels: T,
Expand All @@ -201,13 +313,15 @@ impl OxcDiagnostic {
self
}

/// Add a label to this diagnostic without clobbering existing labels.
pub fn and_label<T: Into<LabeledSpan>>(mut self, label: T) -> Self {
let mut labels = self.inner.labels.unwrap_or_default();
labels.push(label.into());
self.inner.labels = Some(labels);
self
}

/// Add multiple labels to this diagnostic without clobbering existing labels.
pub fn and_labels<L: Into<LabeledSpan>, T: IntoIterator<Item = L>>(
mut self,
labels: T,
Expand All @@ -218,11 +332,15 @@ impl OxcDiagnostic {
self
}

/// Add a URL that provides more information about this diagnostic.
pub fn with_url<S: Into<Cow<'static, str>>>(mut self, url: S) -> Self {
self.inner.url = Some(url.into());
self
}

/// Add source code to this diagnostic and convert it into an [`Error`].
///
/// You should use a [`NamedSource`] if you have a file name as well as the source code.
pub fn with_source_code<T: SourceCode + Send + Sync + 'static>(self, code: T) -> Error {
Error::from(self).with_source_code(code)
}
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_diagnostics/src/reporter/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use std::{
use super::{writer, DiagnosticReporter, Info};
use crate::{Error, Severity};

/// Formats reports using [GitHub Actions
/// annotations](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message). Useful for reporting in CI.
pub struct GithubReporter {
writer: BufWriter<Stdout>,
}
Expand Down
3 changes: 3 additions & 0 deletions crates/oxc_diagnostics/src/reporter/graphical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use std::io::{BufWriter, ErrorKind, Stdout, Write};
use super::{writer, DiagnosticReporter};
use crate::{Error, GraphicalReportHandler};

/// Pretty-prints diagnostics. Primarily meant for human-readable output in a terminal.
///
/// See [`GraphicalReportHandler`] for how to configure colors, context lines, etc.
pub struct GraphicalReporter {
handler: GraphicalReportHandler,
writer: BufWriter<Stdout>,
Expand Down
4 changes: 4 additions & 0 deletions crates/oxc_diagnostics/src/reporter/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ use miette::JSONReportHandler;
use super::DiagnosticReporter;
use crate::Error;

/// Renders reports as a JSON array of objects.
///
/// Note that, due to syntactic restrictions of JSON arrays, this reporter waits until all
/// diagnostics have been reported before writing them to the output stream.
#[derive(Default)]
pub struct JsonReporter {
diagnostics: Vec<Error>,
Expand Down
66 changes: 66 additions & 0 deletions crates/oxc_diagnostics/src/reporter/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! [Reporters](DiagnosticReporter) for rendering and writing diagnostics.

mod checkstyle;
mod github;
mod graphical;
Expand All @@ -18,9 +20,73 @@ fn writer() -> BufWriter<Stdout> {
BufWriter::new(std::io::stdout())
}

/// Reporters are responsible for rendering diagnostics to some format and writing them to some
/// form of output stream.
///
/// Reporters get used by [`DiagnosticService`](crate::service::DiagnosticService) when they
/// receive diagnostics.
///
/// ## Example
/// ```
/// use std::io::{self, Write, BufWriter, Stderr};
/// use oxc_diagnostics::{DiagnosticReporter, Error, Severity};
///
/// pub struct BufReporter {
/// writer: BufWriter<Stderr>,
/// }
///
/// impl Default for BufReporter {
/// fn default() -> Self {
/// Self { writer: BufWriter::new(io::stderr()) }
/// }
/// }
///
/// impl DiagnosticReporter for BufferedReporter {
/// // flush all remaining bytes when no more diagnostics will be reported
/// fn finish(&mut self) {
/// self.writer.flush().unwrap();
/// }
///
/// // write rendered reports to stderr
/// fn render_diagnostics(&mut self, s: &[u8]) {
/// self.writer.write_all(s).unwrap();
/// }
///
/// // render diagnostics to a simple Apache-like log format
/// fn render_error(&mut self, error: Error) -> Option<String> {
/// let level = match error.severity().unwrap_or_default() {
/// Severity::Error => "ERROR",
/// Severity::Warning => "WARN",
/// Severity::Advice => "INFO",
/// };
/// let rendered = format!("[{level}]: {error}");
///
/// Some(rendered)
/// }
/// }
/// ```
pub trait DiagnosticReporter {
/// Lifecycle hook that gets called when no more diagnostics will be reported.
///
/// Used primarily for flushing output stream buffers, but you don't just have to use it for
/// that. Some reporters (e.g. [`JSONReporter`]) store all diagnostics in memory, then write them
/// all at once.
///
/// While this method _should_ only ever be called a single time, this is not a guarantee
/// upheld in Oxc's API. Do not rely on this behavior.
///
/// [`JSONReporter`]: crate::reporter::JsonReporter
fn finish(&mut self);

/// Write a rendered collection of diagnostics to this reporter's output stream.
fn render_diagnostics(&mut self, s: &[u8]);

/// Render a diagnostic into this reporter's desired format. For example, a JSONLinesReporter
/// might return a stringified JSON object on a single line. Returns [`None`] to skip reporting
/// of this diagnostic.
///
/// Reporters should not use this method to write diagnostics to their output stream. That
/// should be done in [`render_diagnostics`](DiagnosticReporter::render_diagnostics).
fn render_error(&mut self, error: Error) -> Option<String>;
}

Expand Down
Loading