docs(diagnostics): fully document oxc_diagnostics (#5865)

Just a step in my mission to document our entire API
This commit is contained in:
DonIsaac 2024-09-19 01:43:08 +00:00
parent 4e37d18b2e
commit 83ca7f56cd
8 changed files with 301 additions and 7 deletions

View file

@ -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>,

View file

@ -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 {

View file

@ -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<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 {
@ -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()
@ -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 {
@ -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 {
@ -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,
@ -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 {
@ -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 {
@ -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,
@ -201,6 +313,7 @@ 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());
@ -208,6 +321,7 @@ impl OxcDiagnostic {
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,
@ -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)
}

View file

@ -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>,
}

View file

@ -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>,

View file

@ -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>,

View file

@ -1,3 +1,5 @@
//! [Reporters](DiagnosticReporter) for rendering and writing diagnostics.
mod checkstyle;
mod github;
mod graphical;
@ -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>;
}

View file

@ -16,6 +16,38 @@ pub type DiagnosticTuple = (PathBuf, Vec<Error>);
pub type DiagnosticSender = mpsc::Sender<Option<DiagnosticTuple>>;
pub type DiagnosticReceiver = mpsc::Receiver<Option<DiagnosticTuple>>;
/// 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<dyn DiagnosticReporter>,
@ -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<R: DiagnosticReporter + 'static>(reporter: R) -> Self {
let (sender, receiver) = mpsc::channel();
Self {
reporter: Box::<GraphicalReporter>::default(),
reporter: Box::new(reporter) as Box<dyn DiagnosticReporter>,
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::<JsonReporter>::default();
}
@ -68,49 +110,83 @@ impl DiagnosticService {
self.reporter = Box::<CheckstyleReporter>::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::<GithubReporter>::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<usize>) -> 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<P: AsRef<Path>>(
path: P,
source_text: &str,
diagnostics: Vec<OxcDiagnostic>,
) -> (PathBuf, Vec<Error>) {
let path = path.as_ref();
let source = Arc::new(NamedSource::new(path.to_string_lossy(), source_text.to_owned()));
let diagnostics = diagnostics
.into_iter()