mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 04:42:10 +00:00
refactor(cli,diagnostics): implement DiagnosticService (#762)
This commit is contained in:
parent
e7c2313817
commit
275124068b
4 changed files with 182 additions and 105 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
io::{BufWriter, Write},
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
|
|
@ -9,13 +8,8 @@ use std::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use miette::NamedSource;
|
|
||||||
use oxc_allocator::Allocator;
|
use oxc_allocator::Allocator;
|
||||||
use oxc_diagnostics::{
|
use oxc_diagnostics::{DiagnosticService, Error};
|
||||||
miette::{self, Diagnostic},
|
|
||||||
thiserror::Error,
|
|
||||||
Error, GraphicalReportHandler, Severity,
|
|
||||||
};
|
|
||||||
use oxc_linter::{Fixer, LintContext, Linter};
|
use oxc_linter::{Fixer, LintContext, Linter};
|
||||||
use oxc_parser::Parser;
|
use oxc_parser::Parser;
|
||||||
use oxc_semantic::SemanticBuilder;
|
use oxc_semantic::SemanticBuilder;
|
||||||
|
|
@ -24,19 +18,16 @@ use oxc_span::SourceType;
|
||||||
use crate::{CliRunResult, Walk, WarningOptions};
|
use crate::{CliRunResult, Walk, WarningOptions};
|
||||||
|
|
||||||
pub struct IsolatedLintHandler {
|
pub struct IsolatedLintHandler {
|
||||||
warning_options: Arc<WarningOptions>,
|
|
||||||
|
|
||||||
linter: Arc<Linter>,
|
linter: Arc<Linter>,
|
||||||
|
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 {
|
impl IsolatedLintHandler {
|
||||||
pub(super) fn new(warning_options: Arc<WarningOptions>, linter: Arc<Linter>) -> Self {
|
pub(super) fn new(warning_options: &WarningOptions, linter: Arc<Linter>) -> Self {
|
||||||
Self { warning_options, linter }
|
let diagnostic_service = DiagnosticService::default()
|
||||||
|
.with_quiet(warning_options.quiet)
|
||||||
|
.with_max_warnings(warning_options.max_warnings);
|
||||||
|
Self { linter, diagnostic_service }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Panics
|
/// # Panics
|
||||||
|
|
@ -46,30 +37,21 @@ impl IsolatedLintHandler {
|
||||||
let now = std::time::Instant::now();
|
let now = std::time::Instant::now();
|
||||||
|
|
||||||
let number_of_files = Arc::new(AtomicUsize::new(0));
|
let number_of_files = Arc::new(AtomicUsize::new(0));
|
||||||
let (tx_error, rx_error) = mpsc::channel::<(PathBuf, Vec<Error>)>();
|
|
||||||
|
|
||||||
self.process_paths(walk, &number_of_files, tx_error);
|
self.process_paths(walk, &number_of_files);
|
||||||
let (number_of_warnings, number_of_errors) = self.process_diagnostics(&rx_error);
|
self.diagnostic_service.run();
|
||||||
|
|
||||||
CliRunResult::LintResult {
|
CliRunResult::LintResult {
|
||||||
duration: now.elapsed(),
|
duration: now.elapsed(),
|
||||||
number_of_rules: self.linter.number_of_rules(),
|
number_of_rules: self.linter.number_of_rules(),
|
||||||
number_of_files: number_of_files.load(Ordering::Relaxed),
|
number_of_files: number_of_files.load(Ordering::Relaxed),
|
||||||
number_of_warnings,
|
number_of_warnings: self.diagnostic_service.warnings_count(),
|
||||||
number_of_errors,
|
number_of_errors: self.diagnostic_service.errors_count(),
|
||||||
max_warnings_exceeded: self
|
max_warnings_exceeded: self.diagnostic_service.max_warnings_exceeded(),
|
||||||
.warning_options
|
|
||||||
.max_warnings
|
|
||||||
.map_or(false, |max_warnings| number_of_warnings > max_warnings),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_paths(
|
fn process_paths(&self, walk: Walk, number_of_files: &Arc<AtomicUsize>) {
|
||||||
&self,
|
|
||||||
walk: Walk,
|
|
||||||
number_of_files: &Arc<AtomicUsize>,
|
|
||||||
tx_error: mpsc::Sender<(PathBuf, Vec<Error>)>,
|
|
||||||
) {
|
|
||||||
let (tx_path, rx_path) = mpsc::channel::<Box<Path>>();
|
let (tx_path, rx_path) = mpsc::channel::<Box<Path>>();
|
||||||
|
|
||||||
let number_of_files = Arc::clone(number_of_files);
|
let number_of_files = Arc::clone(number_of_files);
|
||||||
|
|
@ -82,74 +64,27 @@ impl IsolatedLintHandler {
|
||||||
number_of_files.store(count, Ordering::Relaxed);
|
number_of_files.store(count, Ordering::Relaxed);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut processing = 0;
|
||||||
let linter = Arc::clone(&self.linter);
|
let linter = Arc::clone(&self.linter);
|
||||||
|
let tx_error = self.diagnostic_service.sender().clone();
|
||||||
rayon::spawn(move || {
|
rayon::spawn(move || {
|
||||||
while let Ok(path) = rx_path.recv() {
|
while let Ok(path) = rx_path.recv() {
|
||||||
|
processing += 1;
|
||||||
let tx_error = tx_error.clone();
|
let tx_error = tx_error.clone();
|
||||||
let linter = Arc::clone(&linter);
|
let linter = Arc::clone(&linter);
|
||||||
rayon::spawn(move || {
|
rayon::spawn(move || {
|
||||||
if let Some(diagnostics) = Self::lint_path(&linter, &path) {
|
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<Error>)>,
|
|
||||||
) -> (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<Error>)> {
|
fn lint_path(linter: &Linter, path: &Path) -> Option<(PathBuf, Vec<Error>)> {
|
||||||
let source_text =
|
let source_text =
|
||||||
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path:?}"));
|
fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path:?}"));
|
||||||
|
|
@ -161,7 +96,7 @@ impl IsolatedLintHandler {
|
||||||
.parse();
|
.parse();
|
||||||
|
|
||||||
if !ret.errors.is_empty() {
|
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);
|
let program = allocator.alloc(ret.program);
|
||||||
|
|
@ -172,7 +107,11 @@ impl IsolatedLintHandler {
|
||||||
.build(program);
|
.build(program);
|
||||||
|
|
||||||
if !semantic_ret.errors.is_empty() {
|
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));
|
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();
|
let fix_result = Fixer::new(&source_text, result).fix();
|
||||||
fs::write(path, fix_result.fixed_code.as_bytes()).unwrap();
|
fs::write(path, fix_result.fixed_code.as_bytes()).unwrap();
|
||||||
let errors = fix_result.messages.into_iter().map(|m| m.error).collect();
|
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();
|
let errors = result.into_iter().map(|diagnostic| diagnostic.error).collect();
|
||||||
Some(Self::wrap_diagnostics(path, &source_text, errors))
|
Some(DiagnosticService::wrap_diagnostics(path, &source_text, errors))
|
||||||
}
|
|
||||||
|
|
||||||
fn wrap_diagnostics(
|
|
||||||
path: &Path,
|
|
||||||
source_text: &str,
|
|
||||||
diagnostics: Vec<Error>,
|
|
||||||
) -> (PathBuf, Vec<Error>) {
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,7 @@ impl Runner for LintRunner {
|
||||||
let lint_options =
|
let lint_options =
|
||||||
LintOptions { filter, fix: fix_options.fix, timing: misc_options.timing };
|
LintOptions { filter, fix: fix_options.fix, timing: misc_options.timing };
|
||||||
let linter = Arc::new(Linter::from_options(lint_options));
|
let linter = Arc::new(Linter::from_options(lint_options));
|
||||||
let result =
|
let result = IsolatedLintHandler::new(&warning_options, Arc::clone(&linter)).run(walk);
|
||||||
IsolatedLintHandler::new(Arc::new(warning_options), Arc::clone(&linter)).run(walk);
|
|
||||||
|
|
||||||
linter.print_execution_times_if_enable();
|
linter.print_execution_times_if_enable();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@
|
||||||
|
|
||||||
mod graphic_reporter;
|
mod graphic_reporter;
|
||||||
mod graphical_theme;
|
mod graphical_theme;
|
||||||
|
mod service;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub use crate::service::DiagnosticService;
|
||||||
pub use graphic_reporter::{GraphicalReportHandler, GraphicalTheme};
|
pub use graphic_reporter::{GraphicalReportHandler, GraphicalTheme};
|
||||||
pub use miette;
|
pub use miette;
|
||||||
pub use thiserror;
|
pub use thiserror;
|
||||||
|
|
@ -13,3 +17,11 @@ pub type Severity = miette::Severity;
|
||||||
pub type Report = miette::Report;
|
pub type Report = miette::Report;
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
|
||||||
140
crates/oxc_diagnostics/src/service.rs
Normal file
140
crates/oxc_diagnostics/src/service.rs
Normal file
|
|
@ -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<Error>)>;
|
||||||
|
pub type DiagnosticSender = mpsc::Sender<DiagnosticTuple>;
|
||||||
|
pub type DiagnosticReceiver = mpsc::Receiver<DiagnosticTuple>;
|
||||||
|
|
||||||
|
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<usize>,
|
||||||
|
|
||||||
|
/// Total number of warnings received
|
||||||
|
warnings_count: Cell<usize>,
|
||||||
|
|
||||||
|
/// Total number of errors received
|
||||||
|
errors_count: Cell<usize>,
|
||||||
|
|
||||||
|
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<usize>) -> 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<Error>,
|
||||||
|
) -> (PathBuf, Vec<Error>) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue