refactor(cli,diagnostics): implement DiagnosticService (#762)

This commit is contained in:
Boshen 2023-08-19 18:18:09 +08:00 committed by GitHub
parent e7c2313817
commit 275124068b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 182 additions and 105 deletions

View file

@ -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<WarningOptions>,
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 {
pub(super) fn new(warning_options: Arc<WarningOptions>, linter: Arc<Linter>) -> Self {
Self { warning_options, linter }
pub(super) fn new(warning_options: &WarningOptions, linter: Arc<Linter>) -> 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<Error>)>();
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<AtomicUsize>,
tx_error: mpsc::Sender<(PathBuf, Vec<Error>)>,
) {
fn process_paths(&self, walk: Walk, number_of_files: &Arc<AtomicUsize>) {
let (tx_path, rx_path) = mpsc::channel::<Box<Path>>();
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<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>)> {
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<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)
Some(DiagnosticService::wrap_diagnostics(path, &source_text, errors))
}
}

View file

@ -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();

View file

@ -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<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);

View 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();
}
}