oxc_diagnostics 0.155.0

A collection of JavaScript tools written in Rust.
Documentation
//! [Reporters](DiagnosticReporter) for rendering and writing diagnostics.

use crate::{Error, Severity};

/// 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
/// ```rust,ignore
/// use oxc_diagnostics::{DiagnosticReporter, Error, Severity};
///
/// #[derive(Default)]
/// pub struct BufferedReporter;
///
/// impl DiagnosticReporter for BufferedReporter {
///     // render the finished output, some reporters will store the errors in memory
///     // to output all diagnostics at the end
///     fn finish(&mut self) -> Option<String> {
///         None
///     }
///
///     // 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.
    ///
    /// 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.
    fn finish(&mut self, result: &DiagnosticResult) -> Option<String>;

    /// Whether [`DiagnosticService`](crate::service::DiagnosticService) should replace very long
    /// rendered lines with the synthetic "minified file" warning.
    ///
    /// Human-readable reporters generally want this behavior to avoid dumping unreadable output.
    /// Machine-readable or intentionally single-line reporters should disable it.
    fn supports_minified_file_fallback(&self) -> bool {
        true
    }

    /// Render diagnostics in order, delegating to [`render_error`](Self::render_error) by default.
    fn render_errors(&mut self, errors: Vec<Error>, emit: &mut dyn FnMut(&str)) {
        for error in errors {
            if let Some(rendered) = self.render_error(error) {
                emit(&rendered);
            }
        }
    }

    /// Render diagnostics until `keep` rejects a rendered report.
    fn render_errors_until(
        &mut self,
        errors: Vec<Error>,
        keep: &mut dyn FnMut(Option<&str>, &str) -> bool,
    ) {
        for error in errors {
            let source_name =
                error.source_code().and_then(|source| source.name()).map(ToString::to_string);
            if let Some(rendered) = self.render_error(error)
                && !keep(source_name.as_deref(), &rendered)
            {
                break;
            }
        }
    }

    /// 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 use this method to write diagnostics to their output stream.
    fn render_error(&mut self, error: Error) -> Option<String>;
}

/// DiagnosticResult will be submitted to the Reporter when the [`DiagnosticService`](crate::service::DiagnosticService)
/// is finished receiving all files
#[derive(Default, Debug)]
pub struct DiagnosticResult {
    /// Total number of warnings received
    warnings_count: usize,

    /// Total number of errors received
    errors_count: usize,

    /// Did the threshold for warnings exceeded the max_warnings?
    /// ToDo: We giving the input from outside, let the owner calculate the result
    max_warnings_exceeded: bool,
}

impl DiagnosticResult {
    pub fn new(warnings_count: usize, errors_count: usize, max_warnings_exceeded: bool) -> Self {
        Self { warnings_count, errors_count, max_warnings_exceeded }
    }

    /// Get the number of warning-level diagnostics received.
    pub fn warnings_count(&self) -> usize {
        self.warnings_count
    }

    /// Get the number of error-level diagnostics received.
    pub fn errors_count(&self) -> usize {
        self.errors_count
    }

    /// Did the threshold for warnings exceeded the max_warnings?
    pub fn max_warnings_exceeded(&self) -> bool {
        self.max_warnings_exceeded
    }
}

#[derive(Debug)]
pub struct Info {
    pub start: InfoPosition,
    pub end: InfoPosition,
    pub filename: String,
    pub message: String,
    pub severity: Severity,
    pub rule_id: Option<String>,
}

#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct InfoPosition {
    pub line: usize,
    pub column: usize,
}

fn line_column(data: &[u8], mut offset: usize) -> Option<InfoPosition> {
    if offset > data.len() {
        return None;
    }
    if offset > 0 && offset < data.len() && data[offset - 1] == b'\r' && data[offset] == b'\n' {
        offset -= 1;
    }

    let mut line = 1;
    let mut line_start = 0;
    for index in memchr::memchr2_iter(b'\r', b'\n', &data[..offset]) {
        if data[index] == b'\n' && index > 0 && data[index - 1] == b'\r' {
            continue;
        }
        line += 1;
        line_start = if data[index] == b'\r' && index + 1 < offset && data[index + 1] == b'\n' {
            index + 2
        } else {
            index + 1
        };
    }
    Some(InfoPosition { line, column: offset - line_start + 1 })
}

impl Info {
    pub fn new(diagnostic: &Error) -> Self {
        let mut start = InfoPosition { line: 0, column: 0 };
        let mut end = InfoPosition { line: 0, column: 0 };
        let mut filename = String::new();
        let mut message = String::new();
        let mut severity = Severity::Warning;
        let rule_id = diagnostic.code().map(|code| code.to_string());

        if let Some(source) = diagnostic.source_code() {
            if let Some(name) = source.name() {
                filename = name.to_string();
            }

            if let Some(label) = diagnostic.labels().first()
                && let Ok(start_offset) = usize::try_from(label.offset())
                && let Some(end_offset) = label.offset().checked_add(label.len())
                && let Ok(end_offset) = usize::try_from(end_offset)
                && source.data().get(start_offset..end_offset).is_some()
                && let Some(start_position) = line_column(source.data(), start_offset)
            {
                start = start_position;
                if let Some(end_position) = line_column(source.data(), end_offset) {
                    end = end_position;
                }

                if matches!(diagnostic.severity(), Some(Severity::Error)) {
                    severity = Severity::Error;
                }

                message = diagnostic.to_string();
            }
        }

        Self { start, end, filename, message, severity, rule_id }
    }
}

#[cfg(test)]
mod test {
    use crate::{Error, NamedSource, OxcDiagnostic};

    use super::Info;

    #[test]
    fn includes_source_name_for_unlabelled_diagnostic() {
        let error: Error = OxcDiagnostic::error("Something went wrong")
            .with_source_code(NamedSource::new("file.js", ""));

        assert_eq!(Info::new(&error).filename, "file.js");
    }
}