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"<"), + b'>' => escaped.extend_from_slice(b">"), + b'\'' => escaped.extend_from_slice(b"'"), + b'&' => escaped.extend_from_slice(b"&"), + b'"' => escaped.extend_from_slice(b"""), + + // 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"	"), + b'\n' => escaped.extend_from_slice(b" "), + b'\r' => escaped.extend_from_slice(b" "), + b' ' => escaped.extend_from_slice(b" "), + _ => 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"<"), - b'>' => escaped.extend_from_slice(b">"), - b'\'' => escaped.extend_from_slice(b"'"), - b'&' => escaped.extend_from_slice(b"&"), - b'"' => escaped.extend_from_slice(b"""), - - // 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"	"), - b'\n' => escaped.extend_from_slice(b" "), - b'\r' => escaped.extend_from_slice(b" "), - b' ' => escaped.extend_from_slice(b" "), - _ => 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) + } } }