diff --git a/crates/oxc_cli/src/lint/isolated_handler.rs b/crates/oxc_cli/src/lint/isolated_handler.rs index 35d2343da..1696ae022 100644 --- a/crates/oxc_cli/src/lint/isolated_handler.rs +++ b/crates/oxc_cli/src/lint/isolated_handler.rs @@ -1,6 +1,5 @@ use std::{ fs, - io::{BufWriter, Write}, path::{Path, PathBuf}, rc::Rc, sync::{ @@ -9,13 +8,8 @@ use std::{ }, }; -use miette::NamedSource; use oxc_allocator::Allocator; -use oxc_diagnostics::{ - miette::{self, Diagnostic}, - thiserror::Error, - Error, GraphicalReportHandler, Severity, -}; +use oxc_diagnostics::{DiagnosticService, Error}; use oxc_linter::{Fixer, LintContext, Linter}; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; @@ -24,19 +18,16 @@ use oxc_span::SourceType; use crate::{CliRunResult, Walk, WarningOptions}; pub struct IsolatedLintHandler { - warning_options: Arc, - linter: Arc, + diagnostic_service: DiagnosticService, } -#[derive(Debug, Error, Diagnostic)] -#[error("File is too long to fit on the screen")] -#[diagnostic(help("{0:?} seems like a minified file"))] -pub struct MinifiedFileError(pub PathBuf); - impl IsolatedLintHandler { - pub(super) fn new(warning_options: Arc, linter: Arc) -> Self { - Self { warning_options, linter } + pub(super) fn new(warning_options: &WarningOptions, linter: Arc) -> Self { + let diagnostic_service = DiagnosticService::default() + .with_quiet(warning_options.quiet) + .with_max_warnings(warning_options.max_warnings); + Self { linter, diagnostic_service } } /// # Panics @@ -46,30 +37,21 @@ impl IsolatedLintHandler { let now = std::time::Instant::now(); let number_of_files = Arc::new(AtomicUsize::new(0)); - let (tx_error, rx_error) = mpsc::channel::<(PathBuf, Vec)>(); - self.process_paths(walk, &number_of_files, tx_error); - let (number_of_warnings, number_of_errors) = self.process_diagnostics(&rx_error); + self.process_paths(walk, &number_of_files); + self.diagnostic_service.run(); CliRunResult::LintResult { duration: now.elapsed(), number_of_rules: self.linter.number_of_rules(), number_of_files: number_of_files.load(Ordering::Relaxed), - number_of_warnings, - number_of_errors, - max_warnings_exceeded: self - .warning_options - .max_warnings - .map_or(false, |max_warnings| number_of_warnings > max_warnings), + number_of_warnings: self.diagnostic_service.warnings_count(), + number_of_errors: self.diagnostic_service.errors_count(), + max_warnings_exceeded: self.diagnostic_service.max_warnings_exceeded(), } } - fn process_paths( - &self, - walk: Walk, - number_of_files: &Arc, - tx_error: mpsc::Sender<(PathBuf, Vec)>, - ) { + fn process_paths(&self, walk: Walk, number_of_files: &Arc) { let (tx_path, rx_path) = mpsc::channel::>(); let number_of_files = Arc::clone(number_of_files); @@ -82,74 +64,27 @@ impl IsolatedLintHandler { number_of_files.store(count, Ordering::Relaxed); }); + let mut processing = 0; let linter = Arc::clone(&self.linter); + let tx_error = self.diagnostic_service.sender().clone(); rayon::spawn(move || { while let Ok(path) = rx_path.recv() { + processing += 1; let tx_error = tx_error.clone(); let linter = Arc::clone(&linter); rayon::spawn(move || { if let Some(diagnostics) = Self::lint_path(&linter, &path) { - tx_error.send(diagnostics).unwrap(); + tx_error.send(Some(diagnostics)).unwrap(); + } + processing -= 1; + if processing == 0 { + tx_error.send(None).unwrap(); } - drop(tx_error); }); } }); } - fn process_diagnostics( - &self, - rx_error: &mpsc::Receiver<(PathBuf, Vec)>, - ) -> (usize, usize) { - let mut number_of_warnings = 0; - let mut number_of_errors = 0; - let mut buf_writer = BufWriter::new(std::io::stdout()); - let handler = GraphicalReportHandler::new(); - - while let Ok((path, diagnostics)) = rx_error.recv() { - let mut output = String::new(); - for diagnostic in diagnostics { - let severity = diagnostic.severity(); - let is_warning = severity == Some(Severity::Warning); - let is_error = severity.is_none() || severity == Some(Severity::Error); - if is_warning || is_error { - if is_warning { - number_of_warnings += 1; - } - if is_error { - number_of_errors += 1; - } - // The --quiet flag follows ESLint's --quiet behavior as documented here: https://eslint.org/docs/latest/use/command-line-interface#--quiet - // Note that it does not disable ALL diagnostics, only Warning diagnostics - if self.warning_options.quiet { - continue; - } - - if let Some(max_warnings) = self.warning_options.max_warnings { - if number_of_warnings > max_warnings { - continue; - } - } - } - - let mut err = String::new(); - handler.render_report(&mut err, diagnostic.as_ref()).unwrap(); - // Skip large output and print only once - if err.lines().any(|line| line.len() >= 400) { - let minified_diagnostic = Error::new(MinifiedFileError(path.clone())); - err = format!("{minified_diagnostic:?}"); - output = err; - break; - } - output.push_str(&err); - } - buf_writer.write_all(output.as_bytes()).unwrap(); - } - - buf_writer.flush().unwrap(); - (number_of_warnings, number_of_errors) - } - fn lint_path(linter: &Linter, path: &Path) -> Option<(PathBuf, Vec)> { let source_text = fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path:?}")); @@ -161,7 +96,7 @@ impl IsolatedLintHandler { .parse(); if !ret.errors.is_empty() { - return Some(Self::wrap_diagnostics(path, &source_text, ret.errors)); + return Some(DiagnosticService::wrap_diagnostics(path, &source_text, ret.errors)); }; let program = allocator.alloc(ret.program); @@ -172,7 +107,11 @@ impl IsolatedLintHandler { .build(program); if !semantic_ret.errors.is_empty() { - return Some(Self::wrap_diagnostics(path, &source_text, semantic_ret.errors)); + return Some(DiagnosticService::wrap_diagnostics( + path, + &source_text, + semantic_ret.errors, + )); }; let lint_ctx = LintContext::new(&Rc::new(semantic_ret.semantic)); @@ -186,23 +125,10 @@ impl IsolatedLintHandler { let fix_result = Fixer::new(&source_text, result).fix(); fs::write(path, fix_result.fixed_code.as_bytes()).unwrap(); let errors = fix_result.messages.into_iter().map(|m| m.error).collect(); - return Some(Self::wrap_diagnostics(path, &source_text, errors)); + return Some(DiagnosticService::wrap_diagnostics(path, &source_text, errors)); } let errors = result.into_iter().map(|diagnostic| diagnostic.error).collect(); - Some(Self::wrap_diagnostics(path, &source_text, errors)) - } - - fn wrap_diagnostics( - path: &Path, - source_text: &str, - diagnostics: Vec, - ) -> (PathBuf, Vec) { - let source = Arc::new(NamedSource::new(path.to_string_lossy(), source_text.to_owned())); - let diagnostics = diagnostics - .into_iter() - .map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source))) - .collect(); - (path.to_path_buf(), diagnostics) + Some(DiagnosticService::wrap_diagnostics(path, &source_text, errors)) } } diff --git a/crates/oxc_cli/src/lint/mod.rs b/crates/oxc_cli/src/lint/mod.rs index 8e2bccfac..478fa7c2d 100644 --- a/crates/oxc_cli/src/lint/mod.rs +++ b/crates/oxc_cli/src/lint/mod.rs @@ -42,8 +42,7 @@ impl Runner for LintRunner { let lint_options = LintOptions { filter, fix: fix_options.fix, timing: misc_options.timing }; let linter = Arc::new(Linter::from_options(lint_options)); - let result = - IsolatedLintHandler::new(Arc::new(warning_options), Arc::clone(&linter)).run(walk); + let result = IsolatedLintHandler::new(&warning_options, Arc::clone(&linter)).run(walk); linter.print_execution_times_if_enable(); diff --git a/crates/oxc_diagnostics/src/lib.rs b/crates/oxc_diagnostics/src/lib.rs index 58fa199bf..75ebe5dfb 100644 --- a/crates/oxc_diagnostics/src/lib.rs +++ b/crates/oxc_diagnostics/src/lib.rs @@ -3,7 +3,11 @@ mod graphic_reporter; mod graphical_theme; +mod service; +use std::path::PathBuf; + +pub use crate::service::DiagnosticService; pub use graphic_reporter::{GraphicalReportHandler, GraphicalTheme}; pub use miette; pub use thiserror; @@ -13,3 +17,11 @@ pub type Severity = miette::Severity; pub type Report = miette::Report; pub type Result = std::result::Result; + +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Debug, Error, Diagnostic)] +#[error("File is too long to fit on the screen")] +#[diagnostic(help("{0:?} seems like a minified file"))] +pub struct MinifiedFileError(pub PathBuf); diff --git a/crates/oxc_diagnostics/src/service.rs b/crates/oxc_diagnostics/src/service.rs new file mode 100644 index 000000000..ec871990e --- /dev/null +++ b/crates/oxc_diagnostics/src/service.rs @@ -0,0 +1,140 @@ +use std::{ + cell::Cell, + io::{BufWriter, Write}, + path::{Path, PathBuf}, + sync::mpsc, + sync::Arc, +}; + +use crate::{miette::NamedSource, Error, GraphicalReportHandler, MinifiedFileError, Severity}; + +pub type DiagnosticTuple = Option<(PathBuf, Vec)>; +pub type DiagnosticSender = mpsc::Sender; +pub type DiagnosticReceiver = mpsc::Receiver; + +pub struct DiagnosticService { + /// Disable reporting on warnings, only errors are reported + quiet: bool, + + /// 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 + max_warnings: Option, + + /// Total number of warnings received + warnings_count: Cell, + + /// Total number of errors received + errors_count: Cell, + + sender: DiagnosticSender, + receiver: DiagnosticReceiver, +} + +impl Default for DiagnosticService { + fn default() -> Self { + let (sender, receiver) = mpsc::channel(); + Self { + quiet: false, + max_warnings: None, + warnings_count: Cell::new(0), + errors_count: Cell::new(0), + sender, + receiver, + } + } +} + +impl DiagnosticService { + #[must_use] + pub fn with_quiet(mut self, yes: bool) -> Self { + self.quiet = yes; + self + } + + #[must_use] + pub fn with_max_warnings(mut self, max_warnings: Option) -> Self { + self.max_warnings = max_warnings; + self + } + + pub fn sender(&self) -> &DiagnosticSender { + &self.sender + } + + pub fn warnings_count(&self) -> usize { + self.warnings_count.get() + } + + pub fn errors_count(&self) -> usize { + self.errors_count.get() + } + + 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, + source_text: &str, + diagnostics: Vec, + ) -> (PathBuf, Vec) { + let source = Arc::new(NamedSource::new(path.to_string_lossy(), source_text.to_owned())); + let diagnostics = diagnostics + .into_iter() + .map(|diagnostic| diagnostic.with_source_code(Arc::clone(&source))) + .collect(); + (path.to_path_buf(), diagnostics) + } + + /// # Panics + /// + /// * When the writer fails to write + pub fn run(&self) { + let mut buf_writer = BufWriter::new(std::io::stdout()); + let handler = GraphicalReportHandler::new(); + + while let Ok(Some((path, diagnostics))) = self.receiver.recv() { + let mut output = String::new(); + for diagnostic in diagnostics { + let severity = diagnostic.severity(); + let is_warning = severity == Some(Severity::Warning); + let is_error = severity.is_none() || severity == Some(Severity::Error); + if is_warning || is_error { + if is_warning { + let warnings_count = self.warnings_count() + 1; + self.warnings_count.set(warnings_count); + } + if is_error { + let errors_count = self.errors_count() + 1; + self.errors_count.set(errors_count); + } + // The --quiet flag follows ESLint's --quiet behavior as documented here: https://eslint.org/docs/latest/use/command-line-interface#--quiet + // Note that it does not disable ALL diagnostics, only Warning diagnostics + if self.quiet { + continue; + } + + if let Some(max_warnings) = self.max_warnings { + if self.warnings_count() > max_warnings { + continue; + } + } + } + + let mut err = String::new(); + handler.render_report(&mut err, diagnostic.as_ref()).unwrap(); + // Skip large output and print only once + if err.lines().any(|line| line.len() >= 400) { + let minified_diagnostic = Error::new(MinifiedFileError(path.clone())); + err = format!("{minified_diagnostic:?}"); + output = err; + break; + } + output.push_str(&err); + } + buf_writer.write_all(output.as_bytes()).unwrap(); + } + + buf_writer.flush().unwrap(); + } +}