use std::{
io::{ErrorKind, Write},
path::{Path, PathBuf},
sync::{Arc, mpsc},
};
use cow_utils::CowUtils;
use crate::{
Error, NamedSource, OxcDiagnostic, Severity,
reporter::{DiagnosticReporter, DiagnosticResult},
};
pub type DiagnosticTuple = (PathBuf, Vec<Error>);
pub type DiagnosticSender = mpsc::Sender<Option<DiagnosticTuple>>;
pub type DiagnosticReceiver = mpsc::Receiver<Option<DiagnosticTuple>>;
pub struct DiagnosticService {
reporter: Box<dyn DiagnosticReporter>,
quiet: bool,
silent: bool,
max_warnings: Option<usize>,
sender: DiagnosticSender,
receiver: DiagnosticReceiver,
}
impl DiagnosticService {
pub fn new(reporter: Box<dyn DiagnosticReporter>) -> Self {
let (sender, receiver) = mpsc::channel();
Self { reporter, quiet: false, silent: false, max_warnings: None, sender, receiver }
}
#[must_use]
pub fn with_quiet(mut self, yes: bool) -> Self {
self.quiet = yes;
self
}
#[must_use]
pub fn with_silent(mut self, yes: bool) -> Self {
self.silent = yes;
self
}
#[must_use]
pub fn with_max_warnings(mut self, max_warnings: Option<usize>) -> Self {
self.max_warnings = max_warnings;
self
}
pub fn sender(&self) -> &DiagnosticSender {
&self.sender
}
fn max_warnings_exceeded(&self, warnings_count: usize) -> bool {
self.max_warnings.is_some_and(|max_warnings| warnings_count > max_warnings)
}
pub fn wrap_diagnostics<P: AsRef<Path>>(
path: P,
source_text: &str,
diagnostics: Vec<OxcDiagnostic>,
) -> (PathBuf, Vec<Error>) {
let path = path.as_ref();
let path_display = path.to_string_lossy();
let path_display = path_display.cow_replace('\\', "/");
let source = Arc::new(NamedSource::new(path_display, source_text.to_owned()));
let diagnostics = diagnostics
.into_iter()
.map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source)))
.collect();
(path.to_path_buf(), diagnostics)
}
pub fn run(&mut self, writer: &mut dyn Write) -> DiagnosticResult {
let mut warnings_count: usize = 0;
let mut errors_count: usize = 0;
while let Ok(Some((path, diagnostics))) = self.receiver.recv() {
for diagnostic in diagnostics {
let severity = diagnostic.severity();
let is_warning = severity == Some(Severity::Warning);
let is_error = severity == Some(Severity::Error) || severity.is_none();
if is_warning || is_error {
if is_warning {
warnings_count += 1;
}
if is_error {
errors_count += 1;
}
else if self.quiet {
continue;
}
}
if self.silent {
continue;
}
if let Some(err_str) = self.reporter.render_error(diagnostic) {
if err_str.lines().any(|line| line.len() >= 1200) {
let minified_diagnostic = Error::new(
OxcDiagnostic::warn("File is too long to fit on the screen")
.with_help(format!("{path:?} seems like a minified file")),
);
if let Some(err_str) = self.reporter.render_error(minified_diagnostic) {
writer
.write_all(err_str.as_bytes())
.or_else(Self::check_for_writer_error)
.unwrap();
}
break;
}
writer
.write_all(err_str.as_bytes())
.or_else(Self::check_for_writer_error)
.unwrap();
}
}
}
let result = DiagnosticResult::new(
warnings_count,
errors_count,
self.max_warnings_exceeded(warnings_count),
);
if let Some(finish_output) = self.reporter.finish(&result) {
writer
.write_all(finish_output.as_bytes())
.or_else(Self::check_for_writer_error)
.unwrap();
}
writer.flush().or_else(Self::check_for_writer_error).unwrap();
result
}
fn check_for_writer_error(error: std::io::Error) -> Result<(), std::io::Error> {
if matches!(error.kind(), ErrorKind::Interrupted | ErrorKind::BrokenPipe) {
Ok(())
} else {
Err(error)
}
}
}