From 04b2cac895c6cf4ae507d3959140fda8f0b06cc3 Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sat, 11 Jan 2025 18:24:12 +0100
Subject: [PATCH 01/20] refactor(linter): add output formatter

---
 apps/oxlint/src/command/lint.rs               | 30 +----------
 apps/oxlint/src/command/mod.rs                |  2 +-
 apps/oxlint/src/lint.rs                       | 18 +++----
 crates/oxc_linter/src/lib.rs                  | 47 ++---------------
 .../default_output_formatter.rs               | 29 +++++++++++
 .../output_formatter/json_output_formatter.rs | 31 +++++++++++
 crates/oxc_linter/src/output_formatter/mod.rs | 52 +++++++++++++++++++
 7 files changed, 127 insertions(+), 82 deletions(-)
 create mode 100644 crates/oxc_linter/src/output_formatter/default_output_formatter.rs
 create mode 100644 crates/oxc_linter/src/output_formatter/json_output_formatter.rs
 create mode 100644 crates/oxc_linter/src/output_formatter/mod.rs

diff --git a/apps/oxlint/src/command/lint.rs b/apps/oxlint/src/command/lint.rs
index a02ca8f609db0..91f01933acbef 100644
--- a/apps/oxlint/src/command/lint.rs
+++ b/apps/oxlint/src/command/lint.rs
@@ -1,7 +1,7 @@
-use std::{path::PathBuf, str::FromStr};
+use std::path::PathBuf;
 
 use bpaf::Bpaf;
-use oxc_linter::{AllowWarnDeny, FixKind, LintPlugins};
+use oxc_linter::{output_formatter::OutputFormat, AllowWarnDeny, FixKind, LintPlugins};
 
 use super::{
     ignore::{ignore_options, IgnoreOptions},
@@ -184,32 +184,6 @@ pub struct OutputOptions {
     pub format: OutputFormat,
 }
 
-#[derive(Debug, Clone, Copy, Eq, PartialEq)]
-pub enum OutputFormat {
-    Default,
-    /// GitHub Check Annotation
-    /// <https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message>
-    Github,
-    Json,
-    Unix,
-    Checkstyle,
-}
-
-impl FromStr for OutputFormat {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s {
-            "json" => Ok(Self::Json),
-            "default" => Ok(Self::Default),
-            "unix" => Ok(Self::Unix),
-            "checkstyle" => Ok(Self::Checkstyle),
-            "github" => Ok(Self::Github),
-            _ => Err(format!("'{s}' is not a known format")),
-        }
-    }
-}
-
 /// Enable Plugins
 #[allow(clippy::struct_field_names)]
 #[derive(Debug, Default, Clone, Bpaf)]
diff --git a/apps/oxlint/src/command/mod.rs b/apps/oxlint/src/command/mod.rs
index a2e1733f8211f..2ffe29d79e747 100644
--- a/apps/oxlint/src/command/mod.rs
+++ b/apps/oxlint/src/command/mod.rs
@@ -7,7 +7,7 @@ use bpaf::Bpaf;
 
 pub use self::{
     ignore::IgnoreOptions,
-    lint::{lint_command, LintCommand, OutputFormat, OutputOptions, WarningOptions},
+    lint::{lint_command, LintCommand, OutputOptions, WarningOptions},
 };
 
 const VERSION: &str = match option_env!("OXC_VERSION") {
diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs
index d2b03b79d2494..e10887de8481e 100644
--- a/apps/oxlint/src/lint.rs
+++ b/apps/oxlint/src/lint.rs
@@ -9,15 +9,16 @@ use ignore::gitignore::Gitignore;
 
 use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler};
 use oxc_linter::{
-    loader::LINT_PARTIAL_LOADER_EXT, AllowWarnDeny, ConfigStoreBuilder, InvalidFilterKind,
-    LintFilter, LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc,
+    loader::LINT_PARTIAL_LOADER_EXT,
+    output_formatter::{OutputFormat, OutputFormatter},
+    AllowWarnDeny, ConfigStoreBuilder, InvalidFilterKind, LintFilter, LintOptions, LintService,
+    LintServiceOptions, Linter, Oxlintrc,
 };
 use oxc_span::VALID_EXTENSIONS;
 
 use crate::{
     cli::{
-        CliRunResult, LintCommand, LintResult, MiscOptions, OutputFormat, OutputOptions, Runner,
-        WarningOptions,
+        CliRunResult, LintCommand, LintResult, MiscOptions, OutputOptions, Runner, WarningOptions,
     },
     walk::{Extensions, Walk},
 };
@@ -36,13 +37,12 @@ impl Runner for LintRunner {
     }
 
     fn run(self) -> CliRunResult {
+        let format_str = self.options.output_options.format;
+        let output_formatter = OutputFormatter::new(format_str);
+
         if self.options.list_rules {
             let mut stdout = BufWriter::new(std::io::stdout());
-            if self.options.output_options.format == OutputFormat::Json {
-                Linter::print_rules_json(&mut stdout);
-            } else {
-                Linter::print_rules(&mut stdout);
-            }
+            output_formatter.all_rules(&mut stdout);
             return CliRunResult::None;
         }
 
diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs
index 27ef351b768e5..2b7b990fc5bbe 100644
--- a/crates/oxc_linter/src/lib.rs
+++ b/crates/oxc_linter/src/lib.rs
@@ -20,12 +20,12 @@ mod service;
 mod utils;
 
 pub mod loader;
+pub mod output_formatter;
 pub mod table;
 
-use std::{io::Write, path::Path, rc::Rc, sync::Arc};
+use std::{path::Path, rc::Rc, sync::Arc};
 
 use oxc_semantic::{AstNode, Semantic};
-use rules::RULES;
 
 pub use crate::{
     config::{
@@ -183,52 +183,11 @@ impl Linter {
 
         ctx_host.take_diagnostics()
     }
-
-    /// # Panics
-    pub fn print_rules<W: Write>(writer: &mut W) {
-        let table = RuleTable::new();
-        for section in table.sections {
-            writeln!(writer, "{}", section.render_markdown_table(None)).unwrap();
-        }
-        writeln!(writer, "Default: {}", table.turned_on_by_default_count).unwrap();
-        writeln!(writer, "Total: {}", table.total).unwrap();
-    }
-
-    /// # Panics
-    pub fn print_rules_json<W: Write>(writer: &mut W) {
-        #[derive(Debug, serde::Serialize)]
-        struct RuleInfoJson<'a> {
-            scope: &'a str,
-            value: &'a str,
-            category: RuleCategory,
-        }
-
-        let rules_info = RULES.iter().map(|rule| RuleInfoJson {
-            scope: rule.plugin_name(),
-            value: rule.name(),
-            category: rule.category(),
-        });
-
-        writer
-            .write_all(
-                serde_json::to_string_pretty(&rules_info.collect::<Vec<_>>())
-                    .expect("Failed to serialize")
-                    .as_bytes(),
-            )
-            .unwrap();
-    }
 }
 
 #[cfg(test)]
 mod test {
-    use super::{Linter, Oxlintrc};
-
-    #[test]
-    fn print_rules() {
-        let mut writer = Vec::new();
-        Linter::print_rules(&mut writer);
-        assert!(!writer.is_empty());
-    }
+    use super::Oxlintrc;
 
     #[test]
     fn test_schema_json() {
diff --git a/crates/oxc_linter/src/output_formatter/default_output_formatter.rs b/crates/oxc_linter/src/output_formatter/default_output_formatter.rs
new file mode 100644
index 0000000000000..0c70c851904a4
--- /dev/null
+++ b/crates/oxc_linter/src/output_formatter/default_output_formatter.rs
@@ -0,0 +1,29 @@
+use std::io::Write;
+
+use crate::RuleTable;
+
+pub struct DefaultOutputFormatter;
+
+impl DefaultOutputFormatter {
+    pub fn all_rules<T: Write>(writer: &mut T) {
+        let table = RuleTable::new();
+        for section in table.sections {
+            writeln!(writer, "{}", section.render_markdown_table(None)).unwrap();
+        }
+        writeln!(writer, "Default: {}", table.turned_on_by_default_count).unwrap();
+        writeln!(writer, "Total: {}", table.total).unwrap();
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::output_formatter::default_output_formatter::DefaultOutputFormatter;
+
+    #[test]
+    fn all_rules() {
+        let mut writer = Vec::new();
+
+        DefaultOutputFormatter::all_rules(&mut writer);
+        assert!(!writer.is_empty());
+    }
+}
diff --git a/crates/oxc_linter/src/output_formatter/json_output_formatter.rs b/crates/oxc_linter/src/output_formatter/json_output_formatter.rs
new file mode 100644
index 0000000000000..7bcbef5c26829
--- /dev/null
+++ b/crates/oxc_linter/src/output_formatter/json_output_formatter.rs
@@ -0,0 +1,31 @@
+use crate::rules::RULES;
+use crate::RuleCategory;
+use std::io::Write;
+
+#[derive(Debug)]
+pub struct JsonOutputFormatter;
+
+impl JsonOutputFormatter {
+    pub fn all_rules<T: Write>(writer: &mut T) {
+        #[derive(Debug, serde::Serialize)]
+        struct RuleInfoJson<'a> {
+            scope: &'a str,
+            value: &'a str,
+            category: RuleCategory,
+        }
+
+        let rules_info = RULES.iter().map(|rule| RuleInfoJson {
+            scope: rule.plugin_name(),
+            value: rule.name(),
+            category: rule.category(),
+        });
+
+        writer
+            .write_all(
+                serde_json::to_string_pretty(&rules_info.collect::<Vec<_>>())
+                    .expect("Failed to serialize")
+                    .as_bytes(),
+            )
+            .unwrap();
+    }
+}
diff --git a/crates/oxc_linter/src/output_formatter/mod.rs b/crates/oxc_linter/src/output_formatter/mod.rs
new file mode 100644
index 0000000000000..a945d89504a53
--- /dev/null
+++ b/crates/oxc_linter/src/output_formatter/mod.rs
@@ -0,0 +1,52 @@
+mod default_output_formatter;
+mod json_output_formatter;
+
+use std::io::Write;
+use std::str::FromStr;
+
+use crate::output_formatter::{
+    default_output_formatter::DefaultOutputFormatter, json_output_formatter::JsonOutputFormatter,
+};
+
+pub struct OutputFormatter {
+    format: OutputFormat,
+}
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub enum OutputFormat {
+    Default,
+    /// GitHub Check Annotation
+    /// <https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message>
+    Github,
+    Json,
+    Unix,
+    Checkstyle,
+}
+
+impl FromStr for OutputFormat {
+    type Err = String;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s {
+            "json" => Ok(Self::Json),
+            "default" => Ok(Self::Default),
+            "unix" => Ok(Self::Unix),
+            "checkstyle" => Ok(Self::Checkstyle),
+            "github" => Ok(Self::Github),
+            _ => Err(format!("'{s}' is not a known format")),
+        }
+    }
+}
+
+impl OutputFormatter {
+    pub fn new(format: OutputFormat) -> Self {
+        Self { 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),
+        }
+    }
+}

From dc8ffb059ae3b8d52e072049b60711a860fac97b Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sat, 11 Jan 2025 18:34:07 +0100
Subject: [PATCH 02/20] refactor(linter): add output formatter

---
 .../{default_output_formatter.rs => default.rs}           | 2 +-
 .../{json_output_formatter.rs => json.rs}                 | 0
 crates/oxc_linter/src/output_formatter/mod.rs             | 8 +++-----
 3 files changed, 4 insertions(+), 6 deletions(-)
 rename crates/oxc_linter/src/output_formatter/{default_output_formatter.rs => default.rs} (89%)
 rename crates/oxc_linter/src/output_formatter/{json_output_formatter.rs => json.rs} (100%)

diff --git a/crates/oxc_linter/src/output_formatter/default_output_formatter.rs b/crates/oxc_linter/src/output_formatter/default.rs
similarity index 89%
rename from crates/oxc_linter/src/output_formatter/default_output_formatter.rs
rename to crates/oxc_linter/src/output_formatter/default.rs
index 0c70c851904a4..2646d8ef6ed9b 100644
--- a/crates/oxc_linter/src/output_formatter/default_output_formatter.rs
+++ b/crates/oxc_linter/src/output_formatter/default.rs
@@ -17,7 +17,7 @@ impl DefaultOutputFormatter {
 
 #[cfg(test)]
 mod test {
-    use crate::output_formatter::default_output_formatter::DefaultOutputFormatter;
+    use crate::output_formatter::default::DefaultOutputFormatter;
 
     #[test]
     fn all_rules() {
diff --git a/crates/oxc_linter/src/output_formatter/json_output_formatter.rs b/crates/oxc_linter/src/output_formatter/json.rs
similarity index 100%
rename from crates/oxc_linter/src/output_formatter/json_output_formatter.rs
rename to crates/oxc_linter/src/output_formatter/json.rs
diff --git a/crates/oxc_linter/src/output_formatter/mod.rs b/crates/oxc_linter/src/output_formatter/mod.rs
index a945d89504a53..adb958a4d1bbe 100644
--- a/crates/oxc_linter/src/output_formatter/mod.rs
+++ b/crates/oxc_linter/src/output_formatter/mod.rs
@@ -1,12 +1,10 @@
-mod default_output_formatter;
-mod json_output_formatter;
+mod default;
+mod json;
 
 use std::io::Write;
 use std::str::FromStr;
 
-use crate::output_formatter::{
-    default_output_formatter::DefaultOutputFormatter, json_output_formatter::JsonOutputFormatter,
-};
+use crate::output_formatter::{default::DefaultOutputFormatter, json::JsonOutputFormatter};
 
 pub struct OutputFormatter {
     format: OutputFormat,

From 6abc40a9a2d58e360399bddd8ecfa6449a0c33bc Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sat, 11 Jan 2025 18:50:58 +0100
Subject: [PATCH 03/20] refactor(linter): add output formatter -move to oxlint

---
 Cargo.lock                                                 | 2 ++
 apps/oxlint/Cargo.toml                                     | 2 ++
 apps/oxlint/src/command/lint.rs                            | 4 +++-
 apps/oxlint/src/lib.rs                                     | 1 +
 apps/oxlint/src/lint.rs                                    | 7 +++----
 .../oxlint}/src/output_formatter/default.rs                | 2 +-
 .../oxlint}/src/output_formatter/json.rs                   | 4 ++--
 .../oxc_linter => apps/oxlint}/src/output_formatter/mod.rs | 0
 crates/oxc_linter/src/lib.rs                               | 4 +---
 9 files changed, 15 insertions(+), 11 deletions(-)
 rename {crates/oxc_linter => apps/oxlint}/src/output_formatter/default.rs (95%)
 rename {crates/oxc_linter => apps/oxlint}/src/output_formatter/json.rs (92%)
 rename {crates/oxc_linter => apps/oxlint}/src/output_formatter/mod.rs (100%)

diff --git a/Cargo.lock b/Cargo.lock
index 7f936f0d4c668..3e5b7ae034a71 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2217,6 +2217,8 @@ dependencies = [
  "oxc_linter",
  "oxc_span",
  "rayon",
+ "serde",
+ "serde_json",
  "tempfile",
  "tracing-subscriber",
 ]
diff --git a/apps/oxlint/Cargo.toml b/apps/oxlint/Cargo.toml
index e49afd6666293..af967c99eb5d3 100644
--- a/apps/oxlint/Cargo.toml
+++ b/apps/oxlint/Cargo.toml
@@ -39,6 +39,8 @@ bpaf = { workspace = true, features = ["autocomplete", "bright-color", "derive"]
 ignore = { workspace = true, features = ["simd-accel"] }
 miette = { workspace = true }
 rayon = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
 tempfile = { workspace = true }
 tracing-subscriber = { workspace = true, features = [] } # Omit the `regex` feature
 
diff --git a/apps/oxlint/src/command/lint.rs b/apps/oxlint/src/command/lint.rs
index 91f01933acbef..c2435c1a1fc57 100644
--- a/apps/oxlint/src/command/lint.rs
+++ b/apps/oxlint/src/command/lint.rs
@@ -1,7 +1,9 @@
 use std::path::PathBuf;
 
 use bpaf::Bpaf;
-use oxc_linter::{output_formatter::OutputFormat, AllowWarnDeny, FixKind, LintPlugins};
+use oxc_linter::{AllowWarnDeny, FixKind, LintPlugins};
+
+use crate::output_formatter::OutputFormat;
 
 use super::{
     ignore::{ignore_options, IgnoreOptions},
diff --git a/apps/oxlint/src/lib.rs b/apps/oxlint/src/lib.rs
index 75a5315df3067..d8d5293f2060f 100644
--- a/apps/oxlint/src/lib.rs
+++ b/apps/oxlint/src/lib.rs
@@ -1,5 +1,6 @@
 mod command;
 mod lint;
+mod output_formatter;
 mod result;
 mod runner;
 mod walk;
diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs
index e10887de8481e..5ca498dfcc0d6 100644
--- a/apps/oxlint/src/lint.rs
+++ b/apps/oxlint/src/lint.rs
@@ -9,10 +9,8 @@ use ignore::gitignore::Gitignore;
 
 use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler};
 use oxc_linter::{
-    loader::LINT_PARTIAL_LOADER_EXT,
-    output_formatter::{OutputFormat, OutputFormatter},
-    AllowWarnDeny, ConfigStoreBuilder, InvalidFilterKind, LintFilter, LintOptions, LintService,
-    LintServiceOptions, Linter, Oxlintrc,
+    loader::LINT_PARTIAL_LOADER_EXT, AllowWarnDeny, ConfigStoreBuilder, InvalidFilterKind,
+    LintFilter, LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc,
 };
 use oxc_span::VALID_EXTENSIONS;
 
@@ -20,6 +18,7 @@ use crate::{
     cli::{
         CliRunResult, LintCommand, LintResult, MiscOptions, OutputOptions, Runner, WarningOptions,
     },
+    output_formatter::{OutputFormat, OutputFormatter},
     walk::{Extensions, Walk},
 };
 
diff --git a/crates/oxc_linter/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
similarity index 95%
rename from crates/oxc_linter/src/output_formatter/default.rs
rename to apps/oxlint/src/output_formatter/default.rs
index 2646d8ef6ed9b..37598e40cf1d3 100644
--- a/crates/oxc_linter/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -1,6 +1,6 @@
 use std::io::Write;
 
-use crate::RuleTable;
+use oxc_linter::table::RuleTable;
 
 pub struct DefaultOutputFormatter;
 
diff --git a/crates/oxc_linter/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs
similarity index 92%
rename from crates/oxc_linter/src/output_formatter/json.rs
rename to apps/oxlint/src/output_formatter/json.rs
index 7bcbef5c26829..1b9b1d50828a8 100644
--- a/crates/oxc_linter/src/output_formatter/json.rs
+++ b/apps/oxlint/src/output_formatter/json.rs
@@ -1,5 +1,5 @@
-use crate::rules::RULES;
-use crate::RuleCategory;
+use oxc_linter::rules::RULES;
+use oxc_linter::RuleCategory;
 use std::io::Write;
 
 #[derive(Debug)]
diff --git a/crates/oxc_linter/src/output_formatter/mod.rs b/apps/oxlint/src/output_formatter/mod.rs
similarity index 100%
rename from crates/oxc_linter/src/output_formatter/mod.rs
rename to apps/oxlint/src/output_formatter/mod.rs
diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs
index 2b7b990fc5bbe..3e1e2d8144315 100644
--- a/crates/oxc_linter/src/lib.rs
+++ b/crates/oxc_linter/src/lib.rs
@@ -15,12 +15,11 @@ mod module_graph_visitor;
 mod module_record;
 mod options;
 mod rule;
-mod rules;
 mod service;
 mod utils;
 
 pub mod loader;
-pub mod output_formatter;
+pub mod rules;
 pub mod table;
 
 use std::{path::Path, rc::Rc, sync::Arc};
@@ -45,7 +44,6 @@ use crate::{
     context::ContextHost,
     fixer::{Fixer, Message},
     rules::RuleEnum,
-    table::RuleTable,
     utils::iter_possible_jest_call_node,
 };
 

From 28dd36df835a7c8bda78ac9533515ce1ac9127ac Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sun, 12 Jan 2025 16:15:13 +0100
Subject: [PATCH 04/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 apps/oxlint/src/lint.rs                     |  3 ++-
 apps/oxlint/src/output_formatter/default.rs | 11 ++++++++++-
 apps/oxlint/src/output_formatter/json.rs    | 17 +++++++++++++++++
 apps/oxlint/src/output_formatter/mod.rs     |  9 +++++++++
 4 files changed, 38 insertions(+), 2 deletions(-)

diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs
index 5ca498dfcc0d6..616c6e62080e1 100644
--- a/apps/oxlint/src/lint.rs
+++ b/apps/oxlint/src/lint.rs
@@ -1,6 +1,6 @@
 use std::{
     env,
-    io::BufWriter,
+    io::{BufWriter, Write},
     path::{Path, PathBuf},
     time::Instant,
 };
@@ -42,6 +42,7 @@ impl Runner for LintRunner {
         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;
         }
 
diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
index 37598e40cf1d3..bdd23c8d59127 100644
--- a/apps/oxlint/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -1,6 +1,7 @@
 use std::io::Write;
 
 use oxc_linter::table::RuleTable;
+use oxc_diagnostics::{GraphicalReportHandler, Error};
 
 pub struct DefaultOutputFormatter;
 
@@ -13,6 +14,14 @@ impl DefaultOutputFormatter {
         writeln!(writer, "Default: {}", table.turned_on_by_default_count).unwrap();
         writeln!(writer, "Total: {}", table.total).unwrap();
     }
+
+    pub fn diagnostics<T: Write + std::fmt::Write>(writer: &mut T, diagnostics: &mut Vec<Error>) {
+        let handler = GraphicalReportHandler::new();
+
+        for error in diagnostics {
+            handler.render_report( writer, error.as_ref()).unwrap();
+        }
+    }
 }
 
 #[cfg(test)]
@@ -25,5 +34,5 @@ mod test {
 
         DefaultOutputFormatter::all_rules(&mut writer);
         assert!(!writer.is_empty());
-    }
+    } 
 }
diff --git a/apps/oxlint/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs
index 1b9b1d50828a8..9ebbe2fa312fe 100644
--- a/apps/oxlint/src/output_formatter/json.rs
+++ b/apps/oxlint/src/output_formatter/json.rs
@@ -1,3 +1,5 @@
+use miette::JSONReportHandler;
+use oxc_diagnostics::Error;
 use oxc_linter::rules::RULES;
 use oxc_linter::RuleCategory;
 use std::io::Write;
@@ -28,4 +30,19 @@ impl JsonOutputFormatter {
             )
             .unwrap();
     }
+
+    pub fn diagnostics<T: Write>(writer: &mut T, 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");
+
+        writer.write_all(format!("[\n{messages}\n]").as_bytes()).unwrap();
+    }
 }
diff --git a/apps/oxlint/src/output_formatter/mod.rs b/apps/oxlint/src/output_formatter/mod.rs
index adb958a4d1bbe..5f83dfc4929aa 100644
--- a/apps/oxlint/src/output_formatter/mod.rs
+++ b/apps/oxlint/src/output_formatter/mod.rs
@@ -4,6 +4,8 @@ mod json;
 use std::io::Write;
 use std::str::FromStr;
 
+use oxc_diagnostics::Error;
+
 use crate::output_formatter::{default::DefaultOutputFormatter, json::JsonOutputFormatter};
 
 pub struct OutputFormatter {
@@ -47,4 +49,11 @@ impl OutputFormatter {
             _ => DefaultOutputFormatter::all_rules(writer),
         }
     }
+
+    pub fn diagnostics<T: Write + std::fmt::Write>(&self, writer: &mut T, diagnostics: &mut Vec<Error>) {
+        match self.format {
+            OutputFormat::Json => JsonOutputFormatter::diagnostics(writer, diagnostics),
+            _ => DefaultOutputFormatter::diagnostics(writer, diagnostics),
+        }
+    }
 }

From 6acc1427bfd5492a21c7039b9d2488af13dc405e Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sun, 12 Jan 2025 20:13:33 +0100
Subject: [PATCH 05/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 apps/oxlint/src/lint.rs                       | 31 ++++-------
 .../oxlint/src/output_formatter/checkstyle.rs | 18 +++++++
 apps/oxlint/src/output_formatter/default.rs   | 31 +++++------
 apps/oxlint/src/output_formatter/github.rs    | 18 +++++++
 apps/oxlint/src/output_formatter/json.rs      | 29 ++++------
 apps/oxlint/src/output_formatter/mod.rs       | 53 +++++++++++++------
 apps/oxlint/src/output_formatter/unix.rs      | 18 +++++++
 crates/oxc_diagnostics/src/lib.rs             |  3 +-
 .../src/reporter/checkstyle.rs                | 17 +++---
 crates/oxc_diagnostics/src/reporter/github.rs | 24 +++------
 .../oxc_diagnostics/src/reporter/graphical.rs | 13 +++--
 crates/oxc_diagnostics/src/reporter/json.rs   | 12 +++--
 crates/oxc_diagnostics/src/reporter/mod.rs    | 10 +---
 crates/oxc_diagnostics/src/reporter/unix.rs   | 20 +++----
 crates/oxc_diagnostics/src/service.rs         | 40 +++-----------
 15 files changed, 177 insertions(+), 160 deletions(-)
 create mode 100644 apps/oxlint/src/output_formatter/checkstyle.rs
 create mode 100644 apps/oxlint/src/output_formatter/github.rs
 create mode 100644 apps/oxlint/src/output_formatter/unix.rs

diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs
index 616c6e62080e1..ba329426992b8 100644
--- a/apps/oxlint/src/lint.rs
+++ b/apps/oxlint/src/lint.rs
@@ -6,7 +6,6 @@ use std::{
 };
 
 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,10 +34,13 @@ 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;
@@ -164,7 +164,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({
@@ -174,7 +174,7 @@ impl Runner for LintRunner {
                 lint_service.run(&tx_error);
             }
         });
-        diagnostic_service.run();
+        diagnostic_service.run(&mut stdout);
 
         CliRunResult::LintResult(LintResult {
             duration: now.elapsed(),
@@ -199,23 +199,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/apps/oxlint/src/output_formatter/checkstyle.rs b/apps/oxlint/src/output_formatter/checkstyle.rs
new file mode 100644
index 0000000000000..e4c6811efeb3e
--- /dev/null
+++ b/apps/oxlint/src/output_formatter/checkstyle.rs
@@ -0,0 +1,18 @@
+use std::io::{BufWriter, Stdout, Write};
+
+use oxc_diagnostics::reporter::{CheckstyleReporter, DiagnosticReporter};
+
+use crate::output_formatter::InternalFormatter;
+
+#[derive(Debug, Default)]
+pub struct CheckStyleOutputFormatter;
+
+impl InternalFormatter for CheckStyleOutputFormatter {
+    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
+        writeln!(writer, "flag --rules with flag --format=checkstyle is not allowed").unwrap();
+    }
+
+    fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
+        Box::new(CheckstyleReporter::default())
+    }
+}
diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
index bdd23c8d59127..1d67e7aa91034 100644
--- a/apps/oxlint/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -1,12 +1,15 @@
-use std::io::Write;
+use std::io::{BufWriter, Stdout, Write};
 
+use oxc_diagnostics::reporter::{DiagnosticReporter, GraphicalReporter};
 use oxc_linter::table::RuleTable;
-use oxc_diagnostics::{GraphicalReportHandler, Error};
 
+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 BufWriter<Stdout>) {
         let table = RuleTable::new();
         for section in table.sections {
             writeln!(writer, "{}", section.render_markdown_table(None)).unwrap();
@@ -15,24 +18,22 @@ impl DefaultOutputFormatter {
         writeln!(writer, "Total: {}", table.total).unwrap();
     }
 
-    pub fn diagnostics<T: Write + std::fmt::Write>(writer: &mut T, diagnostics: &mut Vec<Error>) {
-        let handler = GraphicalReportHandler::new();
-
-        for error in diagnostics {
-            handler.render_report( writer, error.as_ref()).unwrap();
-        }
+    fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
+        Box::new(GraphicalReporter::default())
     }
 }
 
 #[cfg(test)]
 mod test {
-    use crate::output_formatter::default::DefaultOutputFormatter;
+    use crate::output_formatter::{default::DefaultOutputFormatter, InternalFormatter};
+    use std::io::BufWriter;
 
     #[test]
     fn all_rules() {
-        let mut writer = Vec::new();
+        let mut writer = BufWriter::new(std::io::stdout());
+        let mut formatter = DefaultOutputFormatter;
 
-        DefaultOutputFormatter::all_rules(&mut writer);
-        assert!(!writer.is_empty());
-    } 
+        formatter.all_rules(&mut writer);
+        assert!(!writer.buffer().is_empty());
+    }
 }
diff --git a/apps/oxlint/src/output_formatter/github.rs b/apps/oxlint/src/output_formatter/github.rs
new file mode 100644
index 0000000000000..8d0f4bf943b25
--- /dev/null
+++ b/apps/oxlint/src/output_formatter/github.rs
@@ -0,0 +1,18 @@
+use std::io::{BufWriter, Stdout, Write};
+
+use oxc_diagnostics::reporter::{DiagnosticReporter, GithubReporter};
+
+use crate::output_formatter::InternalFormatter;
+
+#[derive(Debug, Default)]
+pub struct GithubOutputFormatter;
+
+impl InternalFormatter for GithubOutputFormatter {
+    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
+        writeln!(writer, "flag --rules with flag --format=github is not allowed").unwrap();
+    }
+
+    fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
+        Box::new(GithubReporter)
+    }
+}
diff --git a/apps/oxlint/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs
index 9ebbe2fa312fe..f6f0e12eb849d 100644
--- a/apps/oxlint/src/output_formatter/json.rs
+++ b/apps/oxlint/src/output_formatter/json.rs
@@ -1,14 +1,16 @@
-use miette::JSONReportHandler;
-use oxc_diagnostics::Error;
+use std::io::{BufWriter, Stdout, Write};
+
+use oxc_diagnostics::reporter::{DiagnosticReporter, JsonReporter};
 use oxc_linter::rules::RULES;
 use oxc_linter::RuleCategory;
-use std::io::Write;
 
-#[derive(Debug)]
+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 BufWriter<Stdout>) {
         #[derive(Debug, serde::Serialize)]
         struct RuleInfoJson<'a> {
             scope: &'a str,
@@ -31,18 +33,7 @@ impl JsonOutputFormatter {
             .unwrap();
     }
 
-    pub fn diagnostics<T: Write>(writer: &mut T, 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");
-
-        writer.write_all(format!("[\n{messages}\n]").as_bytes()).unwrap();
+    fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
+        Box::new(JsonReporter::default())
     }
 }
diff --git a/apps/oxlint/src/output_formatter/mod.rs b/apps/oxlint/src/output_formatter/mod.rs
index 5f83dfc4929aa..3df45028deb47 100644
--- a/apps/oxlint/src/output_formatter/mod.rs
+++ b/apps/oxlint/src/output_formatter/mod.rs
@@ -1,16 +1,19 @@
+mod checkstyle;
 mod default;
+mod github;
 mod json;
+mod unix;
 
-use std::io::Write;
+use std::io::{BufWriter, Stdout};
 use std::str::FromStr;
 
-use oxc_diagnostics::Error;
+use checkstyle::CheckStyleOutputFormatter;
+use github::GithubOutputFormatter;
+use unix::UnixOutputFormatter;
 
-use crate::output_formatter::{default::DefaultOutputFormatter, json::JsonOutputFormatter};
+use oxc_diagnostics::reporter::DiagnosticReporter;
 
-pub struct OutputFormatter {
-    format: OutputFormat,
-}
+use crate::output_formatter::{default::DefaultOutputFormatter, json::JsonOutputFormatter};
 
 #[derive(Debug, Clone, Copy, Eq, PartialEq)]
 pub enum OutputFormat {
@@ -38,22 +41,38 @@ impl FromStr for OutputFormat {
     }
 }
 
+trait InternalFormatter {
+    // print all rules which are currently supported by oxlint
+    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>);
+
+    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::<GithubOutputFormatter>::default(),
+            OutputFormat::Unix => Box::<UnixOutputFormatter>::default(),
+            OutputFormat::Default => Box::new(DefaultOutputFormatter),
         }
     }
 
-    pub fn diagnostics<T: Write + std::fmt::Write>(&self, writer: &mut T, diagnostics: &mut Vec<Error>) {
-        match self.format {
-            OutputFormat::Json => JsonOutputFormatter::diagnostics(writer, diagnostics),
-            _ => DefaultOutputFormatter::diagnostics(writer, diagnostics),
-        }
+    // 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()
     }
 }
diff --git a/apps/oxlint/src/output_formatter/unix.rs b/apps/oxlint/src/output_formatter/unix.rs
new file mode 100644
index 0000000000000..ed5c67e5d4316
--- /dev/null
+++ b/apps/oxlint/src/output_formatter/unix.rs
@@ -0,0 +1,18 @@
+use std::io::{BufWriter, Stdout, Write};
+
+use oxc_diagnostics::reporter::{DiagnosticReporter, UnixReporter};
+
+use crate::output_formatter::InternalFormatter;
+
+#[derive(Debug, Default)]
+pub struct UnixOutputFormatter;
+
+impl InternalFormatter for UnixOutputFormatter {
+    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
+        writeln!(writer, "flag --rules with flag --format=unix is not allowed").unwrap();
+    }
+
+    fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
+        Box::new(UnixReporter::default())
+    }
+}
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/checkstyle.rs b/crates/oxc_diagnostics/src/reporter/checkstyle.rs
index ba8ba813af394..182566446f7ef 100644
--- a/crates/oxc_diagnostics/src/reporter/checkstyle.rs
+++ b/crates/oxc_diagnostics/src/reporter/checkstyle.rs
@@ -1,4 +1,7 @@
-use std::borrow::Cow;
+use std::{
+    borrow::Cow,
+    io::{BufWriter, Stdout, Write},
+};
 
 use rustc_hash::FxHashMap;
 
@@ -11,11 +14,11 @@ pub struct CheckstyleReporter {
 }
 
 impl DiagnosticReporter for CheckstyleReporter {
-    fn finish(&mut self) {
-        format_checkstyle(&self.diagnostics);
+    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+        writer.write_all(format_checkstyle(&self.diagnostics).as_bytes()).unwrap();
     }
 
-    fn render_diagnostics(&mut self, _s: &[u8]) {}
+    fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _s: &[u8]) {}
 
     fn render_error(&mut self, error: Error) -> Option<String> {
         self.diagnostics.push(error);
@@ -24,7 +27,7 @@ impl DiagnosticReporter for CheckstyleReporter {
 }
 
 #[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 +51,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>
diff --git a/crates/oxc_diagnostics/src/reporter/github.rs b/crates/oxc_diagnostics/src/reporter/github.rs
index 1fd8d818d5b25..928b2244acf16 100644
--- a/crates/oxc_diagnostics/src/reporter/github.rs
+++ b/crates/oxc_diagnostics/src/reporter/github.rs
@@ -3,32 +3,24 @@ use std::{
     io::{BufWriter, Stdout, Write},
 };
 
-use super::{writer, DiagnosticReporter, Info};
+use super::{DiagnosticReporter, Info};
 use crate::{Error, Severity};
 
 /// 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() }
-    }
-}
+pub struct GithubReporter;
 
 impl DiagnosticReporter for GithubReporter {
-    fn finish(&mut self) {
-        self.writer.flush().unwrap();
+    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+        writer.flush().unwrap();
     }
 
-    fn render_diagnostics(&mut self, _s: &[u8]) {}
+    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
+        writer.write_all(s).unwrap();
+    }
 
     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))
     }
 }
 
diff --git a/crates/oxc_diagnostics/src/reporter/graphical.rs b/crates/oxc_diagnostics/src/reporter/graphical.rs
index 69437a5844a0d..b45006eb5d6fe 100644
--- a/crates/oxc_diagnostics/src/reporter/graphical.rs
+++ b/crates/oxc_diagnostics/src/reporter/graphical.rs
@@ -1,6 +1,6 @@
 use std::io::{BufWriter, ErrorKind, Stdout, Write};
 
-use super::{writer, DiagnosticReporter};
+use super::DiagnosticReporter;
 use crate::{Error, GraphicalReportHandler};
 
 /// Pretty-prints diagnostics. Primarily meant for human-readable output in a terminal.
@@ -8,18 +8,17 @@ use crate::{Error, GraphicalReportHandler};
 /// 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() }
+        Self { handler: GraphicalReportHandler::new() }
     }
 }
 
 impl DiagnosticReporter for GraphicalReporter {
-    fn finish(&mut self) {
-        self.writer
+    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+        writer
             .flush()
             .or_else(|e| {
                 // Do not panic when the process is skill (e.g. piping into `less`).
@@ -32,8 +31,8 @@ impl DiagnosticReporter for GraphicalReporter {
             .unwrap();
     }
 
-    fn render_diagnostics(&mut self, s: &[u8]) {
-        self.writer
+    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
+        writer
             .write_all(s)
             .or_else(|e| {
                 // Do not panic when the process is skill (e.g. piping into `less`).
diff --git a/crates/oxc_diagnostics/src/reporter/json.rs b/crates/oxc_diagnostics/src/reporter/json.rs
index c31b9ea383778..d02aaa6c7cdc7 100644
--- a/crates/oxc_diagnostics/src/reporter/json.rs
+++ b/crates/oxc_diagnostics/src/reporter/json.rs
@@ -1,3 +1,5 @@
+use std::io::{BufWriter, Stdout, Write};
+
 use miette::JSONReportHandler;
 
 use super::DiagnosticReporter;
@@ -15,11 +17,11 @@ pub struct JsonReporter {
 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 finish(&mut self, writer: &mut BufWriter<Stdout>) {
+        writer.write_all(format_json(&mut self.diagnostics).as_bytes()).unwrap();
     }
 
-    fn render_diagnostics(&mut self, _s: &[u8]) {}
+    fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _s: &[u8]) {}
 
     fn render_error(&mut self, error: Error) -> Option<String> {
         self.diagnostics.push(error);
@@ -29,7 +31,7 @@ impl DiagnosticReporter for JsonReporter {
 
 /// <https://github.com/fregante/eslint-formatters/tree/ae1fd9748596447d1fd09625c33d9e7ba9a3d06d/packages/eslint-formatter-json>
 #[allow(clippy::print_stdout)]
-fn format_json(diagnostics: &mut Vec<Error>) {
+fn format_json(diagnostics: &mut Vec<Error>) -> String {
     let handler = JSONReportHandler::new();
     let messages = diagnostics
         .drain(..)
@@ -40,5 +42,5 @@ fn format_json(diagnostics: &mut Vec<Error>) {
         })
         .collect::<Vec<_>>()
         .join(",\n");
-    println!("[\n{messages}\n]");
+    format!("[\n{messages}\n]")
 }
diff --git a/crates/oxc_diagnostics/src/reporter/mod.rs b/crates/oxc_diagnostics/src/reporter/mod.rs
index 469ee6f8127f5..c72eb23bb8869 100644
--- a/crates/oxc_diagnostics/src/reporter/mod.rs
+++ b/crates/oxc_diagnostics/src/reporter/mod.rs
@@ -14,12 +14,6 @@ pub use self::{
 };
 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.
 ///
@@ -76,10 +70,10 @@ pub trait DiagnosticReporter {
     /// upheld in Oxc's API. Do not rely on this behavior.
     ///
     /// [`JSONReporter`]: crate::reporter::JsonReporter
-    fn finish(&mut self);
+    fn finish(&mut self, writer: &mut BufWriter<Stdout>);
 
     /// Write a rendered collection of diagnostics to this reporter's output stream.
-    fn render_diagnostics(&mut self, s: &[u8]);
+    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]);
 
     /// 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
diff --git a/crates/oxc_diagnostics/src/reporter/unix.rs b/crates/oxc_diagnostics/src/reporter/unix.rs
index a3e9ecaa2297b..f400d41cb3498 100644
--- a/crates/oxc_diagnostics/src/reporter/unix.rs
+++ b/crates/oxc_diagnostics/src/reporter/unix.rs
@@ -3,32 +3,26 @@ use std::{
     io::{BufWriter, Stdout, Write},
 };
 
-use super::{writer, DiagnosticReporter, Info};
+use super::{DiagnosticReporter, Info};
 use crate::{Error, Severity};
 
+#[derive(Default)]
 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) {
+    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
         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();
+            writer.write_all(line.as_bytes()).unwrap();
         }
-        self.writer.flush().unwrap();
+        writer.flush().unwrap();
     }
 
-    fn render_diagnostics(&mut self, s: &[u8]) {
-        self.writer.write_all(s).unwrap();
+    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
+        writer.write_all(s).unwrap();
     }
 
     fn render_error(&mut self, error: Error) -> Option<String> {
diff --git a/crates/oxc_diagnostics/src/service.rs b/crates/oxc_diagnostics/src/service.rs
index 89d222cc51e77..2c9acbcebb0c9 100644
--- a/crates/oxc_diagnostics/src/service.rs
+++ b/crates/oxc_diagnostics/src/service.rs
@@ -1,14 +1,12 @@
 use std::{
     cell::Cell,
+    io::{BufWriter, Stdout},
     path::{Path, PathBuf},
     sync::{mpsc, Arc},
 };
 
 use crate::{
-    reporter::{
-        CheckstyleReporter, DiagnosticReporter, GithubReporter, GraphicalReporter, JsonReporter,
-        UnixReporter,
-    },
+    reporter::{DiagnosticReporter, GraphicalReporter},
     Error, NamedSource, OxcDiagnostic, Severity,
 };
 
@@ -73,20 +71,17 @@ pub struct DiagnosticService {
 
 impl Default for DiagnosticService {
     fn default() -> Self {
-        Self::new(GraphicalReporter::default())
+        Self::new(Box::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 +92,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,7 +174,7 @@ impl DiagnosticService {
     /// # Panics
     ///
     /// * When the writer fails to write
-    pub fn run(&mut self) {
+    pub fn run(&mut self, writer: &mut BufWriter<Stdout>) {
         while let Ok(Some((path, diagnostics))) = self.receiver.recv() {
             let mut output = String::new();
             for diagnostic in diagnostics {
@@ -240,9 +216,9 @@ impl DiagnosticService {
                     output.push_str(&err_str);
                 }
             }
-            self.reporter.render_diagnostics(output.as_bytes());
+            self.reporter.render_diagnostics(writer, output.as_bytes());
         }
 
-        self.reporter.finish();
+        self.reporter.finish(writer);
     }
 }

From 178b475d09831c5022b8b4406453aba887fc8b1c Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sun, 12 Jan 2025 20:44:28 +0100
Subject: [PATCH 06/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 crates/oxc_diagnostics/src/service.rs | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/crates/oxc_diagnostics/src/service.rs b/crates/oxc_diagnostics/src/service.rs
index 2c9acbcebb0c9..750c1c4c9540e 100644
--- a/crates/oxc_diagnostics/src/service.rs
+++ b/crates/oxc_diagnostics/src/service.rs
@@ -6,7 +6,7 @@ use std::{
 };
 
 use crate::{
-    reporter::{DiagnosticReporter, GraphicalReporter},
+    reporter::DiagnosticReporter,
     Error, NamedSource, OxcDiagnostic, Severity,
 };
 
@@ -69,12 +69,6 @@ pub struct DiagnosticService {
     receiver: DiagnosticReceiver,
 }
 
-impl Default for DiagnosticService {
-    fn default() -> Self {
-        Self::new(Box::new(GraphicalReporter::default()))
-    }
-}
-
 impl DiagnosticService {
     /// Create a new [`DiagnosticService`] that will render and report diagnostics using the
     /// provided [`DiagnosticReporter`].

From f6ccdcd48cfe6b179428d8ee1e5ba6bf96b86079 Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sun, 12 Jan 2025 20:45:35 +0100
Subject: [PATCH 07/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 crates/oxc_linter/src/tester.rs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/crates/oxc_linter/src/tester.rs b/crates/oxc_linter/src/tester.rs
index bc0a93a224a0b..e9e06018dad73 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;

From 1e74eb047a667f1b4bd00bd3b698f8b18c394030 Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sun, 12 Jan 2025 21:05:37 +0100
Subject: [PATCH 08/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 Cargo.lock                                    |   1 +
 apps/oxlint/Cargo.toml                        |   1 +
 .../oxlint/src/output_formatter/checkstyle.rs | 110 +++++++++++++++++-
 apps/oxlint/src/output_formatter/default.rs   |  53 ++++++++-
 apps/oxlint/src/output_formatter/github.rs    |  76 +++++++++++-
 apps/oxlint/src/output_formatter/json.rs      |  44 ++++++-
 apps/oxlint/src/output_formatter/unix.rs      |  47 +++++++-
 .../src/reporter/checkstyle.rs                | 108 -----------------
 crates/oxc_diagnostics/src/reporter/github.rs |  73 ------------
 .../oxc_diagnostics/src/reporter/graphical.rs |  53 ---------
 crates/oxc_diagnostics/src/reporter/json.rs   |  46 --------
 crates/oxc_diagnostics/src/reporter/mod.rs    |  26 ++---
 crates/oxc_diagnostics/src/reporter/unix.rs   |  44 -------
 crates/oxc_diagnostics/src/service.rs         |   5 +-
 crates/oxc_linter/src/tester.rs               |   2 +-
 15 files changed, 333 insertions(+), 356 deletions(-)
 delete mode 100644 crates/oxc_diagnostics/src/reporter/checkstyle.rs
 delete mode 100644 crates/oxc_diagnostics/src/reporter/github.rs
 delete mode 100644 crates/oxc_diagnostics/src/reporter/graphical.rs
 delete mode 100644 crates/oxc_diagnostics/src/reporter/json.rs
 delete mode 100644 crates/oxc_diagnostics/src/reporter/unix.rs

diff --git a/Cargo.lock b/Cargo.lock
index 3e5b7ae034a71..5b4fc7cd1647c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2217,6 +2217,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 af967c99eb5d3..a1067d69e2378 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/output_formatter/checkstyle.rs b/apps/oxlint/src/output_formatter/checkstyle.rs
index e4c6811efeb3e..2a4a77b1fcceb 100644
--- a/apps/oxlint/src/output_formatter/checkstyle.rs
+++ b/apps/oxlint/src/output_formatter/checkstyle.rs
@@ -1,6 +1,14 @@
-use std::io::{BufWriter, Stdout, Write};
+use std::{
+    borrow::Cow,
+    io::{BufWriter, Stdout, Write},
+};
 
-use oxc_diagnostics::reporter::{CheckstyleReporter, DiagnosticReporter};
+use rustc_hash::FxHashMap;
+
+use oxc_diagnostics::{
+    reporter::{DiagnosticReporter, Info},
+    Error, Severity,
+};
 
 use crate::output_formatter::InternalFormatter;
 
@@ -16,3 +24,101 @@ impl InternalFormatter for CheckStyleOutputFormatter {
         Box::new(CheckstyleReporter::default())
     }
 }
+
+#[derive(Default)]
+pub struct CheckstyleReporter {
+    diagnostics: Vec<Error>,
+}
+
+impl DiagnosticReporter for CheckstyleReporter {
+    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+        writer.write_all(format_checkstyle(&self.diagnostics).as_bytes()).unwrap();
+    }
+
+    fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _s: &[u8]) {}
+
+    fn render_error(&mut self, error: Error) -> Option<String> {
+        self.diagnostics.push(error);
+        None
+    }
+}
+
+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 {
+        grouped.entry(info.filename.clone()).or_default().push(info);
+    }
+    let messages = grouped.into_values().map(|infos| {
+         let messages = infos
+             .iter()
+             .fold(String::new(), |mut acc, info| {
+                 let Info { line, column, message, severity, rule_id, .. } = info;
+                 let severity = match severity {
+                     Severity::Error => "error",
+                     _ => "warning",
+                 };
+                 let message = rule_id.as_ref().map_or_else(|| xml_escape(message), |rule_id| Cow::Owned(format!("{} ({rule_id})", xml_escape(message))));
+                 let source = rule_id.as_ref().map_or_else(|| Cow::Borrowed(""), |rule_id| Cow::Owned(format!("eslint.rules.{rule_id}")));
+                 let line = format!(r#"<error line="{line}" column="{column}" severity="{severity}" message="{message}" source="{source}" />"#);
+                 acc.push_str(&line);
+                 acc
+             });
+         let filename = &infos[0].filename;
+         format!(r#"<file name="{filename}">{messages}</file>"#)
+     }).collect::<Vec<_>>().join(" ");
+    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>
+fn xml_escape(raw: &str) -> Cow<str> {
+    xml_escape_impl(raw, |ch| matches!(ch, b'<' | b'>' | b'&' | b'\'' | b'\"'))
+}
+
+fn xml_escape_impl<F: Fn(u8) -> bool>(raw: &str, escape_chars: F) -> Cow<str> {
+    let bytes = raw.as_bytes();
+    let mut escaped = None;
+    let mut iter = bytes.iter();
+    let mut pos = 0;
+    while let Some(i) = iter.position(|&b| escape_chars(b)) {
+        if escaped.is_none() {
+            escaped = Some(Vec::with_capacity(raw.len()));
+        }
+        let escaped = escaped.as_mut().expect("initialized");
+        let new_pos = pos + i;
+        escaped.extend_from_slice(&bytes[pos..new_pos]);
+        match bytes[new_pos] {
+            b'<' => escaped.extend_from_slice(b"&lt;"),
+            b'>' => escaped.extend_from_slice(b"&gt;"),
+            b'\'' => escaped.extend_from_slice(b"&apos;"),
+            b'&' => escaped.extend_from_slice(b"&amp;"),
+            b'"' => escaped.extend_from_slice(b"&quot;"),
+
+            // This set of escapes handles characters that should be escaped
+            // in elements of xs:lists, because those characters works as
+            // delimiters of list elements
+            b'\t' => escaped.extend_from_slice(b"&#9;"),
+            b'\n' => escaped.extend_from_slice(b"&#10;"),
+            b'\r' => escaped.extend_from_slice(b"&#13;"),
+            b' ' => escaped.extend_from_slice(b"&#32;"),
+            _ => unreachable!(
+                "Only '<', '>','\', '&', '\"', '\\t', '\\r', '\\n', and ' ' are escaped"
+            ),
+        }
+        pos = new_pos + 1;
+    }
+
+    if let Some(mut escaped) = escaped {
+        if let Some(raw) = bytes.get(pos..) {
+            escaped.extend_from_slice(raw);
+        }
+
+        // SAFETY: we operate on UTF-8 input and search for an one byte chars only,
+        // so all slices that was put to the `escaped` is a valid UTF-8 encoded strings
+        Cow::Owned(unsafe { String::from_utf8_unchecked(escaped) })
+    } else {
+        Cow::Borrowed(raw)
+    }
+}
diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
index 1d67e7aa91034..6ec53fe362410 100644
--- a/apps/oxlint/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -1,6 +1,6 @@
-use std::io::{BufWriter, Stdout, Write};
+use std::io::{BufWriter, ErrorKind, Stdout, Write};
 
-use oxc_diagnostics::reporter::{DiagnosticReporter, GraphicalReporter};
+use oxc_diagnostics::{reporter::DiagnosticReporter, Error, GraphicalReportHandler};
 use oxc_linter::table::RuleTable;
 
 use crate::output_formatter::InternalFormatter;
@@ -23,6 +23,55 @@ impl InternalFormatter for DefaultOutputFormatter {
     }
 }
 
+/// 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,
+}
+
+impl Default for GraphicalReporter {
+    fn default() -> Self {
+        Self { handler: GraphicalReportHandler::new() }
+    }
+}
+
+impl DiagnosticReporter for GraphicalReporter {
+    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+        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, writer: &mut BufWriter<Stdout>, s: &[u8]) {
+        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)
+    }
+}
+
 #[cfg(test)]
 mod test {
     use crate::output_formatter::{default::DefaultOutputFormatter, InternalFormatter};
diff --git a/apps/oxlint/src/output_formatter/github.rs b/apps/oxlint/src/output_formatter/github.rs
index 8d0f4bf943b25..42d2d0ffbf9d5 100644
--- a/apps/oxlint/src/output_formatter/github.rs
+++ b/apps/oxlint/src/output_formatter/github.rs
@@ -1,6 +1,12 @@
-use std::io::{BufWriter, Stdout, Write};
+use std::{
+    borrow::Cow,
+    io::{BufWriter, Stdout, Write},
+};
 
-use oxc_diagnostics::reporter::{DiagnosticReporter, GithubReporter};
+use oxc_diagnostics::{
+    reporter::{DiagnosticReporter, Info},
+    Error, Severity,
+};
 
 use crate::output_formatter::InternalFormatter;
 
@@ -16,3 +22,69 @@ impl InternalFormatter for GithubOutputFormatter {
         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, writer: &mut BufWriter<Stdout>) {
+        writer.flush().unwrap();
+    }
+
+    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
+        writer.write_all(s).unwrap();
+    }
+
+    fn render_error(&mut self, error: Error) -> Option<String> {
+        Some(format_github(&error))
+    }
+}
+
+fn format_github(diagnostic: &Error) -> String {
+    let Info { line, column, filename, message, severity, rule_id } = Info::new(diagnostic);
+    let severity = match severity {
+        Severity::Error => "error",
+        Severity::Warning | miette::Severity::Advice => "warning",
+    };
+    let title = rule_id.map_or(Cow::Borrowed("oxlint"), Cow::Owned);
+    let filename = escape_property(&filename);
+    let message = escape_data(&message);
+    format!(
+        "::{severity} file={filename},line={line},endLine={line},col={column},endColumn={column},title={title}::{message}\n"
+    )
+}
+
+fn escape_data(value: &str) -> String {
+    // Refs:
+    // - https://github.com/actions/runner/blob/a4c57f27477077e57545af79851551ff7f5632bd/src/Runner.Common/ActionCommand.cs#L18-L22
+    // - https://github.com/actions/toolkit/blob/fe3e7ce9a7f995d29d1fcfd226a32bca407f9dc8/packages/core/src/command.ts#L80-L94
+    let mut result = String::with_capacity(value.len());
+    for c in value.chars() {
+        match c {
+            '\r' => result.push_str("%0D"),
+            '\n' => result.push_str("%0A"),
+            '%' => result.push_str("%25"),
+            _ => result.push(c),
+        }
+    }
+    result
+}
+
+fn escape_property(value: &str) -> String {
+    // Refs:
+    // - https://github.com/actions/runner/blob/a4c57f27477077e57545af79851551ff7f5632bd/src/Runner.Common/ActionCommand.cs#L25-L32
+    // - https://github.com/actions/toolkit/blob/fe3e7ce9a7f995d29d1fcfd226a32bca407f9dc8/packages/core/src/command.ts#L80-L94
+    let mut result = String::with_capacity(value.len());
+    for c in value.chars() {
+        match c {
+            '\r' => result.push_str("%0D"),
+            '\n' => result.push_str("%0A"),
+            ':' => result.push_str("%3A"),
+            ',' => result.push_str("%2C"),
+            '%' => result.push_str("%25"),
+            _ => result.push(c),
+        }
+    }
+    result
+}
diff --git a/apps/oxlint/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs
index f6f0e12eb849d..74b634974f77f 100644
--- a/apps/oxlint/src/output_formatter/json.rs
+++ b/apps/oxlint/src/output_formatter/json.rs
@@ -1,9 +1,11 @@
 use std::io::{BufWriter, Stdout, Write};
 
-use oxc_diagnostics::reporter::{DiagnosticReporter, JsonReporter};
+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)]
@@ -37,3 +39,43 @@ impl InternalFormatter for JsonOutputFormatter {
         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, writer: &mut BufWriter<Stdout>) {
+        writer.write_all(format_json(&mut self.diagnostics).as_bytes()).unwrap();
+    }
+
+    fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _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>) -> 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]")
+}
diff --git a/apps/oxlint/src/output_formatter/unix.rs b/apps/oxlint/src/output_formatter/unix.rs
index ed5c67e5d4316..cff2b3146e870 100644
--- a/apps/oxlint/src/output_formatter/unix.rs
+++ b/apps/oxlint/src/output_formatter/unix.rs
@@ -1,6 +1,12 @@
-use std::io::{BufWriter, Stdout, Write};
+use std::{
+    borrow::Cow,
+    io::{BufWriter, Stdout, Write},
+};
 
-use oxc_diagnostics::reporter::{DiagnosticReporter, UnixReporter};
+use oxc_diagnostics::{
+    reporter::{DiagnosticReporter, Info},
+    Error, Severity,
+};
 
 use crate::output_formatter::InternalFormatter;
 
@@ -16,3 +22,40 @@ impl InternalFormatter for UnixOutputFormatter {
         Box::new(UnixReporter::default())
     }
 }
+
+#[derive(Default)]
+struct UnixReporter {
+    total: usize,
+}
+
+impl DiagnosticReporter for UnixReporter {
+    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+        let total = self.total;
+        if total > 0 {
+            let line = format!("\n{total} problem{}\n", if total > 1 { "s" } else { "" });
+            writer.write_all(line.as_bytes()).unwrap();
+        }
+        writer.flush().unwrap();
+    }
+
+    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
+        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")
+}
diff --git a/crates/oxc_diagnostics/src/reporter/checkstyle.rs b/crates/oxc_diagnostics/src/reporter/checkstyle.rs
deleted file mode 100644
index 182566446f7ef..0000000000000
--- a/crates/oxc_diagnostics/src/reporter/checkstyle.rs
+++ /dev/null
@@ -1,108 +0,0 @@
-use std::{
-    borrow::Cow,
-    io::{BufWriter, Stdout, Write},
-};
-
-use rustc_hash::FxHashMap;
-
-use super::{DiagnosticReporter, Info};
-use crate::{Error, Severity};
-
-#[derive(Default)]
-pub struct CheckstyleReporter {
-    diagnostics: Vec<Error>,
-}
-
-impl DiagnosticReporter for CheckstyleReporter {
-    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
-        writer.write_all(format_checkstyle(&self.diagnostics).as_bytes()).unwrap();
-    }
-
-    fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _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]) -> String {
-    let infos = diagnostics.iter().map(Info::new).collect::<Vec<_>>();
-    let mut grouped: FxHashMap<String, Vec<Info>> = FxHashMap::default();
-    for info in infos {
-        grouped.entry(info.filename.clone()).or_default().push(info);
-    }
-    let messages = grouped.into_values().map(|infos| {
-         let messages = infos
-             .iter()
-             .fold(String::new(), |mut acc, info| {
-                 let Info { line, column, message, severity, rule_id, .. } = info;
-                 let severity = match severity {
-                     Severity::Error => "error",
-                     _ => "warning",
-                 };
-                 let message = rule_id.as_ref().map_or_else(|| xml_escape(message), |rule_id| Cow::Owned(format!("{} ({rule_id})", xml_escape(message))));
-                 let source = rule_id.as_ref().map_or_else(|| Cow::Borrowed(""), |rule_id| Cow::Owned(format!("eslint.rules.{rule_id}")));
-                 let line = format!(r#"<error line="{line}" column="{column}" severity="{severity}" message="{message}" source="{source}" />"#);
-                 acc.push_str(&line);
-                 acc
-             });
-         let filename = &infos[0].filename;
-         format!(r#"<file name="{filename}">{messages}</file>"#)
-     }).collect::<Vec<_>>().join(" ");
-    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>
-fn xml_escape(raw: &str) -> Cow<str> {
-    xml_escape_impl(raw, |ch| matches!(ch, b'<' | b'>' | b'&' | b'\'' | b'\"'))
-}
-
-fn xml_escape_impl<F: Fn(u8) -> bool>(raw: &str, escape_chars: F) -> Cow<str> {
-    let bytes = raw.as_bytes();
-    let mut escaped = None;
-    let mut iter = bytes.iter();
-    let mut pos = 0;
-    while let Some(i) = iter.position(|&b| escape_chars(b)) {
-        if escaped.is_none() {
-            escaped = Some(Vec::with_capacity(raw.len()));
-        }
-        let escaped = escaped.as_mut().expect("initialized");
-        let new_pos = pos + i;
-        escaped.extend_from_slice(&bytes[pos..new_pos]);
-        match bytes[new_pos] {
-            b'<' => escaped.extend_from_slice(b"&lt;"),
-            b'>' => escaped.extend_from_slice(b"&gt;"),
-            b'\'' => escaped.extend_from_slice(b"&apos;"),
-            b'&' => escaped.extend_from_slice(b"&amp;"),
-            b'"' => escaped.extend_from_slice(b"&quot;"),
-
-            // This set of escapes handles characters that should be escaped
-            // in elements of xs:lists, because those characters works as
-            // delimiters of list elements
-            b'\t' => escaped.extend_from_slice(b"&#9;"),
-            b'\n' => escaped.extend_from_slice(b"&#10;"),
-            b'\r' => escaped.extend_from_slice(b"&#13;"),
-            b' ' => escaped.extend_from_slice(b"&#32;"),
-            _ => unreachable!(
-                "Only '<', '>','\', '&', '\"', '\\t', '\\r', '\\n', and ' ' are escaped"
-            ),
-        }
-        pos = new_pos + 1;
-    }
-
-    if let Some(mut escaped) = escaped {
-        if let Some(raw) = bytes.get(pos..) {
-            escaped.extend_from_slice(raw);
-        }
-
-        // SAFETY: we operate on UTF-8 input and search for an one byte chars only,
-        // so all slices that was put to the `escaped` is a valid UTF-8 encoded strings
-        Cow::Owned(unsafe { String::from_utf8_unchecked(escaped) })
-    } else {
-        Cow::Borrowed(raw)
-    }
-}
diff --git a/crates/oxc_diagnostics/src/reporter/github.rs b/crates/oxc_diagnostics/src/reporter/github.rs
deleted file mode 100644
index 928b2244acf16..0000000000000
--- a/crates/oxc_diagnostics/src/reporter/github.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-use std::{
-    borrow::Cow,
-    io::{BufWriter, Stdout, Write},
-};
-
-use super::{DiagnosticReporter, Info};
-use crate::{Error, Severity};
-
-/// 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;
-
-impl DiagnosticReporter for GithubReporter {
-    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
-        writer.flush().unwrap();
-    }
-
-    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
-        writer.write_all(s).unwrap();
-    }
-
-    fn render_error(&mut self, error: Error) -> Option<String> {
-        Some(format_github(&error))
-    }
-}
-
-fn format_github(diagnostic: &Error) -> String {
-    let Info { line, column, filename, message, severity, rule_id } = Info::new(diagnostic);
-    let severity = match severity {
-        Severity::Error => "error",
-        Severity::Warning | miette::Severity::Advice => "warning",
-    };
-    let title = rule_id.map_or(Cow::Borrowed("oxlint"), Cow::Owned);
-    let filename = escape_property(&filename);
-    let message = escape_data(&message);
-    format!(
-        "::{severity} file={filename},line={line},endLine={line},col={column},endColumn={column},title={title}::{message}\n"
-    )
-}
-
-fn escape_data(value: &str) -> String {
-    // Refs:
-    // - https://github.com/actions/runner/blob/a4c57f27477077e57545af79851551ff7f5632bd/src/Runner.Common/ActionCommand.cs#L18-L22
-    // - https://github.com/actions/toolkit/blob/fe3e7ce9a7f995d29d1fcfd226a32bca407f9dc8/packages/core/src/command.ts#L80-L94
-    let mut result = String::with_capacity(value.len());
-    for c in value.chars() {
-        match c {
-            '\r' => result.push_str("%0D"),
-            '\n' => result.push_str("%0A"),
-            '%' => result.push_str("%25"),
-            _ => result.push(c),
-        }
-    }
-    result
-}
-
-fn escape_property(value: &str) -> String {
-    // Refs:
-    // - https://github.com/actions/runner/blob/a4c57f27477077e57545af79851551ff7f5632bd/src/Runner.Common/ActionCommand.cs#L25-L32
-    // - https://github.com/actions/toolkit/blob/fe3e7ce9a7f995d29d1fcfd226a32bca407f9dc8/packages/core/src/command.ts#L80-L94
-    let mut result = String::with_capacity(value.len());
-    for c in value.chars() {
-        match c {
-            '\r' => result.push_str("%0D"),
-            '\n' => result.push_str("%0A"),
-            ':' => result.push_str("%3A"),
-            ',' => result.push_str("%2C"),
-            '%' => result.push_str("%25"),
-            _ => result.push(c),
-        }
-    }
-    result
-}
diff --git a/crates/oxc_diagnostics/src/reporter/graphical.rs b/crates/oxc_diagnostics/src/reporter/graphical.rs
deleted file mode 100644
index b45006eb5d6fe..0000000000000
--- a/crates/oxc_diagnostics/src/reporter/graphical.rs
+++ /dev/null
@@ -1,53 +0,0 @@
-use std::io::{BufWriter, ErrorKind, Stdout, Write};
-
-use super::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,
-}
-
-impl Default for GraphicalReporter {
-    fn default() -> Self {
-        Self { handler: GraphicalReportHandler::new() }
-    }
-}
-
-impl DiagnosticReporter for GraphicalReporter {
-    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
-        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, writer: &mut BufWriter<Stdout>, s: &[u8]) {
-        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)
-    }
-}
diff --git a/crates/oxc_diagnostics/src/reporter/json.rs b/crates/oxc_diagnostics/src/reporter/json.rs
deleted file mode 100644
index d02aaa6c7cdc7..0000000000000
--- a/crates/oxc_diagnostics/src/reporter/json.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-use std::io::{BufWriter, Stdout, Write};
-
-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, writer: &mut BufWriter<Stdout>) {
-        writer.write_all(format_json(&mut self.diagnostics).as_bytes()).unwrap();
-    }
-
-    fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _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>) -> 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]")
-}
diff --git a/crates/oxc_diagnostics/src/reporter/mod.rs b/crates/oxc_diagnostics/src/reporter/mod.rs
index c72eb23bb8869..e731ffe0a2dbc 100644
--- a/crates/oxc_diagnostics/src/reporter/mod.rs
+++ b/crates/oxc_diagnostics/src/reporter/mod.rs
@@ -1,17 +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};
 
 /// Reporters are responsible for rendering diagnostics to some format and writing them to some
@@ -84,17 +74,17 @@ pub trait DiagnosticReporter {
     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();
diff --git a/crates/oxc_diagnostics/src/reporter/unix.rs b/crates/oxc_diagnostics/src/reporter/unix.rs
deleted file mode 100644
index f400d41cb3498..0000000000000
--- a/crates/oxc_diagnostics/src/reporter/unix.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-use std::{
-    borrow::Cow,
-    io::{BufWriter, Stdout, Write},
-};
-
-use super::{DiagnosticReporter, Info};
-use crate::{Error, Severity};
-
-#[derive(Default)]
-pub struct UnixReporter {
-    total: usize,
-}
-
-impl DiagnosticReporter for UnixReporter {
-    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
-        let total = self.total;
-        if total > 0 {
-            let line = format!("\n{total} problem{}\n", if total > 1 { "s" } else { "" });
-            writer.write_all(line.as_bytes()).unwrap();
-        }
-        writer.flush().unwrap();
-    }
-
-    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
-        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")
-}
diff --git a/crates/oxc_diagnostics/src/service.rs b/crates/oxc_diagnostics/src/service.rs
index 750c1c4c9540e..e00cbb34ea16f 100644
--- a/crates/oxc_diagnostics/src/service.rs
+++ b/crates/oxc_diagnostics/src/service.rs
@@ -5,10 +5,7 @@ use std::{
     sync::{mpsc, Arc},
 };
 
-use crate::{
-    reporter::DiagnosticReporter,
-    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>>;
diff --git a/crates/oxc_linter/src/tester.rs b/crates/oxc_linter/src/tester.rs
index e9e06018dad73..515588bec374a 100644
--- a/crates/oxc_linter/src/tester.rs
+++ b/crates/oxc_linter/src/tester.rs
@@ -1,7 +1,7 @@
 use std::{
     env,
     path::{Path, PathBuf},
-    sync::mpsc
+    sync::mpsc,
 };
 
 use cow_utils::CowUtils;

From 5350b5d27f81a9d1b2d5c1bbbc656647408aa0f3 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Sun, 12 Jan 2025 20:07:05 +0000
Subject: [PATCH 09/20] [autofix.ci] apply automated fixes

---
 crates/oxc_diagnostics/Cargo.toml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/crates/oxc_diagnostics/Cargo.toml b/crates/oxc_diagnostics/Cargo.toml
index 870ca995f0275..db41f17198cc1 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 }

From 8d80193a49c161b6746dfd7649a04c4335f709f9 Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Sun, 12 Jan 2025 20:07:56 +0000
Subject: [PATCH 10/20] [autofix.ci] apply automated fixes (attempt 2/3)

---
 Cargo.lock | 1 -
 1 file changed, 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock
index 5b4fc7cd1647c..77d459cb29737 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1693,7 +1693,6 @@ name = "oxc_diagnostics"
 version = "0.45.0"
 dependencies = [
  "oxc-miette",
- "rustc-hash",
 ]
 
 [[package]]

From 23d6dc8578294e3d6352c8a386b116e274d65fff Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sun, 12 Jan 2025 23:18:10 +0100
Subject: [PATCH 11/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 apps/oxlint/src/output_formatter/checkstyle.rs | 5 +++--
 apps/oxlint/src/output_formatter/default.rs    | 4 ++--
 apps/oxlint/src/output_formatter/github.rs     | 2 +-
 apps/oxlint/src/output_formatter/json.rs       | 3 ++-
 apps/oxlint/src/output_formatter/unix.rs       | 2 +-
 crates/oxc_diagnostics/src/reporter/mod.rs     | 4 ++--
 6 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/apps/oxlint/src/output_formatter/checkstyle.rs b/apps/oxlint/src/output_formatter/checkstyle.rs
index 2a4a77b1fcceb..031b7649416ee 100644
--- a/apps/oxlint/src/output_formatter/checkstyle.rs
+++ b/apps/oxlint/src/output_formatter/checkstyle.rs
@@ -26,13 +26,14 @@ impl InternalFormatter for CheckStyleOutputFormatter {
 }
 
 #[derive(Default)]
-pub struct CheckstyleReporter {
+struct CheckstyleReporter {
     diagnostics: Vec<Error>,
 }
 
 impl DiagnosticReporter for CheckstyleReporter {
-    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+    fn finish(&mut self, writer: &mut dyn Write) {
         writer.write_all(format_checkstyle(&self.diagnostics).as_bytes()).unwrap();
+        writer.flush().unwrap();
     }
 
     fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _s: &[u8]) {}
diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
index 6ec53fe362410..ab42900d7e71e 100644
--- a/apps/oxlint/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -26,7 +26,7 @@ impl InternalFormatter for DefaultOutputFormatter {
 /// 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 {
+struct GraphicalReporter {
     handler: GraphicalReportHandler,
 }
 
@@ -37,7 +37,7 @@ impl Default for GraphicalReporter {
 }
 
 impl DiagnosticReporter for GraphicalReporter {
-    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+    fn finish(&mut self, writer: &mut dyn Write) {
         writer
             .flush()
             .or_else(|e| {
diff --git a/apps/oxlint/src/output_formatter/github.rs b/apps/oxlint/src/output_formatter/github.rs
index 42d2d0ffbf9d5..74eb1c6dce806 100644
--- a/apps/oxlint/src/output_formatter/github.rs
+++ b/apps/oxlint/src/output_formatter/github.rs
@@ -28,7 +28,7 @@ impl InternalFormatter for GithubOutputFormatter {
 struct GithubReporter;
 
 impl DiagnosticReporter for GithubReporter {
-    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+    fn finish(&mut self, writer: &mut dyn Write) {
         writer.flush().unwrap();
     }
 
diff --git a/apps/oxlint/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs
index 74b634974f77f..717408ad7a2d5 100644
--- a/apps/oxlint/src/output_formatter/json.rs
+++ b/apps/oxlint/src/output_formatter/json.rs
@@ -52,8 +52,9 @@ struct JsonReporter {
 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, writer: &mut BufWriter<Stdout>) {
+    fn finish(&mut self, writer: &mut dyn Write) {
         writer.write_all(format_json(&mut self.diagnostics).as_bytes()).unwrap();
+        writer.flush().unwrap();
     }
 
     fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _s: &[u8]) {}
diff --git a/apps/oxlint/src/output_formatter/unix.rs b/apps/oxlint/src/output_formatter/unix.rs
index cff2b3146e870..f643ba730cc37 100644
--- a/apps/oxlint/src/output_formatter/unix.rs
+++ b/apps/oxlint/src/output_formatter/unix.rs
@@ -29,7 +29,7 @@ struct UnixReporter {
 }
 
 impl DiagnosticReporter for UnixReporter {
-    fn finish(&mut self, writer: &mut BufWriter<Stdout>) {
+    fn finish(&mut self, writer: &mut dyn Write) {
         let total = self.total;
         if total > 0 {
             let line = format!("\n{total} problem{}\n", if total > 1 { "s" } else { "" });
diff --git a/crates/oxc_diagnostics/src/reporter/mod.rs b/crates/oxc_diagnostics/src/reporter/mod.rs
index e731ffe0a2dbc..0c94a202361d0 100644
--- a/crates/oxc_diagnostics/src/reporter/mod.rs
+++ b/crates/oxc_diagnostics/src/reporter/mod.rs
@@ -1,6 +1,6 @@
 //! [Reporters](DiagnosticReporter) for rendering and writing diagnostics.
 
-use std::io::{BufWriter, Stdout};
+use std::io::{BufWriter, Stdout, Write};
 
 use crate::{Error, Severity};
 
@@ -60,7 +60,7 @@ pub trait DiagnosticReporter {
     /// upheld in Oxc's API. Do not rely on this behavior.
     ///
     /// [`JSONReporter`]: crate::reporter::JsonReporter
-    fn finish(&mut self, writer: &mut BufWriter<Stdout>);
+    fn finish(&mut self, writer: &mut dyn Write);
 
     /// Write a rendered collection of diagnostics to this reporter's output stream.
     fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]);

From 5be6cb46b5d02582a8de3ae62fb0bca2695a354b Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sun, 12 Jan 2025 23:23:43 +0100
Subject: [PATCH 12/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 apps/oxlint/src/output_formatter/checkstyle.rs |  9 +++------
 apps/oxlint/src/output_formatter/default.rs    | 11 +++++------
 apps/oxlint/src/output_formatter/github.rs     |  9 +++------
 apps/oxlint/src/output_formatter/json.rs       |  6 +++---
 apps/oxlint/src/output_formatter/mod.rs        |  4 ++--
 apps/oxlint/src/output_formatter/unix.rs       |  9 +++------
 crates/oxc_diagnostics/src/reporter/mod.rs     |  4 ++--
 7 files changed, 21 insertions(+), 31 deletions(-)

diff --git a/apps/oxlint/src/output_formatter/checkstyle.rs b/apps/oxlint/src/output_formatter/checkstyle.rs
index 031b7649416ee..529a2e4ca4c92 100644
--- a/apps/oxlint/src/output_formatter/checkstyle.rs
+++ b/apps/oxlint/src/output_formatter/checkstyle.rs
@@ -1,7 +1,4 @@
-use std::{
-    borrow::Cow,
-    io::{BufWriter, Stdout, Write},
-};
+use std::{borrow::Cow, io::Write};
 
 use rustc_hash::FxHashMap;
 
@@ -16,7 +13,7 @@ use crate::output_formatter::InternalFormatter;
 pub struct CheckStyleOutputFormatter;
 
 impl InternalFormatter for CheckStyleOutputFormatter {
-    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
+    fn all_rules(&mut self, writer: &mut dyn Write) {
         writeln!(writer, "flag --rules with flag --format=checkstyle is not allowed").unwrap();
     }
 
@@ -36,7 +33,7 @@ impl DiagnosticReporter for CheckstyleReporter {
         writer.flush().unwrap();
     }
 
-    fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _s: &[u8]) {}
+    fn render_diagnostics(&mut self, _writer: &mut dyn Write, _s: &[u8]) {}
 
     fn render_error(&mut self, error: Error) -> Option<String> {
         self.diagnostics.push(error);
diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
index ab42900d7e71e..58ef6feb61944 100644
--- a/apps/oxlint/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -1,4 +1,4 @@
-use std::io::{BufWriter, ErrorKind, Stdout, Write};
+use std::io::{ErrorKind, Write};
 
 use oxc_diagnostics::{reporter::DiagnosticReporter, Error, GraphicalReportHandler};
 use oxc_linter::table::RuleTable;
@@ -9,7 +9,7 @@ use crate::output_formatter::InternalFormatter;
 pub struct DefaultOutputFormatter;
 
 impl InternalFormatter for DefaultOutputFormatter {
-    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
+    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();
@@ -51,7 +51,7 @@ impl DiagnosticReporter for GraphicalReporter {
             .unwrap();
     }
 
-    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
+    fn render_diagnostics(&mut self, writer: &mut dyn Write, s: &[u8]) {
         writer
             .write_all(s)
             .or_else(|e| {
@@ -75,14 +75,13 @@ impl DiagnosticReporter for GraphicalReporter {
 #[cfg(test)]
 mod test {
     use crate::output_formatter::{default::DefaultOutputFormatter, InternalFormatter};
-    use std::io::BufWriter;
 
     #[test]
     fn all_rules() {
-        let mut writer = BufWriter::new(std::io::stdout());
+        let mut writer = Vec::new();
         let mut formatter = DefaultOutputFormatter;
 
         formatter.all_rules(&mut writer);
-        assert!(!writer.buffer().is_empty());
+        assert!(!writer.is_empty());
     }
 }
diff --git a/apps/oxlint/src/output_formatter/github.rs b/apps/oxlint/src/output_formatter/github.rs
index 74eb1c6dce806..056633389d077 100644
--- a/apps/oxlint/src/output_formatter/github.rs
+++ b/apps/oxlint/src/output_formatter/github.rs
@@ -1,7 +1,4 @@
-use std::{
-    borrow::Cow,
-    io::{BufWriter, Stdout, Write},
-};
+use std::{borrow::Cow, io::Write};
 
 use oxc_diagnostics::{
     reporter::{DiagnosticReporter, Info},
@@ -14,7 +11,7 @@ use crate::output_formatter::InternalFormatter;
 pub struct GithubOutputFormatter;
 
 impl InternalFormatter for GithubOutputFormatter {
-    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
+    fn all_rules(&mut self, writer: &mut dyn Write) {
         writeln!(writer, "flag --rules with flag --format=github is not allowed").unwrap();
     }
 
@@ -32,7 +29,7 @@ impl DiagnosticReporter for GithubReporter {
         writer.flush().unwrap();
     }
 
-    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
+    fn render_diagnostics(&mut self, writer: &mut dyn Write, s: &[u8]) {
         writer.write_all(s).unwrap();
     }
 
diff --git a/apps/oxlint/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs
index 717408ad7a2d5..52256496a86d3 100644
--- a/apps/oxlint/src/output_formatter/json.rs
+++ b/apps/oxlint/src/output_formatter/json.rs
@@ -1,4 +1,4 @@
-use std::io::{BufWriter, Stdout, Write};
+use std::io::Write;
 
 use oxc_diagnostics::{reporter::DiagnosticReporter, Error};
 use oxc_linter::rules::RULES;
@@ -12,7 +12,7 @@ use crate::output_formatter::InternalFormatter;
 pub struct JsonOutputFormatter;
 
 impl InternalFormatter for JsonOutputFormatter {
-    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
+    fn all_rules(&mut self, writer: &mut dyn Write) {
         #[derive(Debug, serde::Serialize)]
         struct RuleInfoJson<'a> {
             scope: &'a str,
@@ -57,7 +57,7 @@ impl DiagnosticReporter for JsonReporter {
         writer.flush().unwrap();
     }
 
-    fn render_diagnostics(&mut self, _writer: &mut BufWriter<Stdout>, _s: &[u8]) {}
+    fn render_diagnostics(&mut self, _writer: &mut dyn Write, _s: &[u8]) {}
 
     fn render_error(&mut self, error: Error) -> Option<String> {
         self.diagnostics.push(error);
diff --git a/apps/oxlint/src/output_formatter/mod.rs b/apps/oxlint/src/output_formatter/mod.rs
index 3df45028deb47..70c852e501eb2 100644
--- a/apps/oxlint/src/output_formatter/mod.rs
+++ b/apps/oxlint/src/output_formatter/mod.rs
@@ -4,7 +4,7 @@ mod github;
 mod json;
 mod unix;
 
-use std::io::{BufWriter, Stdout};
+use std::io::{BufWriter, Stdout, Write};
 use std::str::FromStr;
 
 use checkstyle::CheckStyleOutputFormatter;
@@ -43,7 +43,7 @@ impl FromStr for OutputFormat {
 
 trait InternalFormatter {
     // print all rules which are currently supported by oxlint
-    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>);
+    fn all_rules(&mut self, writer: &mut dyn Write);
 
     fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter>;
 }
diff --git a/apps/oxlint/src/output_formatter/unix.rs b/apps/oxlint/src/output_formatter/unix.rs
index f643ba730cc37..95b411ba23513 100644
--- a/apps/oxlint/src/output_formatter/unix.rs
+++ b/apps/oxlint/src/output_formatter/unix.rs
@@ -1,7 +1,4 @@
-use std::{
-    borrow::Cow,
-    io::{BufWriter, Stdout, Write},
-};
+use std::{borrow::Cow, io::Write};
 
 use oxc_diagnostics::{
     reporter::{DiagnosticReporter, Info},
@@ -14,7 +11,7 @@ use crate::output_formatter::InternalFormatter;
 pub struct UnixOutputFormatter;
 
 impl InternalFormatter for UnixOutputFormatter {
-    fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
+    fn all_rules(&mut self, writer: &mut dyn Write) {
         writeln!(writer, "flag --rules with flag --format=unix is not allowed").unwrap();
     }
 
@@ -38,7 +35,7 @@ impl DiagnosticReporter for UnixReporter {
         writer.flush().unwrap();
     }
 
-    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]) {
+    fn render_diagnostics(&mut self, writer: &mut dyn Write, s: &[u8]) {
         writer.write_all(s).unwrap();
     }
 
diff --git a/crates/oxc_diagnostics/src/reporter/mod.rs b/crates/oxc_diagnostics/src/reporter/mod.rs
index 0c94a202361d0..e63ad3b79cb27 100644
--- a/crates/oxc_diagnostics/src/reporter/mod.rs
+++ b/crates/oxc_diagnostics/src/reporter/mod.rs
@@ -1,6 +1,6 @@
 //! [Reporters](DiagnosticReporter) for rendering and writing diagnostics.
 
-use std::io::{BufWriter, Stdout, Write};
+use std::io::Write;
 
 use crate::{Error, Severity};
 
@@ -63,7 +63,7 @@ pub trait DiagnosticReporter {
     fn finish(&mut self, writer: &mut dyn Write);
 
     /// Write a rendered collection of diagnostics to this reporter's output stream.
-    fn render_diagnostics(&mut self, writer: &mut BufWriter<Stdout>, s: &[u8]);
+    fn render_diagnostics(&mut self, writer: &mut dyn Write, s: &[u8]);
 
     /// 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

From 0e979d75d3b19b00c8eb31c4eb0a368d5f2b7900 Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Sun, 12 Jan 2025 23:25:09 +0100
Subject: [PATCH 13/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 crates/oxc_diagnostics/src/reporter/mod.rs | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/crates/oxc_diagnostics/src/reporter/mod.rs b/crates/oxc_diagnostics/src/reporter/mod.rs
index e63ad3b79cb27..1840cf11a049d 100644
--- a/crates/oxc_diagnostics/src/reporter/mod.rs
+++ b/crates/oxc_diagnostics/src/reporter/mod.rs
@@ -53,13 +53,11 @@ 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
+    /// that. 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, writer: &mut dyn Write);
 
     /// Write a rendered collection of diagnostics to this reporter's output stream.

From 6a3ff9eea39cfb0e38b06fe70956c24ab71da06f Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Mon, 13 Jan 2025 00:12:02 +0100
Subject: [PATCH 14/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 .../oxlint/src/output_formatter/checkstyle.rs |  7 +--
 apps/oxlint/src/output_formatter/default.rs   | 30 ++-----------
 apps/oxlint/src/output_formatter/github.rs    |  8 +---
 apps/oxlint/src/output_formatter/json.rs      |  7 +--
 apps/oxlint/src/output_formatter/unix.rs      | 10 ++---
 crates/oxc_diagnostics/src/reporter/mod.rs    |  7 +--
 crates/oxc_diagnostics/src/service.rs         | 43 +++++++++++++++++--
 7 files changed, 52 insertions(+), 60 deletions(-)

diff --git a/apps/oxlint/src/output_formatter/checkstyle.rs b/apps/oxlint/src/output_formatter/checkstyle.rs
index 529a2e4ca4c92..13eba0d0dcf00 100644
--- a/apps/oxlint/src/output_formatter/checkstyle.rs
+++ b/apps/oxlint/src/output_formatter/checkstyle.rs
@@ -28,13 +28,10 @@ struct CheckstyleReporter {
 }
 
 impl DiagnosticReporter for CheckstyleReporter {
-    fn finish(&mut self, writer: &mut dyn Write) {
-        writer.write_all(format_checkstyle(&self.diagnostics).as_bytes()).unwrap();
-        writer.flush().unwrap();
+    fn finish(&mut self) -> Option<String> {
+        Some(format_checkstyle(&self.diagnostics))
     }
 
-    fn render_diagnostics(&mut self, _writer: &mut dyn Write, _s: &[u8]) {}
-
     fn render_error(&mut self, error: Error) -> Option<String> {
         self.diagnostics.push(error);
         None
diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
index 58ef6feb61944..28ef6555f36f8 100644
--- a/apps/oxlint/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -1,4 +1,4 @@
-use std::io::{ErrorKind, Write};
+use std::io::Write;
 
 use oxc_diagnostics::{reporter::DiagnosticReporter, Error, GraphicalReportHandler};
 use oxc_linter::table::RuleTable;
@@ -37,32 +37,8 @@ impl Default for GraphicalReporter {
 }
 
 impl DiagnosticReporter for GraphicalReporter {
-    fn finish(&mut self, writer: &mut dyn Write) {
-        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, writer: &mut dyn Write, s: &[u8]) {
-        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 finish(&mut self) -> Option<String> {
+        None
     }
 
     fn render_error(&mut self, error: Error) -> Option<String> {
diff --git a/apps/oxlint/src/output_formatter/github.rs b/apps/oxlint/src/output_formatter/github.rs
index 056633389d077..1c25715cf96c8 100644
--- a/apps/oxlint/src/output_formatter/github.rs
+++ b/apps/oxlint/src/output_formatter/github.rs
@@ -25,12 +25,8 @@ impl InternalFormatter for GithubOutputFormatter {
 struct GithubReporter;
 
 impl DiagnosticReporter for GithubReporter {
-    fn finish(&mut self, writer: &mut dyn Write) {
-        writer.flush().unwrap();
-    }
-
-    fn render_diagnostics(&mut self, writer: &mut dyn Write, s: &[u8]) {
-        writer.write_all(s).unwrap();
+    fn finish(&mut self) -> Option<String> {
+        None
     }
 
     fn render_error(&mut self, error: Error) -> Option<String> {
diff --git a/apps/oxlint/src/output_formatter/json.rs b/apps/oxlint/src/output_formatter/json.rs
index 52256496a86d3..a701515c37c39 100644
--- a/apps/oxlint/src/output_formatter/json.rs
+++ b/apps/oxlint/src/output_formatter/json.rs
@@ -52,13 +52,10 @@ struct JsonReporter {
 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, writer: &mut dyn Write) {
-        writer.write_all(format_json(&mut self.diagnostics).as_bytes()).unwrap();
-        writer.flush().unwrap();
+    fn finish(&mut self) -> Option<String> {
+        Some(format_json(&mut self.diagnostics))
     }
 
-    fn render_diagnostics(&mut self, _writer: &mut dyn Write, _s: &[u8]) {}
-
     fn render_error(&mut self, error: Error) -> Option<String> {
         self.diagnostics.push(error);
         None
diff --git a/apps/oxlint/src/output_formatter/unix.rs b/apps/oxlint/src/output_formatter/unix.rs
index 95b411ba23513..e3608c6f90d66 100644
--- a/apps/oxlint/src/output_formatter/unix.rs
+++ b/apps/oxlint/src/output_formatter/unix.rs
@@ -26,17 +26,13 @@ struct UnixReporter {
 }
 
 impl DiagnosticReporter for UnixReporter {
-    fn finish(&mut self, writer: &mut dyn Write) {
+    fn finish(&mut self) -> Option<String> {
         let total = self.total;
         if total > 0 {
-            let line = format!("\n{total} problem{}\n", if total > 1 { "s" } else { "" });
-            writer.write_all(line.as_bytes()).unwrap();
+            return Some(format!("\n{total} problem{}\n", if total > 1 { "s" } else { "" }));
         }
-        writer.flush().unwrap();
-    }
 
-    fn render_diagnostics(&mut self, writer: &mut dyn Write, s: &[u8]) {
-        writer.write_all(s).unwrap();
+        None
     }
 
     fn render_error(&mut self, error: Error) -> Option<String> {
diff --git a/crates/oxc_diagnostics/src/reporter/mod.rs b/crates/oxc_diagnostics/src/reporter/mod.rs
index 1840cf11a049d..9ecf5d7735d95 100644
--- a/crates/oxc_diagnostics/src/reporter/mod.rs
+++ b/crates/oxc_diagnostics/src/reporter/mod.rs
@@ -1,7 +1,5 @@
 //! [Reporters](DiagnosticReporter) for rendering and writing diagnostics.
 
-use std::io::Write;
-
 use crate::{Error, Severity};
 
 /// Reporters are responsible for rendering diagnostics to some format and writing them to some
@@ -58,10 +56,7 @@ pub trait DiagnosticReporter {
     ///
     /// 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.
-    fn finish(&mut self, writer: &mut dyn Write);
-
-    /// Write a rendered collection of diagnostics to this reporter's output stream.
-    fn render_diagnostics(&mut self, writer: &mut dyn Write, 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
diff --git a/crates/oxc_diagnostics/src/service.rs b/crates/oxc_diagnostics/src/service.rs
index e00cbb34ea16f..24d08f79db57a 100644
--- a/crates/oxc_diagnostics/src/service.rs
+++ b/crates/oxc_diagnostics/src/service.rs
@@ -1,6 +1,6 @@
 use std::{
     cell::Cell,
-    io::{BufWriter, Stdout},
+    io::{ErrorKind, Write},
     path::{Path, PathBuf},
     sync::{mpsc, Arc},
 };
@@ -165,7 +165,7 @@ impl DiagnosticService {
     /// # Panics
     ///
     /// * When the writer fails to write
-    pub fn run(&mut self, writer: &mut BufWriter<Stdout>) {
+    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 {
@@ -207,9 +207,44 @@ impl DiagnosticService {
                     output.push_str(&err_str);
                 }
             }
-            self.reporter.render_diagnostics(writer, output.as_bytes());
+
+            writer
+                .write_all(output.as_bytes())
+                .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();
+        }
+
+        if let Some(finish_output) = self.reporter.finish() {
+            writer
+                .write_all(finish_output.as_bytes())
+                .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();
         }
 
-        self.reporter.finish(writer);
+        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();
     }
 }

From 7b0403a9a94c0cf28eaf82c5f3cf1bbd248acf43 Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Mon, 13 Jan 2025 01:03:36 +0100
Subject: [PATCH 15/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 .../src/{reporter/mod.rs => reporter.rs}      | 30 +++++--------------
 1 file changed, 8 insertions(+), 22 deletions(-)
 rename crates/oxc_diagnostics/src/{reporter/mod.rs => reporter.rs} (77%)

diff --git a/crates/oxc_diagnostics/src/reporter/mod.rs b/crates/oxc_diagnostics/src/reporter.rs
similarity index 77%
rename from crates/oxc_diagnostics/src/reporter/mod.rs
rename to crates/oxc_diagnostics/src/reporter.rs
index 9ecf5d7735d95..34443c273a934 100644
--- a/crates/oxc_diagnostics/src/reporter/mod.rs
+++ b/crates/oxc_diagnostics/src/reporter.rs
@@ -10,28 +10,16 @@ use crate::{Error, Severity};
 ///
 /// ## 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
@@ -50,8 +38,7 @@ use crate::{Error, Severity};
 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
@@ -62,8 +49,7 @@ pub trait DiagnosticReporter {
     /// 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>;
 }
 

From 4bdd3418421abf53dd7fb2a0699f539beaf5b5c9 Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Mon, 13 Jan 2025 19:37:01 +0100
Subject: [PATCH 16/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 .../oxlint/src/output_formatter/checkstyle.rs | 28 ++++++++++++++++
 apps/oxlint/src/output_formatter/default.rs   | 33 ++++++++++++++++++-
 apps/oxlint/src/output_formatter/github.rs    | 32 +++++++++++++++++-
 apps/oxlint/src/output_formatter/json.rs      | 31 +++++++++++++++++
 apps/oxlint/src/output_formatter/mod.rs       |  2 +-
 apps/oxlint/src/output_formatter/unix.rs      | 30 +++++++++++++++++
 6 files changed, 153 insertions(+), 3 deletions(-)

diff --git a/apps/oxlint/src/output_formatter/checkstyle.rs b/apps/oxlint/src/output_formatter/checkstyle.rs
index 13eba0d0dcf00..5a847718d2ffd 100644
--- a/apps/oxlint/src/output_formatter/checkstyle.rs
+++ b/apps/oxlint/src/output_formatter/checkstyle.rs
@@ -117,3 +117,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>");
+    }
+}
diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
index 28ef6555f36f8..f7b06f0acb6be 100644
--- a/apps/oxlint/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -50,7 +50,13 @@ impl DiagnosticReporter for GraphicalReporter {
 
 #[cfg(test)]
 mod test {
-    use crate::output_formatter::{default::DefaultOutputFormatter, InternalFormatter};
+    use crate::output_formatter::{
+        default::{DefaultOutputFormatter, GraphicalReporter},
+        InternalFormatter,
+    };
+    use miette::NamedSource;
+    use oxc_diagnostics::{reporter::DiagnosticReporter, OxcDiagnostic};
+    use oxc_span::Span;
 
     #[test]
     fn all_rules() {
@@ -60,4 +66,29 @@ mod test {
         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();
+        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  \u{1b}[38;2;244;191;117;1m⚠\u{1b}[0m \u{1b}[38;2;244;191;117;1merror message\u{1b}[0m\n   ╭─[\u{1b}[38;2;92;157;255;1mfile://test.ts\u{1b}[0m:1:1]\n \u{1b}[2m1\u{1b}[0m │ debugger;\n   · \u{1b}[38;2;246;87;248m────────\u{1b}[0m\n   ╰────\n"
+        );
+    }
 }
diff --git a/apps/oxlint/src/output_formatter/github.rs b/apps/oxlint/src/output_formatter/github.rs
index 1c25715cf96c8..d6b9a33481835 100644
--- a/apps/oxlint/src/output_formatter/github.rs
+++ b/apps/oxlint/src/output_formatter/github.rs
@@ -7,7 +7,7 @@ use oxc_diagnostics::{
 
 use crate::output_formatter::InternalFormatter;
 
-#[derive(Debug, Default)]
+#[derive(Debug)]
 pub struct GithubOutputFormatter;
 
 impl InternalFormatter for GithubOutputFormatter {
@@ -81,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 a701515c37c39..7e1cdd946f192 100644
--- a/apps/oxlint/src/output_formatter/json.rs
+++ b/apps/oxlint/src/output_formatter/json.rs
@@ -77,3 +77,34 @@ fn format_json(diagnostics: &mut Vec<Error>) -> String {
         .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 70c852e501eb2..4b437eb69e9dd 100644
--- a/apps/oxlint/src/output_formatter/mod.rs
+++ b/apps/oxlint/src/output_formatter/mod.rs
@@ -61,7 +61,7 @@ impl OutputFormatter {
         match format {
             OutputFormat::Json => Box::<JsonOutputFormatter>::default(),
             OutputFormat::Checkstyle => Box::<CheckStyleOutputFormatter>::default(),
-            OutputFormat::Github => Box::<GithubOutputFormatter>::default(),
+            OutputFormat::Github => Box::new(GithubOutputFormatter),
             OutputFormat::Unix => Box::<UnixOutputFormatter>::default(),
             OutputFormat::Default => Box::new(DefaultOutputFormatter),
         }
diff --git a/apps/oxlint/src/output_formatter/unix.rs b/apps/oxlint/src/output_formatter/unix.rs
index e3608c6f90d66..01412baee0b9c 100644
--- a/apps/oxlint/src/output_formatter/unix.rs
+++ b/apps/oxlint/src/output_formatter/unix.rs
@@ -52,3 +52,33 @@ fn format_unix(diagnostic: &Error) -> String {
         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() {
+        let mut reporter = UnixReporter::default();
+
+        let result = reporter.finish();
+
+        assert!(result.is_none());
+    }
+
+    #[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");
+    }
+}

From 3404567e4477d0639f00b57e1175af3600b6c002 Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Mon, 13 Jan 2025 20:00:09 +0100
Subject: [PATCH 17/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 .../oxlint/src/output_formatter/checkstyle.rs |  3 +++
 apps/oxlint/src/output_formatter/unix.rs      | 19 ++++++++++++++++++-
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/apps/oxlint/src/output_formatter/checkstyle.rs b/apps/oxlint/src/output_formatter/checkstyle.rs
index 5a847718d2ffd..57b8077f72f11 100644
--- a/apps/oxlint/src/output_formatter/checkstyle.rs
+++ b/apps/oxlint/src/output_formatter/checkstyle.rs
@@ -22,6 +22,9 @@ impl InternalFormatter for CheckStyleOutputFormatter {
     }
 }
 
+/// Reporter to output diagnostics in checkstyle format
+/// 
+/// Checkstyle Format Documentation: <https://checkstyle.sourceforge.io/>
 #[derive(Default)]
 struct CheckstyleReporter {
     diagnostics: Vec<Error>,
diff --git a/apps/oxlint/src/output_formatter/unix.rs b/apps/oxlint/src/output_formatter/unix.rs
index 01412baee0b9c..4c34f334ebd4d 100644
--- a/apps/oxlint/src/output_formatter/unix.rs
+++ b/apps/oxlint/src/output_formatter/unix.rs
@@ -20,6 +20,8 @@ impl InternalFormatter for UnixOutputFormatter {
     }
 }
 
+/// 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,
@@ -61,7 +63,7 @@ mod test {
     use super::UnixReporter;
 
     #[test]
-    fn reporter_finish() {
+    fn reporter_finish_empty() {
         let mut reporter = UnixReporter::default();
 
         let result = reporter.finish();
@@ -69,6 +71,21 @@ mod test {
         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();

From e720048e4c2fd00a66f2be3acbd6ecf3aad1d51c Mon Sep 17 00:00:00 2001
From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com>
Date: Mon, 13 Jan 2025 19:01:35 +0000
Subject: [PATCH 18/20] [autofix.ci] apply automated fixes

---
 apps/oxlint/src/output_formatter/checkstyle.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apps/oxlint/src/output_formatter/checkstyle.rs b/apps/oxlint/src/output_formatter/checkstyle.rs
index 57b8077f72f11..05655fb3dfad4 100644
--- a/apps/oxlint/src/output_formatter/checkstyle.rs
+++ b/apps/oxlint/src/output_formatter/checkstyle.rs
@@ -23,7 +23,7 @@ impl InternalFormatter for CheckStyleOutputFormatter {
 }
 
 /// Reporter to output diagnostics in checkstyle format
-/// 
+///
 /// Checkstyle Format Documentation: <https://checkstyle.sourceforge.io/>
 #[derive(Default)]
 struct CheckstyleReporter {

From 049893dbdb4fdcb6fd20a99e815016d02b13be0b Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Tue, 14 Jan 2025 19:04:05 +0100
Subject: [PATCH 19/20] refactor(linter): move DiagnosticsReporters to oxlint

---
 apps/oxlint/src/output_formatter/default.rs | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/apps/oxlint/src/output_formatter/default.rs b/apps/oxlint/src/output_formatter/default.rs
index f7b06f0acb6be..838bf7720243d 100644
--- a/apps/oxlint/src/output_formatter/default.rs
+++ b/apps/oxlint/src/output_formatter/default.rs
@@ -47,6 +47,13 @@ impl DiagnosticReporter for GraphicalReporter {
         Some(output)
     }
 }
+impl GraphicalReporter {
+    #[cfg(test)]
+    pub fn with_handler(mut self, handler: GraphicalReportHandler) -> Self {
+        self.handler = handler;
+        self
+    }
+}
 
 #[cfg(test)]
 mod test {
@@ -54,7 +61,7 @@ mod test {
         default::{DefaultOutputFormatter, GraphicalReporter},
         InternalFormatter,
     };
-    use miette::NamedSource;
+    use miette::{GraphicalReportHandler, GraphicalTheme, NamedSource};
     use oxc_diagnostics::{reporter::DiagnosticReporter, OxcDiagnostic};
     use oxc_span::Span;
 
@@ -78,7 +85,10 @@ mod test {
 
     #[test]
     fn reporter_error() {
-        let mut reporter = GraphicalReporter::default();
+        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;"));
@@ -88,7 +98,7 @@ mod test {
         assert!(result.is_some());
         assert_eq!(
             result.unwrap(),
-            "\n  \u{1b}[38;2;244;191;117;1m⚠\u{1b}[0m \u{1b}[38;2;244;191;117;1merror message\u{1b}[0m\n   ╭─[\u{1b}[38;2;92;157;255;1mfile://test.ts\u{1b}[0m:1:1]\n \u{1b}[2m1\u{1b}[0m │ debugger;\n   · \u{1b}[38;2;246;87;248m────────\u{1b}[0m\n   ╰────\n"
+            "\n  ! error message\n   ,-[file://test.ts:1:1]\n 1 | debugger;\n   : ^^^^^^^^\n   `----\n"
         );
     }
 }

From 749e1488b46c6e664f8c1de7890e6370de858469 Mon Sep 17 00:00:00 2001
From: Sysix <sysix@sysix-coding.de>
Date: Wed, 15 Jan 2025 22:06:18 +0100
Subject: [PATCH 20/20] refactor(linter): move DiagnosticsReporters to oxlint -
 optimize service, do not cache output

---
 crates/oxc_diagnostics/src/service.rs | 60 +++++++++++----------------
 1 file changed, 24 insertions(+), 36 deletions(-)

diff --git a/crates/oxc_diagnostics/src/service.rs b/crates/oxc_diagnostics/src/service.rs
index 24d08f79db57a..b46c2678f63d3 100644
--- a/crates/oxc_diagnostics/src/service.rs
+++ b/crates/oxc_diagnostics/src/service.rs
@@ -167,7 +167,6 @@ impl DiagnosticService {
     /// * When the writer fails to write
     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);
@@ -192,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) {
@@ -200,51 +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();
                 }
             }
-
-            writer
-                .write_all(output.as_bytes())
-                .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();
         }
 
         if let Some(finish_output) = self.reporter.finish() {
             writer
                 .write_all(finish_output.as_bytes())
-                .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)
-                    }
-                })
+                .or_else(Self::check_for_writer_error)
                 .unwrap();
         }
 
-        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();
+        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)
+        }
     }
 }