diff --git a/Cargo.lock b/Cargo.lock index 96278d3fb0138..083c1fa37681b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1694,7 +1694,6 @@ name = "oxc_diagnostics" version = "0.46.0" dependencies = [ "oxc-miette", - "rustc-hash", ] [[package]] @@ -2222,6 +2221,7 @@ dependencies = [ "oxc_linter", "oxc_span", "rayon", + "rustc-hash", "serde", "serde_json", "tempfile", diff --git a/apps/oxlint/Cargo.toml b/apps/oxlint/Cargo.toml index f1387b0a4bbd5..727fa52e82a20 100644 --- a/apps/oxlint/Cargo.toml +++ b/apps/oxlint/Cargo.toml @@ -39,6 +39,7 @@ bpaf = { workspace = true, features = ["autocomplete", "bright-color", "derive"] ignore = { workspace = true, features = ["simd-accel"] } miette = { workspace = true } rayon = { workspace = true } +rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tempfile = { workspace = true } diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index c3f2400ab0d38..62d6fbd969d47 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -1,12 +1,11 @@ use std::{ env, fs, - io::BufWriter, + io::{BufWriter, Write}, path::{Path, PathBuf}, time::Instant, }; use ignore::gitignore::Gitignore; - use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler}; use oxc_linter::{ loader::LINT_PARTIAL_LOADER_EXT, AllowWarnDeny, ConfigStoreBuilder, InvalidFilterKind, @@ -15,9 +14,7 @@ use oxc_linter::{ use oxc_span::VALID_EXTENSIONS; use crate::{ - cli::{ - CliRunResult, LintCommand, LintResult, MiscOptions, OutputOptions, Runner, WarningOptions, - }, + cli::{CliRunResult, LintCommand, LintResult, MiscOptions, Runner, WarningOptions}, output_formatter::{OutputFormat, OutputFormatter}, walk::{Extensions, Walk}, }; @@ -37,11 +34,15 @@ impl Runner for LintRunner { fn run(self) -> CliRunResult { let format_str = self.options.output_options.format; - let output_formatter = OutputFormatter::new(format_str); + let mut output_formatter = OutputFormatter::new(format_str); + + // stdio is blocked by LineWriter, use a BufWriter to reduce syscalls. + // See `https://github.com/rust-lang/rust/issues/60673`. + let mut stdout = BufWriter::new(std::io::stdout()); if self.options.list_rules { - let mut stdout = BufWriter::new(std::io::stdout()); output_formatter.all_rules(&mut stdout); + stdout.flush().unwrap(); return CliRunResult::None; } @@ -180,7 +181,7 @@ impl Runner for LintRunner { let lint_service = LintService::new(linter, options); let mut diagnostic_service = - Self::get_diagnostic_service(&warning_options, &output_options, &misc_options); + Self::get_diagnostic_service(&output_formatter, &warning_options, &misc_options); // Spawn linting in another thread so diagnostics can be printed immediately from diagnostic_service.run. rayon::spawn({ @@ -190,7 +191,7 @@ impl Runner for LintRunner { lint_service.run(&tx_error); } }); - diagnostic_service.run(); + diagnostic_service.run(&mut stdout); CliRunResult::LintResult(LintResult { duration: now.elapsed(), @@ -215,23 +216,14 @@ impl LintRunner { } fn get_diagnostic_service( + reporter: &OutputFormatter, warning_options: &WarningOptions, - output_options: &OutputOptions, misc_options: &MiscOptions, ) -> DiagnosticService { - let mut diagnostic_service = DiagnosticService::default() + DiagnosticService::new(reporter.get_diagnostic_reporter()) .with_quiet(warning_options.quiet) .with_silent(misc_options.silent) - .with_max_warnings(warning_options.max_warnings); - - match output_options.format { - OutputFormat::Default => {} - OutputFormat::Json => diagnostic_service.set_json_reporter(), - OutputFormat::Unix => diagnostic_service.set_unix_reporter(), - OutputFormat::Checkstyle => diagnostic_service.set_checkstyle_reporter(), - OutputFormat::Github => diagnostic_service.set_github_reporter(), - } - diagnostic_service + .with_max_warnings(warning_options.max_warnings) } // moved into a separate function for readability, but it's only ever used diff --git a/crates/oxc_diagnostics/src/reporter/checkstyle.rs b/apps/oxlint/src/output_formatter/checkstyle.rs similarity index 66% rename from crates/oxc_diagnostics/src/reporter/checkstyle.rs rename to apps/oxlint/src/output_formatter/checkstyle.rs index ba8ba813af394..05655fb3dfad4 100644 --- a/crates/oxc_diagnostics/src/reporter/checkstyle.rs +++ b/apps/oxlint/src/output_formatter/checkstyle.rs @@ -1,30 +1,47 @@ -use std::borrow::Cow; +use std::{borrow::Cow, io::Write}; use rustc_hash::FxHashMap; -use super::{DiagnosticReporter, Info}; -use crate::{Error, Severity}; +use oxc_diagnostics::{ + reporter::{DiagnosticReporter, Info}, + Error, Severity, +}; +use crate::output_formatter::InternalFormatter; + +#[derive(Debug, Default)] +pub struct CheckStyleOutputFormatter; + +impl InternalFormatter for CheckStyleOutputFormatter { + fn all_rules(&mut self, writer: &mut dyn Write) { + writeln!(writer, "flag --rules with flag --format=checkstyle is not allowed").unwrap(); + } + + fn get_diagnostic_reporter(&self) -> Box { + Box::new(CheckstyleReporter::default()) + } +} + +/// Reporter to output diagnostics in checkstyle format +/// +/// Checkstyle Format Documentation: #[derive(Default)] -pub struct CheckstyleReporter { +struct CheckstyleReporter { diagnostics: Vec, } impl DiagnosticReporter for CheckstyleReporter { - fn finish(&mut self) { - format_checkstyle(&self.diagnostics); + fn finish(&mut self) -> Option { + Some(format_checkstyle(&self.diagnostics)) } - fn render_diagnostics(&mut self, _s: &[u8]) {} - fn render_error(&mut self, error: Error) -> Option { self.diagnostics.push(error); None } } -#[allow(clippy::print_stdout)] -fn format_checkstyle(diagnostics: &[Error]) { +fn format_checkstyle(diagnostics: &[Error]) -> String { let infos = diagnostics.iter().map(Info::new).collect::>(); let mut grouped: FxHashMap> = FxHashMap::default(); for info in infos { @@ -48,9 +65,9 @@ fn format_checkstyle(diagnostics: &[Error]) { let filename = &infos[0].filename; format!(r#"{messages}"#) }).collect::>().join(" "); - println!( + format!( r#"{messages}"# - ); + ) } /// @@ -103,3 +120,31 @@ fn xml_escape_impl bool>(raw: &str, escape_chars: F) -> Cow { Cow::Borrowed(raw) } } + +#[cfg(test)] +mod test { + use oxc_diagnostics::{reporter::DiagnosticReporter, NamedSource, OxcDiagnostic}; + use oxc_span::Span; + + use super::CheckstyleReporter; + + #[test] + fn reporter() { + let mut reporter = CheckstyleReporter::default(); + + let error = OxcDiagnostic::warn("error message") + .with_label(Span::new(0, 8)) + .with_source_code(NamedSource::new("file://test.ts", "debugger;")); + + let first_result = reporter.render_error(error); + + // reporter keeps it in memory + assert!(first_result.is_none()); + + // report not gives us all diagnostics at ones + let second_result = reporter.finish(); + + assert!(second_result.is_some()); + assert_eq!(second_result.unwrap(), ""); + } +} diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs index 37598e40cf1d3..838bf7720243d 100644 --- a/apps/oxlint/src/output_formatter/default.rs +++ b/apps/oxlint/src/output_formatter/default.rs @@ -1,11 +1,15 @@ use std::io::Write; +use oxc_diagnostics::{reporter::DiagnosticReporter, Error, GraphicalReportHandler}; use oxc_linter::table::RuleTable; +use crate::output_formatter::InternalFormatter; + +#[derive(Debug)] pub struct DefaultOutputFormatter; -impl DefaultOutputFormatter { - pub fn all_rules(writer: &mut T) { +impl InternalFormatter for DefaultOutputFormatter { + fn all_rules(&mut self, writer: &mut dyn Write) { let table = RuleTable::new(); for section in table.sections { writeln!(writer, "{}", section.render_markdown_table(None)).unwrap(); @@ -13,17 +17,88 @@ impl DefaultOutputFormatter { writeln!(writer, "Default: {}", table.turned_on_by_default_count).unwrap(); writeln!(writer, "Total: {}", table.total).unwrap(); } + + fn get_diagnostic_reporter(&self) -> Box { + Box::new(GraphicalReporter::default()) + } +} + +/// Pretty-prints diagnostics. Primarily meant for human-readable output in a terminal. +/// +/// See [`GraphicalReportHandler`] for how to configure colors, context lines, etc. +struct GraphicalReporter { + handler: GraphicalReportHandler, +} + +impl Default for GraphicalReporter { + fn default() -> Self { + Self { handler: GraphicalReportHandler::new() } + } +} + +impl DiagnosticReporter for GraphicalReporter { + fn finish(&mut self) -> Option { + None + } + + fn render_error(&mut self, error: Error) -> Option { + let mut output = String::new(); + self.handler.render_report(&mut output, error.as_ref()).unwrap(); + Some(output) + } +} +impl GraphicalReporter { + #[cfg(test)] + pub fn with_handler(mut self, handler: GraphicalReportHandler) -> Self { + self.handler = handler; + self + } } #[cfg(test)] mod test { - use crate::output_formatter::default::DefaultOutputFormatter; + use crate::output_formatter::{ + default::{DefaultOutputFormatter, GraphicalReporter}, + InternalFormatter, + }; + use miette::{GraphicalReportHandler, GraphicalTheme, NamedSource}; + use oxc_diagnostics::{reporter::DiagnosticReporter, OxcDiagnostic}; + use oxc_span::Span; #[test] fn all_rules() { let mut writer = Vec::new(); + let mut formatter = DefaultOutputFormatter; - DefaultOutputFormatter::all_rules(&mut writer); + formatter.all_rules(&mut writer); assert!(!writer.is_empty()); } + + #[test] + fn reporter_finish() { + let mut reporter = GraphicalReporter::default(); + + let result = reporter.finish(); + + assert!(result.is_none()); + } + + #[test] + fn reporter_error() { + let mut reporter = GraphicalReporter::default().with_handler( + GraphicalReportHandler::new_themed(GraphicalTheme::none()).with_links(false), + ); + + let error = OxcDiagnostic::warn("error message") + .with_label(Span::new(0, 8)) + .with_source_code(NamedSource::new("file://test.ts", "debugger;")); + + let result = reporter.render_error(error); + + assert!(result.is_some()); + assert_eq!( + result.unwrap(), + "\n ! error message\n ,-[file://test.ts:1:1]\n 1 | debugger;\n : ^^^^^^^^\n `----\n" + ); + } } diff --git a/crates/oxc_diagnostics/src/reporter/github.rs b/apps/oxlint/src/output_formatter/github.rs similarity index 60% rename from crates/oxc_diagnostics/src/reporter/github.rs rename to apps/oxlint/src/output_formatter/github.rs index 1fd8d818d5b25..d6b9a33481835 100644 --- a/crates/oxc_diagnostics/src/reporter/github.rs +++ b/apps/oxlint/src/output_formatter/github.rs @@ -1,34 +1,36 @@ -use std::{ - borrow::Cow, - io::{BufWriter, Stdout, Write}, +use std::{borrow::Cow, io::Write}; + +use oxc_diagnostics::{ + reporter::{DiagnosticReporter, Info}, + Error, Severity, }; -use super::{writer, DiagnosticReporter, Info}; -use crate::{Error, Severity}; +use crate::output_formatter::InternalFormatter; -/// Formats reports using [GitHub Actions -/// annotations](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message). Useful for reporting in CI. -pub struct GithubReporter { - writer: BufWriter, -} +#[derive(Debug)] +pub struct GithubOutputFormatter; + +impl InternalFormatter for GithubOutputFormatter { + fn all_rules(&mut self, writer: &mut dyn Write) { + writeln!(writer, "flag --rules with flag --format=github is not allowed").unwrap(); + } -impl Default for GithubReporter { - fn default() -> Self { - Self { writer: writer() } + fn get_diagnostic_reporter(&self) -> Box { + Box::new(GithubReporter) } } +/// Formats reports using [GitHub Actions +/// annotations](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message). Useful for reporting in CI. +struct GithubReporter; + impl DiagnosticReporter for GithubReporter { - fn finish(&mut self) { - self.writer.flush().unwrap(); + fn finish(&mut self) -> Option { + None } - fn render_diagnostics(&mut self, _s: &[u8]) {} - fn render_error(&mut self, error: Error) -> Option { - let message = format_github(&error); - self.writer.write_all(message.as_bytes()).unwrap(); - None + Some(format_github(&error)) } } @@ -79,3 +81,33 @@ fn escape_property(value: &str) -> String { } result } + +#[cfg(test)] +mod test { + use oxc_diagnostics::{reporter::DiagnosticReporter, NamedSource, OxcDiagnostic}; + use oxc_span::Span; + + use super::GithubReporter; + + #[test] + fn reporter_finish() { + let mut reporter = GithubReporter; + + let result = reporter.finish(); + + assert!(result.is_none()); + } + + #[test] + fn reporter_error() { + let mut reporter = GithubReporter; + let error = OxcDiagnostic::warn("error message") + .with_label(Span::new(0, 8)) + .with_source_code(NamedSource::new("file://test.ts", "debugger;")); + + let result = reporter.render_error(error); + + assert!(result.is_some()); + assert_eq!(result.unwrap(), "::warning file=file%3A//test.ts,line=1,endLine=1,col=1,endColumn=1,title=oxlint::error message\n"); + } +} diff --git a/apps/oxlint/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs index 1b9b1d50828a8..7e1cdd946f192 100644 --- a/apps/oxlint/src/output_formatter/json.rs +++ b/apps/oxlint/src/output_formatter/json.rs @@ -1,12 +1,18 @@ +use std::io::Write; + +use oxc_diagnostics::{reporter::DiagnosticReporter, Error}; use oxc_linter::rules::RULES; use oxc_linter::RuleCategory; -use std::io::Write; -#[derive(Debug)] +use miette::JSONReportHandler; + +use crate::output_formatter::InternalFormatter; + +#[derive(Debug, Default)] pub struct JsonOutputFormatter; -impl JsonOutputFormatter { - pub fn all_rules(writer: &mut T) { +impl InternalFormatter for JsonOutputFormatter { + fn all_rules(&mut self, writer: &mut dyn Write) { #[derive(Debug, serde::Serialize)] struct RuleInfoJson<'a> { scope: &'a str, @@ -28,4 +34,77 @@ impl JsonOutputFormatter { ) .unwrap(); } + + fn get_diagnostic_reporter(&self) -> Box { + Box::new(JsonReporter::default()) + } +} + +/// Renders reports as a JSON array of objects. +/// +/// Note that, due to syntactic restrictions of JSON arrays, this reporter waits until all +/// diagnostics have been reported before writing them to the output stream. +#[derive(Default)] +struct JsonReporter { + diagnostics: Vec, +} + +impl DiagnosticReporter for JsonReporter { + // NOTE: this output does not conform to eslint json format yet + // https://eslint.org/docs/latest/use/formatters/#json + fn finish(&mut self) -> Option { + Some(format_json(&mut self.diagnostics)) + } + + fn render_error(&mut self, error: Error) -> Option { + self.diagnostics.push(error); + None + } +} + +/// +#[allow(clippy::print_stdout)] +fn format_json(diagnostics: &mut Vec) -> String { + let handler = JSONReportHandler::new(); + let messages = diagnostics + .drain(..) + .map(|error| { + let mut output = String::from("\t"); + handler.render_report(&mut output, error.as_ref()).unwrap(); + output + }) + .collect::>() + .join(",\n"); + format!("[\n{messages}\n]") +} + +#[cfg(test)] +mod test { + use oxc_diagnostics::{reporter::DiagnosticReporter, NamedSource, OxcDiagnostic}; + use oxc_span::Span; + + use super::JsonReporter; + + #[test] + fn reporter() { + let mut reporter = JsonReporter::default(); + + let error = OxcDiagnostic::warn("error message") + .with_label(Span::new(0, 8)) + .with_source_code(NamedSource::new("file://test.ts", "debugger;")); + + let first_result = reporter.render_error(error); + + // reporter keeps it in memory + assert!(first_result.is_none()); + + // report not gives us all diagnostics at ones + let second_result = reporter.finish(); + + assert!(second_result.is_some()); + assert_eq!( + second_result.unwrap(), + "[\n\t{\"message\": \"error message\",\"severity\": \"warning\",\"causes\": [],\"filename\": \"file://test.ts\",\"labels\": [{\"span\": {\"offset\": 0,\"length\": 8}}],\"related\": []}\n]" + ); + } } diff --git a/apps/oxlint/src/output_formatter/mod.rs b/apps/oxlint/src/output_formatter/mod.rs index adb958a4d1bbe..4b437eb69e9dd 100644 --- a/apps/oxlint/src/output_formatter/mod.rs +++ b/apps/oxlint/src/output_formatter/mod.rs @@ -1,14 +1,19 @@ +mod checkstyle; mod default; +mod github; mod json; +mod unix; -use std::io::Write; +use std::io::{BufWriter, Stdout, Write}; use std::str::FromStr; -use crate::output_formatter::{default::DefaultOutputFormatter, json::JsonOutputFormatter}; +use checkstyle::CheckStyleOutputFormatter; +use github::GithubOutputFormatter; +use unix::UnixOutputFormatter; -pub struct OutputFormatter { - format: OutputFormat, -} +use oxc_diagnostics::reporter::DiagnosticReporter; + +use crate::output_formatter::{default::DefaultOutputFormatter, json::JsonOutputFormatter}; #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum OutputFormat { @@ -36,15 +41,38 @@ impl FromStr for OutputFormat { } } +trait InternalFormatter { + // print all rules which are currently supported by oxlint + fn all_rules(&mut self, writer: &mut dyn Write); + + fn get_diagnostic_reporter(&self) -> Box; +} + +pub struct OutputFormatter { + internal_formatter: Box, +} + impl OutputFormatter { pub fn new(format: OutputFormat) -> Self { - Self { format } + Self { internal_formatter: Self::get_internal_formatter(format) } } - // print all rules which are currently supported by oxlint - pub fn all_rules(&self, writer: &mut T) { - match self.format { - OutputFormat::Json => JsonOutputFormatter::all_rules(writer), - _ => DefaultOutputFormatter::all_rules(writer), + + fn get_internal_formatter(format: OutputFormat) -> Box { + match format { + OutputFormat::Json => Box::::default(), + OutputFormat::Checkstyle => Box::::default(), + OutputFormat::Github => Box::new(GithubOutputFormatter), + OutputFormat::Unix => Box::::default(), + OutputFormat::Default => Box::new(DefaultOutputFormatter), } } + + // print all rules which are currently supported by oxlint + pub fn all_rules(&mut self, writer: &mut BufWriter) { + self.internal_formatter.all_rules(writer); + } + + pub fn get_diagnostic_reporter(&self) -> Box { + self.internal_formatter.get_diagnostic_reporter() + } } diff --git a/apps/oxlint/src/output_formatter/unix.rs b/apps/oxlint/src/output_formatter/unix.rs new file mode 100644 index 0000000000000..4c34f334ebd4d --- /dev/null +++ b/apps/oxlint/src/output_formatter/unix.rs @@ -0,0 +1,101 @@ +use std::{borrow::Cow, io::Write}; + +use oxc_diagnostics::{ + reporter::{DiagnosticReporter, Info}, + Error, Severity, +}; + +use crate::output_formatter::InternalFormatter; + +#[derive(Debug, Default)] +pub struct UnixOutputFormatter; + +impl InternalFormatter for UnixOutputFormatter { + fn all_rules(&mut self, writer: &mut dyn Write) { + writeln!(writer, "flag --rules with flag --format=unix is not allowed").unwrap(); + } + + fn get_diagnostic_reporter(&self) -> Box { + Box::new(UnixReporter::default()) + } +} + +/// Reporter to output diagnostics in a simple one line output. +/// At the end it reports the total numbers of diagnostics. +#[derive(Default)] +struct UnixReporter { + total: usize, +} + +impl DiagnosticReporter for UnixReporter { + fn finish(&mut self) -> Option { + let total = self.total; + if total > 0 { + return Some(format!("\n{total} problem{}\n", if total > 1 { "s" } else { "" })); + } + + None + } + + fn render_error(&mut self, error: Error) -> Option { + self.total += 1; + Some(format_unix(&error)) + } +} + +/// +fn format_unix(diagnostic: &Error) -> String { + let Info { line, column, filename, message, severity, rule_id } = Info::new(diagnostic); + let severity = match severity { + Severity::Error => "Error", + _ => "Warning", + }; + let rule_id = + rule_id.map_or_else(|| Cow::Borrowed(""), |rule_id| Cow::Owned(format!("/{rule_id}"))); + format!("{filename}:{line}:{column}: {message} [{severity}{rule_id}]\n") +} + +#[cfg(test)] +mod test { + use oxc_diagnostics::{reporter::DiagnosticReporter, NamedSource, OxcDiagnostic}; + use oxc_span::Span; + + use super::UnixReporter; + + #[test] + fn reporter_finish_empty() { + let mut reporter = UnixReporter::default(); + + let result = reporter.finish(); + + assert!(result.is_none()); + } + + #[test] + fn reporter_finish_one_entry() { + let mut reporter = UnixReporter::default(); + + let error = OxcDiagnostic::warn("error message") + .with_label(Span::new(0, 8)) + .with_source_code(NamedSource::new("file://test.ts", "debugger;")); + + let _ = reporter.render_error(error); + let result = reporter.finish(); + + assert!(result.is_some()); + assert_eq!(result.unwrap(), "\n1 problem\n"); + } + + #[test] + fn reporter_error() { + let mut reporter = UnixReporter::default(); + let error = OxcDiagnostic::warn("error message") + .with_label(Span::new(0, 8)) + .with_source_code(NamedSource::new("file://test.ts", "debugger;")); + + let result = reporter.render_error(error); + + assert!(result.is_some()); + assert_eq!(result.unwrap(), "file://test.ts:1:1: error message [Warning]\n"); + } +} diff --git a/crates/oxc_diagnostics/Cargo.toml b/crates/oxc_diagnostics/Cargo.toml index 7afce7406f573..88ba53002b0ee 100644 --- a/crates/oxc_diagnostics/Cargo.toml +++ b/crates/oxc_diagnostics/Cargo.toml @@ -20,4 +20,3 @@ doctest = false [dependencies] miette = { workspace = true } -rustc-hash = { workspace = true } diff --git a/crates/oxc_diagnostics/src/lib.rs b/crates/oxc_diagnostics/src/lib.rs index c4bb9169529f9..b04456a5ced6e 100644 --- a/crates/oxc_diagnostics/src/lib.rs +++ b/crates/oxc_diagnostics/src/lib.rs @@ -48,7 +48,6 @@ //! service.run(); //! ``` -mod reporter; mod service; use std::{ @@ -57,6 +56,8 @@ use std::{ ops::{Deref, DerefMut}, }; +pub mod reporter; + pub use crate::service::{DiagnosticSender, DiagnosticService, DiagnosticTuple}; pub type Error = miette::Error; diff --git a/crates/oxc_diagnostics/src/reporter/mod.rs b/crates/oxc_diagnostics/src/reporter.rs similarity index 62% rename from crates/oxc_diagnostics/src/reporter/mod.rs rename to crates/oxc_diagnostics/src/reporter.rs index 469ee6f8127f5..34443c273a934 100644 --- a/crates/oxc_diagnostics/src/reporter/mod.rs +++ b/crates/oxc_diagnostics/src/reporter.rs @@ -1,25 +1,7 @@ //! [Reporters](DiagnosticReporter) for rendering and writing diagnostics. -mod checkstyle; -mod github; -mod graphical; -mod json; -mod unix; - -use std::io::{BufWriter, Stdout}; - -pub use self::{ - checkstyle::CheckstyleReporter, github::GithubReporter, graphical::GraphicalReporter, - json::JsonReporter, unix::UnixReporter, -}; use crate::{Error, Severity}; -/// stdio is blocked by LineWriter, use a BufWriter to reduce syscalls. -/// See `https://github.com/rust-lang/rust/issues/60673`. -fn writer() -> BufWriter { - BufWriter::new(std::io::stdout()) -} - /// Reporters are responsible for rendering diagnostics to some format and writing them to some /// form of output stream. /// @@ -28,28 +10,16 @@ fn writer() -> BufWriter { /// /// ## Example /// ``` -/// use std::io::{self, Write, BufWriter, Stderr}; /// use oxc_diagnostics::{DiagnosticReporter, Error, Severity}; /// -/// pub struct BufReporter { -/// writer: BufWriter, -/// } -/// -/// impl Default for BufReporter { -/// fn default() -> Self { -/// Self { writer: BufWriter::new(io::stderr()) } -/// } -/// } +/// #[derive(Default)] +/// pub struct BufferedReporter; /// /// impl DiagnosticReporter for BufferedReporter { -/// // flush all remaining bytes when no more diagnostics will be reported -/// fn finish(&mut self) { -/// self.writer.flush().unwrap(); -/// } -/// -/// // write rendered reports to stderr -/// fn render_diagnostics(&mut self, s: &[u8]) { -/// self.writer.write_all(s).unwrap(); +/// // render the finished output, some reporters will store the errors in memory +/// // to output all diagnostics at the end +/// fn finish(&mut self) -> Option { +/// None /// } /// /// // render diagnostics to a simple Apache-like log format @@ -68,39 +38,32 @@ fn writer() -> BufWriter { pub trait DiagnosticReporter { /// Lifecycle hook that gets called when no more diagnostics will be reported. /// - /// Used primarily for flushing output stream buffers, but you don't just have to use it for - /// that. Some reporters (e.g. [`JSONReporter`]) store all diagnostics in memory, then write them + /// 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. - /// - /// [`JSONReporter`]: crate::reporter::JsonReporter - fn finish(&mut self); - - /// Write a rendered collection of diagnostics to this reporter's output stream. - fn render_diagnostics(&mut self, s: &[u8]); + fn finish(&mut self) -> Option; /// 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 not use this method to write diagnostics to their output stream. That - /// should be done in [`render_diagnostics`](DiagnosticReporter::render_diagnostics). + /// Reporters should use this method to write diagnostics to their output stream. fn render_error(&mut self, error: Error) -> Option; } -struct Info { - line: usize, - column: usize, - filename: String, - message: String, - severity: Severity, - rule_id: Option, +pub struct Info { + pub line: usize, + pub column: usize, + pub filename: String, + pub message: String, + pub severity: Severity, + pub rule_id: Option, } impl Info { - fn new(diagnostic: &Error) -> Self { + pub fn new(diagnostic: &Error) -> Self { let mut line = 0; let mut column = 0; let mut filename = String::new(); diff --git a/crates/oxc_diagnostics/src/reporter/graphical.rs b/crates/oxc_diagnostics/src/reporter/graphical.rs deleted file mode 100644 index 69437a5844a0d..0000000000000 --- a/crates/oxc_diagnostics/src/reporter/graphical.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::io::{BufWriter, ErrorKind, Stdout, Write}; - -use super::{writer, DiagnosticReporter}; -use crate::{Error, GraphicalReportHandler}; - -/// Pretty-prints diagnostics. Primarily meant for human-readable output in a terminal. -/// -/// See [`GraphicalReportHandler`] for how to configure colors, context lines, etc. -pub struct GraphicalReporter { - handler: GraphicalReportHandler, - writer: BufWriter, -} - -impl Default for GraphicalReporter { - fn default() -> Self { - Self { handler: GraphicalReportHandler::new(), writer: writer() } - } -} - -impl DiagnosticReporter for GraphicalReporter { - fn finish(&mut self) { - self.writer - .flush() - .or_else(|e| { - // Do not panic when the process is skill (e.g. piping into `less`). - if matches!(e.kind(), ErrorKind::Interrupted | ErrorKind::BrokenPipe) { - Ok(()) - } else { - Err(e) - } - }) - .unwrap(); - } - - fn render_diagnostics(&mut self, s: &[u8]) { - self.writer - .write_all(s) - .or_else(|e| { - // Do not panic when the process is skill (e.g. piping into `less`). - if matches!(e.kind(), ErrorKind::Interrupted | ErrorKind::BrokenPipe) { - Ok(()) - } else { - Err(e) - } - }) - .unwrap(); - } - - fn render_error(&mut self, error: Error) -> Option { - let mut output = String::new(); - self.handler.render_report(&mut output, error.as_ref()).unwrap(); - Some(output) - } -} diff --git a/crates/oxc_diagnostics/src/reporter/json.rs b/crates/oxc_diagnostics/src/reporter/json.rs deleted file mode 100644 index c31b9ea383778..0000000000000 --- a/crates/oxc_diagnostics/src/reporter/json.rs +++ /dev/null @@ -1,44 +0,0 @@ -use miette::JSONReportHandler; - -use super::DiagnosticReporter; -use crate::Error; - -/// Renders reports as a JSON array of objects. -/// -/// Note that, due to syntactic restrictions of JSON arrays, this reporter waits until all -/// diagnostics have been reported before writing them to the output stream. -#[derive(Default)] -pub struct JsonReporter { - diagnostics: Vec, -} - -impl DiagnosticReporter for JsonReporter { - // NOTE: this output does not conform to eslint json format yet - // https://eslint.org/docs/latest/use/formatters/#json - fn finish(&mut self) { - format_json(&mut self.diagnostics); - } - - fn render_diagnostics(&mut self, _s: &[u8]) {} - - fn render_error(&mut self, error: Error) -> Option { - self.diagnostics.push(error); - None - } -} - -/// -#[allow(clippy::print_stdout)] -fn format_json(diagnostics: &mut Vec) { - let handler = JSONReportHandler::new(); - let messages = diagnostics - .drain(..) - .map(|error| { - let mut output = String::from("\t"); - handler.render_report(&mut output, error.as_ref()).unwrap(); - output - }) - .collect::>() - .join(",\n"); - println!("[\n{messages}\n]"); -} diff --git a/crates/oxc_diagnostics/src/reporter/unix.rs b/crates/oxc_diagnostics/src/reporter/unix.rs deleted file mode 100644 index a3e9ecaa2297b..0000000000000 --- a/crates/oxc_diagnostics/src/reporter/unix.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::{ - borrow::Cow, - io::{BufWriter, Stdout, Write}, -}; - -use super::{writer, DiagnosticReporter, Info}; -use crate::{Error, Severity}; - -pub struct UnixReporter { - total: usize, - writer: BufWriter, -} - -impl Default for UnixReporter { - fn default() -> Self { - Self { total: 0, writer: writer() } - } -} - -impl DiagnosticReporter for UnixReporter { - fn finish(&mut self) { - let total = self.total; - if total > 0 { - let line = format!("\n{total} problem{}\n", if total > 1 { "s" } else { "" }); - self.writer.write_all(line.as_bytes()).unwrap(); - } - self.writer.flush().unwrap(); - } - - fn render_diagnostics(&mut self, s: &[u8]) { - self.writer.write_all(s).unwrap(); - } - - fn render_error(&mut self, error: Error) -> Option { - self.total += 1; - Some(format_unix(&error)) - } -} - -/// -fn format_unix(diagnostic: &Error) -> String { - let Info { line, column, filename, message, severity, rule_id } = Info::new(diagnostic); - let severity = match severity { - Severity::Error => "Error", - _ => "Warning", - }; - let rule_id = - rule_id.map_or_else(|| Cow::Borrowed(""), |rule_id| Cow::Owned(format!("/{rule_id}"))); - format!("{filename}:{line}:{column}: {message} [{severity}{rule_id}]\n") -} diff --git a/crates/oxc_diagnostics/src/service.rs b/crates/oxc_diagnostics/src/service.rs index 89d222cc51e77..b46c2678f63d3 100644 --- a/crates/oxc_diagnostics/src/service.rs +++ b/crates/oxc_diagnostics/src/service.rs @@ -1,16 +1,11 @@ use std::{ cell::Cell, + io::{ErrorKind, Write}, path::{Path, PathBuf}, sync::{mpsc, Arc}, }; -use crate::{ - reporter::{ - CheckstyleReporter, DiagnosticReporter, GithubReporter, GraphicalReporter, JsonReporter, - UnixReporter, - }, - Error, NamedSource, OxcDiagnostic, Severity, -}; +use crate::{reporter::DiagnosticReporter, Error, NamedSource, OxcDiagnostic, Severity}; pub type DiagnosticTuple = (PathBuf, Vec); pub type DiagnosticSender = mpsc::Sender>; @@ -71,22 +66,13 @@ pub struct DiagnosticService { receiver: DiagnosticReceiver, } -impl Default for DiagnosticService { - fn default() -> Self { - Self::new(GraphicalReporter::default()) - } -} - impl DiagnosticService { /// Create a new [`DiagnosticService`] that will render and report diagnostics using the /// provided [`DiagnosticReporter`]. - /// - /// TODO(@DonIsaac): make `DiagnosticReporter` public so oxc consumers can create their own - /// implementations. - pub(crate) fn new(reporter: R) -> Self { + pub fn new(reporter: Box) -> Self { let (sender, receiver) = mpsc::channel(); Self { - reporter: Box::new(reporter) as Box, + reporter, quiet: false, silent: false, max_warnings: None, @@ -97,25 +83,6 @@ impl DiagnosticService { } } - /// Configure this service to format reports as a JSON array of objects. - pub fn set_json_reporter(&mut self) { - self.reporter = Box::::default(); - } - - pub fn set_unix_reporter(&mut self) { - self.reporter = Box::::default(); - } - - pub fn set_checkstyle_reporter(&mut self) { - self.reporter = Box::::default(); - } - - /// Configure this service to formats reports using [GitHub Actions - /// annotations](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message). - pub fn set_github_reporter(&mut self) { - self.reporter = Box::::default(); - } - /// Set to `true` to only report errors and ignore warnings. /// /// Use [`with_silent`](DiagnosticService::with_silent) to disable reporting entirely. @@ -198,9 +165,8 @@ impl DiagnosticService { /// # Panics /// /// * When the writer fails to write - pub fn run(&mut self) { + pub fn run(&mut self, writer: &mut dyn Write) { 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); @@ -225,7 +191,7 @@ impl DiagnosticService { continue; } - if let Some(mut err_str) = self.reporter.render_error(diagnostic) { + if let Some(err_str) = self.reporter.render_error(diagnostic) { // Skip large output and print only once. // Setting to 1200 because graphical output may contain ansi escape codes and other decorations. if err_str.lines().any(|line| line.len() >= 1200) { @@ -233,16 +199,40 @@ impl DiagnosticService { OxcDiagnostic::warn("File is too long to fit on the screen") .with_help(format!("{path:?} seems like a minified file")), ); - err_str = format!("{minified_diagnostic:?}"); - output = err_str; + + 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; } - output.push_str(&err_str); + + writer + .write_all(err_str.as_bytes()) + .or_else(Self::check_for_writer_error) + .unwrap(); } } - self.reporter.render_diagnostics(output.as_bytes()); } - self.reporter.finish(); + if let Some(finish_output) = self.reporter.finish() { + 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(); + } + + fn check_for_writer_error(error: std::io::Error) -> Result<(), std::io::Error> { + // Do not panic when the process is skill (e.g. piping into `less`). + if matches!(error.kind(), ErrorKind::Interrupted | ErrorKind::BrokenPipe) { + Ok(()) + } else { + Err(error) + } } } diff --git a/crates/oxc_linter/src/tester.rs b/crates/oxc_linter/src/tester.rs index bc0a93a224a0b..515588bec374a 100644 --- a/crates/oxc_linter/src/tester.rs +++ b/crates/oxc_linter/src/tester.rs @@ -1,11 +1,12 @@ use std::{ env, path::{Path, PathBuf}, + sync::mpsc, }; use cow_utils::CowUtils; use oxc_allocator::Allocator; -use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler, GraphicalTheme, NamedSource}; +use oxc_diagnostics::{GraphicalReportHandler, GraphicalTheme, NamedSource}; use serde::Deserialize; use serde_json::Value; @@ -471,9 +472,8 @@ impl Tester { let options = LintServiceOptions::new(cwd, paths).with_cross_module(self.plugins.has_import()); let lint_service = LintService::from_linter(linter, options); - let diagnostic_service = DiagnosticService::default(); - let tx_error = diagnostic_service.sender(); - let result = lint_service.run_source(&allocator, source_text, false, tx_error); + let (sender, _receiver) = mpsc::channel(); + let result = lint_service.run_source(&allocator, source_text, false, &sender); if result.is_empty() { return TestResult::Passed;