Skip to content

Commit

Permalink
docs(diagnostics): fully document oxc_diagnostics (#5865)
Browse files Browse the repository at this point in the history
Just a step in my mission to document our entire API
  • Loading branch information
DonIsaac committed Sep 19, 2024
1 parent 4e37d18 commit 83ca7f5
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 7 deletions.
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

0 comments on commit 83ca7f5

Please sign in to comment.