From 83ca7f56cd29e7dc715930783c6765b4186d0c8b Mon Sep 17 00:00:00 2001 From: DonIsaac <22823424+DonIsaac@users.noreply.github.com> Date: Thu, 19 Sep 2024 01:43:08 +0000 Subject: [PATCH] docs(diagnostics): fully document `oxc_diagnostics` (#5865) Just a step in my mission to document our entire API --- .../oxc_diagnostics/src/graphic_reporter.rs | 22 ++++ crates/oxc_diagnostics/src/graphical_theme.rs | 3 + crates/oxc_diagnostics/src/lib.rs | 122 +++++++++++++++++- crates/oxc_diagnostics/src/reporter/github.rs | 2 + .../oxc_diagnostics/src/reporter/graphical.rs | 3 + crates/oxc_diagnostics/src/reporter/json.rs | 4 + crates/oxc_diagnostics/src/reporter/mod.rs | 66 ++++++++++ crates/oxc_diagnostics/src/service.rs | 86 +++++++++++- 8 files changed, 301 insertions(+), 7 deletions(-) diff --git a/crates/oxc_diagnostics/src/graphic_reporter.rs b/crates/oxc_diagnostics/src/graphic_reporter.rs index 82961bb3b9895..ea55f7fd31b5d 100644 --- a/crates/oxc_diagnostics/src/graphic_reporter.rs +++ b/crates/oxc_diagnostics/src/graphic_reporter.rs @@ -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, + /// 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, pub(crate) word_splitter: Option, diff --git a/crates/oxc_diagnostics/src/graphical_theme.rs b/crates/oxc_diagnostics/src/graphical_theme.rs index 11455c2180452..f5b6f8d85e1d0 100644 --- a/crates/oxc_diagnostics/src/graphical_theme.rs +++ b/crates/oxc_diagnostics/src/graphical_theme.rs @@ -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 { diff --git a/crates/oxc_diagnostics/src/lib.rs b/crates/oxc_diagnostics/src/lib.rs index 1b837344b040a..40e77dbb03b84 100644 --- a/crates/oxc_diagnostics/src/lib.rs +++ b/crates/oxc_diagnostics/src/lib.rs @@ -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; @@ -26,6 +73,9 @@ pub type Result = std::result::Result; 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 { @@ -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> { self.help.as_ref().map(Box::new).map(|c| c as Box) } + /// The severity level of this diagnostic. + /// + /// Diagnostics with missing severity levels should be treated as [errors](Severity::Error). fn severity(&self) -> Option { Some(self.severity) } + /// Labels covering problematic portions of source code. fn labels(&self) -> Option + '_>> { self.labels .as_ref() @@ -105,16 +160,21 @@ impl Diagnostic for OxcDiagnostic { .map(|b| b as Box>) } + /// 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> { self.code.is_some().then(|| Box::new(&self.code) as Box) } + /// A URL that provides more information about the problem that occurred. fn url<'a>(&'a self) -> Option> { self.url.as_ref().map(Box::new).map(|c| c as Box) } } impl OxcDiagnostic { + /// Create new an error-level [`OxcDiagnostic`]. pub fn error>>(message: T) -> Self { Self { inner: Box::new(OxcDiagnosticInner { @@ -128,6 +188,7 @@ impl OxcDiagnostic { } } + /// Create new a warning-level [`OxcDiagnostic`]. pub fn warn>>(message: T) -> Self { Self { inner: Box::new(OxcDiagnosticInner { @@ -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>, U: Into>>( self, @@ -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>>(mut self, code_scope: T) -> Self { self.inner.code.scope = match self.inner.code.scope { @@ -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>>(mut self, code_num: T) -> Self { self.inner.code.number = match self.inner.code.number { @@ -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>>(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>(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, T: IntoIterator>( mut self, labels: T, @@ -201,6 +313,7 @@ impl OxcDiagnostic { self } + /// Add a label to this diagnostic without clobbering existing labels. pub fn and_label>(mut self, label: T) -> Self { let mut labels = self.inner.labels.unwrap_or_default(); labels.push(label.into()); @@ -208,6 +321,7 @@ impl OxcDiagnostic { self } + /// Add multiple labels to this diagnostic without clobbering existing labels. pub fn and_labels, T: IntoIterator>( mut self, labels: T, @@ -218,11 +332,15 @@ impl OxcDiagnostic { self } + /// Add a URL that provides more information about this diagnostic. pub fn with_url>>(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(self, code: T) -> Error { Error::from(self).with_source_code(code) } diff --git a/crates/oxc_diagnostics/src/reporter/github.rs b/crates/oxc_diagnostics/src/reporter/github.rs index a289a8dcb8f97..1fd8d818d5b25 100644 --- a/crates/oxc_diagnostics/src/reporter/github.rs +++ b/crates/oxc_diagnostics/src/reporter/github.rs @@ -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, } diff --git a/crates/oxc_diagnostics/src/reporter/graphical.rs b/crates/oxc_diagnostics/src/reporter/graphical.rs index 6751b2483b30e..69437a5844a0d 100644 --- a/crates/oxc_diagnostics/src/reporter/graphical.rs +++ b/crates/oxc_diagnostics/src/reporter/graphical.rs @@ -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, diff --git a/crates/oxc_diagnostics/src/reporter/json.rs b/crates/oxc_diagnostics/src/reporter/json.rs index c86efe0f5c541..2a52fe9767736 100644 --- a/crates/oxc_diagnostics/src/reporter/json.rs +++ b/crates/oxc_diagnostics/src/reporter/json.rs @@ -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, diff --git a/crates/oxc_diagnostics/src/reporter/mod.rs b/crates/oxc_diagnostics/src/reporter/mod.rs index faae0168e82f8..469ee6f8127f5 100644 --- a/crates/oxc_diagnostics/src/reporter/mod.rs +++ b/crates/oxc_diagnostics/src/reporter/mod.rs @@ -1,3 +1,5 @@ +//! [Reporters](DiagnosticReporter) for rendering and writing diagnostics. + mod checkstyle; mod github; mod graphical; @@ -18,9 +20,73 @@ fn writer() -> BufWriter { 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, +/// } +/// +/// 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 { +/// 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; } diff --git a/crates/oxc_diagnostics/src/service.rs b/crates/oxc_diagnostics/src/service.rs index d3349bb5a8df9..e314d2f0a5092 100644 --- a/crates/oxc_diagnostics/src/service.rs +++ b/crates/oxc_diagnostics/src/service.rs @@ -16,6 +16,38 @@ pub type DiagnosticTuple = (PathBuf, Vec); pub type DiagnosticSender = mpsc::Sender>; pub type DiagnosticReceiver = mpsc::Receiver>; +/// Listens for diagnostics sent over a [channel](DiagnosticSender) by some job, and +/// formats/reports them to the user. +/// +/// [`DiagnosticService`] is designed to support multi-threaded jobs that may produce +/// reports. These jobs can send [messages](DiagnosticTuple) to the service over its +/// multi-producer, single-consumer [channel](DiagnosticService::sender). +/// +/// # Example +/// ```rust +/// use std::thread; +/// use oxc_diagnostics::{Error, OxcDiagnostic, DiagnosticService}; +/// +/// // By default, services will pretty-print diagnostics to the console +/// let mut service = DiagnosticService::default(); +/// // Get a clone of the sender to send diagnostics to the service +/// let mut sender = service.sender().clone(); +/// +/// // Spawn a thread that does work and reports diagnostics +/// thread::spawn(move || { +/// sender.send(Some(( +/// PathBuf::from("file.txt"), +/// vec![Error::new(OxcDiagnostic::error("Something went wrong"))], +/// ))); +/// +/// // Send `None` to have the service stop listening for messages. +/// // If you don't ever send `None`, the service will poll forever. +/// sender.send(None); +/// }); +/// +/// // Listen for and process messages +/// service.run() +/// ``` pub struct DiagnosticService { reporter: Box, @@ -41,9 +73,20 @@ pub struct DiagnosticService { impl Default for DiagnosticService { fn default() -> Self { + Self::new(GraphicalReporter::default()) + } +} + +impl DiagnosticService { + /// Create a new [`DiagnosticService`] that will render and report diagnostics using the + /// provided [`DiagnosticReporter`]. + /// + /// TODO(@DonIsaac): make `DiagnosticReporter` public so oxc consumers can create their own + /// implementations. + pub(crate) fn new(reporter: R) -> Self { let (sender, receiver) = mpsc::channel(); Self { - reporter: Box::::default(), + reporter: Box::new(reporter) as Box, quiet: false, silent: false, max_warnings: None, @@ -53,9 +96,8 @@ impl Default for DiagnosticService { receiver, } } -} -impl DiagnosticService { + /// Configure this service to format reports as a JSON array of objects. pub fn set_json_reporter(&mut self) { self.reporter = Box::::default(); } @@ -68,49 +110,83 @@ impl DiagnosticService { self.reporter = Box::::default(); } + /// Configure this service to formats reports using [GitHub Actions + /// annotations](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message). pub fn set_github_reporter(&mut self) { self.reporter = Box::::default(); } + /// Set to `true` to only report errors and ignore warnings. + /// + /// Use [`with_silent`](DiagnosticService::with_silent) to disable reporting entirely. + /// + /// Default: `false` #[must_use] pub fn with_quiet(mut self, yes: bool) -> Self { self.quiet = yes; self } + /// Set to `true` to disable reporting entirely. + /// + /// Use [`with_quiet`](DiagnosticService::with_quiet) to only disable reporting on warnings. + /// + /// Default is `false`. #[must_use] pub fn with_silent(mut self, yes: bool) -> Self { self.silent = yes; self } + /// Specify a warning threshold, which can be used to force exit with an error status if there + /// are too many warning-level rule violations in your project. Errors do not count towards the + /// warning limit. + /// + /// Use [`max_warnings_exceeded`](DiagnosticService::max_warnings_exceeded) to check if too + /// many warnings have been received. + /// + /// Default: [`None`] #[must_use] pub fn with_max_warnings(mut self, max_warnings: Option) -> Self { self.max_warnings = max_warnings; self } + /// Channel for sending [diagnostic messages] to the service. + /// + /// The service will only start processing diagnostics after [`run`](DiagnosticService::run) + /// has been called. + /// + /// [diagnostics]: DiagnosticTuple pub fn sender(&self) -> &DiagnosticSender { &self.sender } + /// Get the number of warning-level diagnostics received. pub fn warnings_count(&self) -> usize { self.warnings_count.get() } + /// Get the number of error-level diagnostics received. pub fn errors_count(&self) -> usize { self.errors_count.get() } + /// Check if the max warning threshold, as set by + /// [`with_max_warnings`](DiagnosticService::with_max_warnings), has been exceeded. pub fn max_warnings_exceeded(&self) -> bool { self.max_warnings.map_or(false, |max_warnings| self.warnings_count.get() > max_warnings) } - pub fn wrap_diagnostics( - path: &Path, + /// Wrap [diagnostics] with the source code and path, converting them into [Error]s. + /// + /// [diagnostics]: OxcDiagnostic + pub fn wrap_diagnostics>( + path: P, source_text: &str, diagnostics: Vec, ) -> (PathBuf, Vec) { + let path = path.as_ref(); let source = Arc::new(NamedSource::new(path.to_string_lossy(), source_text.to_owned())); let diagnostics = diagnostics .into_iter()