refactor(linter): move DiagnosticsReporters to oxlint (#8454)

it was never the plan that oxc_diagnostics should be the
`std::io::stdout` writer:

8fc238ac34/crates/oxc_diagnostics/src/service.rs (L84-L85)

This PR does refactor all reporters to `oxlint` crate and make the
current implementation of `oxc_diagnostics` public for others to
consume.

I added some tests to reflect to expected output (and found some bugs^^)

For the future I think the BufWriter for `std::io::stdout` should come
from outside.
Or maybe `std::fmt::stdout`? I do not know^^"

I could not test 100% of the code, I hope I can fix this with the next
PR which will include a own Tester for oxlint (like `oxc_linter`).
Please be extra careful when reviewing it.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Alexander S. 2025-01-16 09:11:22 +01:00 committed by GitHub
parent 9ec4e24eb7
commit b4c87e27a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 486 additions and 328 deletions

2
Cargo.lock generated
View file

@ -1703,7 +1703,6 @@ name = "oxc_diagnostics"
version = "0.46.0"
dependencies = [
"oxc-miette",
"rustc-hash",
]
[[package]]
@ -2231,6 +2230,7 @@ dependencies = [
"oxc_linter",
"oxc_span",
"rayon",
"rustc-hash",
"serde",
"serde_json",
"tempfile",

View file

@ -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 }

View file

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

View file

@ -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<dyn DiagnosticReporter> {
Box::new(CheckstyleReporter::default())
}
}
/// Reporter to output diagnostics in checkstyle format
///
/// Checkstyle Format Documentation: <https://checkstyle.sourceforge.io/>
#[derive(Default)]
pub struct CheckstyleReporter {
struct CheckstyleReporter {
diagnostics: Vec<Error>,
}
impl DiagnosticReporter for CheckstyleReporter {
fn finish(&mut self) {
format_checkstyle(&self.diagnostics);
fn finish(&mut self) -> Option<String> {
Some(format_checkstyle(&self.diagnostics))
}
fn render_diagnostics(&mut self, _s: &[u8]) {}
fn render_error(&mut self, error: Error) -> Option<String> {
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::<Vec<_>>();
let mut grouped: FxHashMap<String, Vec<Info>> = FxHashMap::default();
for info in infos {
@ -48,9 +65,9 @@ fn format_checkstyle(diagnostics: &[Error]) {
let filename = &infos[0].filename;
format!(r#"<file name="{filename}">{messages}</file>"#)
}).collect::<Vec<_>>().join(" ");
println!(
format!(
r#"<?xml version="1.0" encoding="utf-8"?><checkstyle version="4.3">{messages}</checkstyle>"#
);
)
}
/// <https://github.com/tafia/quick-xml/blob/6e34a730853fe295d68dc28460153f08a5a12955/src/escapei.rs#L84-L86>
@ -103,3 +120,31 @@ fn xml_escape_impl<F: Fn(u8) -> bool>(raw: &str, escape_chars: F) -> Cow<str> {
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(), "<?xml version=\"1.0\" encoding=\"utf-8\"?><checkstyle version=\"4.3\"><file name=\"file://test.ts\"><error line=\"1\" column=\"1\" severity=\"warning\" message=\"error message\" source=\"\" /></file></checkstyle>");
}
}

View file

@ -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<T: Write>(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<dyn DiagnosticReporter> {
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<String> {
None
}
fn render_error(&mut self, error: Error) -> Option<String> {
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"
);
}
}

View file

@ -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;
#[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();
}
fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
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.
pub struct GithubReporter {
writer: BufWriter<Stdout>,
}
impl Default for GithubReporter {
fn default() -> Self {
Self { writer: writer() }
}
}
struct GithubReporter;
impl DiagnosticReporter for GithubReporter {
fn finish(&mut self) {
self.writer.flush().unwrap();
fn finish(&mut self) -> Option<String> {
None
}
fn render_diagnostics(&mut self, _s: &[u8]) {}
fn render_error(&mut self, error: Error) -> Option<String> {
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");
}
}

View file

@ -1,12 +1,18 @@
use oxc_linter::rules::RULES;
use oxc_linter::RuleCategory;
use std::io::Write;
#[derive(Debug)]
use oxc_diagnostics::{reporter::DiagnosticReporter, Error};
use oxc_linter::rules::RULES;
use oxc_linter::RuleCategory;
use miette::JSONReportHandler;
use crate::output_formatter::InternalFormatter;
#[derive(Debug, Default)]
pub struct JsonOutputFormatter;
impl JsonOutputFormatter {
pub fn all_rules<T: Write>(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<dyn DiagnosticReporter> {
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<Error>,
}
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<String> {
Some(format_json(&mut self.diagnostics))
}
fn render_error(&mut self, error: Error) -> Option<String> {
self.diagnostics.push(error);
None
}
}
/// <https://github.com/fregante/eslint-formatters/tree/ae1fd9748596447d1fd09625c33d9e7ba9a3d06d/packages/eslint-formatter-json>
#[allow(clippy::print_stdout)]
fn format_json(diagnostics: &mut Vec<Error>) -> 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::<Vec<_>>()
.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]"
);
}
}

View file

@ -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<dyn DiagnosticReporter>;
}
pub struct OutputFormatter {
internal_formatter: Box<dyn InternalFormatter>,
}
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<T: Write>(&self, writer: &mut T) {
match self.format {
OutputFormat::Json => JsonOutputFormatter::all_rules(writer),
_ => DefaultOutputFormatter::all_rules(writer),
fn get_internal_formatter(format: OutputFormat) -> Box<dyn InternalFormatter> {
match format {
OutputFormat::Json => Box::<JsonOutputFormatter>::default(),
OutputFormat::Checkstyle => Box::<CheckStyleOutputFormatter>::default(),
OutputFormat::Github => Box::new(GithubOutputFormatter),
OutputFormat::Unix => Box::<UnixOutputFormatter>::default(),
OutputFormat::Default => Box::new(DefaultOutputFormatter),
}
}
// print all rules which are currently supported by oxlint
pub fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
self.internal_formatter.all_rules(writer);
}
pub fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
self.internal_formatter.get_diagnostic_reporter()
}
}

View file

@ -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<dyn DiagnosticReporter> {
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<String> {
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<String> {
self.total += 1;
Some(format_unix(&error))
}
}
/// <https://github.com/fregante/eslint-formatters/tree/ae1fd9748596447d1fd09625c33d9e7ba9a3d06d/packages/eslint-formatter-unix>
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");
}
}

View file

@ -20,4 +20,3 @@ doctest = false
[dependencies]
miette = { workspace = true }
rustc-hash = { workspace = true }

View file

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

View file

@ -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<Stdout> {
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<Stdout> {
///
/// ## Example
/// ```
/// use std::io::{self, Write, BufWriter, Stderr};
/// use oxc_diagnostics::{DiagnosticReporter, Error, Severity};
///
/// pub struct BufReporter {
/// writer: BufWriter<Stderr>,
/// }
///
/// 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<String> {
/// None
/// }
///
/// // render diagnostics to a simple Apache-like log format
@ -68,39 +38,32 @@ fn writer() -> BufWriter<Stdout> {
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<String>;
/// 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<String>;
}
struct Info {
line: usize,
column: usize,
filename: String,
message: String,
severity: Severity,
rule_id: Option<String>,
pub struct Info {
pub line: usize,
pub column: usize,
pub filename: String,
pub message: String,
pub severity: Severity,
pub rule_id: Option<String>,
}
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();

View file

@ -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<Stdout>,
}
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<String> {
let mut output = String::new();
self.handler.render_report(&mut output, error.as_ref()).unwrap();
Some(output)
}
}

View file

@ -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<Error>,
}
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<String> {
self.diagnostics.push(error);
None
}
}
/// <https://github.com/fregante/eslint-formatters/tree/ae1fd9748596447d1fd09625c33d9e7ba9a3d06d/packages/eslint-formatter-json>
#[allow(clippy::print_stdout)]
fn format_json(diagnostics: &mut Vec<Error>) {
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::<Vec<_>>()
.join(",\n");
println!("[\n{messages}\n]");
}

View file

@ -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<Stdout>,
}
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<String> {
self.total += 1;
Some(format_unix(&error))
}
}
/// <https://github.com/fregante/eslint-formatters/tree/ae1fd9748596447d1fd09625c33d9e7ba9a3d06d/packages/eslint-formatter-unix>
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")
}

View file

@ -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<Error>);
pub type DiagnosticSender = mpsc::Sender<Option<DiagnosticTuple>>;
@ -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<R: DiagnosticReporter + 'static>(reporter: R) -> Self {
pub fn new(reporter: Box<dyn DiagnosticReporter>) -> Self {
let (sender, receiver) = mpsc::channel();
Self {
reporter: Box::new(reporter) as Box<dyn DiagnosticReporter>,
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::<JsonReporter>::default();
}
pub fn set_unix_reporter(&mut self) {
self.reporter = Box::<UnixReporter>::default();
}
pub fn set_checkstyle_reporter(&mut self) {
self.reporter = Box::<CheckstyleReporter>::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::<GithubReporter>::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)
}
}
}

View file

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