diff --git a/.changeset/all-pumas-stop.md b/.changeset/all-pumas-stop.md new file mode 100644 index 000000000000..d9ebe0cc7a7d --- /dev/null +++ b/.changeset/all-pumas-stop.md @@ -0,0 +1,31 @@ +--- +"@biomejs/biome": minor +--- + +Added support for multiple reporters, and the ability to save reporters on arbitrary files. + +#### Combine two reporters in CI + +If you run Biome on GitHub, take advantage of the reporter and still see the errors in console, you can now use both reporters: + +```shell +biome ci --reporter=default --reporter=github +``` + +#### Save reporter output to a file + +With the new `--reporter-file` CLI option, it's now possible to save the output of all reporters to a file. The file is a path, +so you can pass a relative or an absolute path: + +```shell +biome ci --reporter=rdjson --reporter-file=/etc/tmp/report.json +biome ci --reporter=summary --reporter-file=./reports/file.txt +``` + +You can combine these two features. For example, have the `default` reporter written on terminal, and the `rdjson` reporter written on file: + +```shell +biome ci --reporter=default --reporter=rdjson --reporter-file=/etc/tmp/report.json +``` + +**The `--reporter` and `--reporter-file` flags must appear next to each other, otherwise an error is thrown.** diff --git a/Cargo.lock b/Cargo.lock index c6ba4433ecf2..b506c95d7f5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,6 +208,7 @@ dependencies = [ "biome_js_formatter", "biome_js_syntax", "biome_json_analyze", + "biome_json_factory", "biome_json_formatter", "biome_json_parser", "biome_json_syntax", diff --git a/crates/biome_cli/Cargo.toml b/crates/biome_cli/Cargo.toml index 8f0331470a89..51dfd3949008 100644 --- a/crates/biome_cli/Cargo.toml +++ b/crates/biome_cli/Cargo.toml @@ -41,6 +41,7 @@ biome_js_analyze = { workspace = true } biome_js_formatter = { workspace = true } biome_js_syntax = { workspace = true } biome_json_analyze = { workspace = true } +biome_json_factory = { workspace = true } biome_json_formatter = { workspace = true } biome_json_parser = { workspace = true } biome_json_syntax = { workspace = true } diff --git a/crates/biome_cli/src/cli_options.rs b/crates/biome_cli/src/cli_options.rs index 7dac898b5c69..026c16cfc7dd 100644 --- a/crates/biome_cli/src/cli_options.rs +++ b/crates/biome_cli/src/cli_options.rs @@ -52,12 +52,8 @@ pub struct CliOptions { pub error_on_warnings: bool, /// Allows to change how diagnostics and summary are reported. - #[bpaf( - long("reporter"), - argument("json|json-pretty|github|junit|summary|gitlab|checkstyle|rdjson|sarif"), - fallback(CliReporter::default()) - )] - pub reporter: CliReporter, + #[bpaf(external, many)] + pub cli_reporter: Vec, /// The level of diagnostics to show. In order, from the lowest to the most important: info, warn, error. Passing `--diagnostic-level=error` will cause Biome to print only diagnostics that contain only errors. #[bpaf( @@ -119,8 +115,28 @@ impl FromStr for ColorsArg { } } +#[derive(Debug, Default, Clone, Eq, PartialEq, Bpaf)] +#[bpaf(adjacent)] +pub struct CliReporter { + #[bpaf( + long("reporter"), + argument("default|json|json-pretty|github|junit|summary|gitlab|checkstyle|rdjson|sarif"), + fallback(CliReporterKind::default()) + )] + pub(crate) kind: CliReporterKind, + + #[bpaf(long("reporter-file"), argument("PATH"))] + pub(crate) destination: Option, +} + +impl CliReporter { + pub(crate) fn is_file_report(&self) -> bool { + self.destination.is_some() + } +} + #[derive(Debug, Default, Clone, Eq, PartialEq)] -pub enum CliReporter { +pub enum CliReporterKind { /// The default reporter #[default] Default, @@ -146,15 +162,16 @@ pub enum CliReporter { impl CliReporter { pub(crate) const fn is_default(&self) -> bool { - matches!(self, Self::Default) + matches!(self.kind, CliReporterKind::Default) } } -impl FromStr for CliReporter { +impl FromStr for CliReporterKind { type Err = String; fn from_str(s: &str) -> Result { match s { + "default" => Ok(Self::Default), "json" => Ok(Self::Json), "json-pretty" => Ok(Self::JsonPretty), "summary" => Ok(Self::Summary), @@ -171,19 +188,19 @@ impl FromStr for CliReporter { } } -impl Display for CliReporter { +impl Display for CliReporterKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::Default => f.write_str("default"), - Self::Json => f.write_str("json"), - Self::JsonPretty => f.write_str("json-pretty"), - Self::Summary => f.write_str("summary"), - Self::GitHub => f.write_str("github"), - Self::Junit => f.write_str("junit"), - Self::GitLab => f.write_str("gitlab"), - Self::Checkstyle => f.write_str("checkstyle"), - Self::RdJson => f.write_str("rdjson"), - Self::Sarif => f.write_str("sarif"), + Self::Default { .. } => f.write_str("default"), + Self::Json { .. } => f.write_str("json"), + Self::JsonPretty { .. } => f.write_str("json-pretty"), + Self::Summary { .. } => f.write_str("summary"), + Self::GitHub { .. } => f.write_str("github"), + Self::Junit { .. } => f.write_str("junit"), + Self::GitLab { .. } => f.write_str("gitlab"), + Self::Checkstyle { .. } => f.write_str("checkstyle"), + Self::RdJson { .. } => f.write_str("rdjson"), + Self::Sarif { .. } => f.write_str("sarif"), } } } diff --git a/crates/biome_cli/src/commands/mod.rs b/crates/biome_cli/src/commands/mod.rs index b2397c96f089..1233050c092f 100644 --- a/crates/biome_cli/src/commands/mod.rs +++ b/crates/biome_cli/src/commands/mod.rs @@ -1,5 +1,5 @@ use crate::changed::{get_changed_files, get_staged_files}; -use crate::cli_options::{CliOptions, CliReporter, ColorsArg, cli_options}; +use crate::cli_options::{CliOptions, CliReporterKind, ColorsArg, cli_options}; use crate::logging::log_options; use crate::logging::{LogOptions, LoggingKind}; use crate::{CliDiagnostic, LoggingLevel, VERSION}; @@ -708,11 +708,15 @@ impl BiomeCommand { } } - pub const fn get_color(&self) -> Option<&ColorsArg> { + pub fn get_color(&self) -> Option<&ColorsArg> { match self.cli_options() { Some(cli_options) => { // To properly display GitHub annotations we need to disable colors - if matches!(cli_options.reporter, CliReporter::GitHub) { + if cli_options + .cli_reporter + .iter() + .any(|r| r.kind == CliReporterKind::GitHub) + { return Some(&ColorsArg::Off); } // We want force colors in CI, to give e better UX experience diff --git a/crates/biome_cli/src/lib.rs b/crates/biome_cli/src/lib.rs index 9b8552639582..aae1f0355047 100644 --- a/crates/biome_cli/src/lib.rs +++ b/crates/biome_cli/src/lib.rs @@ -45,10 +45,6 @@ pub(crate) const VERSION: &str = match option_env!("BIOME_VERSION") { None => env!("CARGO_PKG_VERSION"), }; -/// JSON file that is temporarily to handle internal files via [Workspace]. -/// When using this file, make sure to close it via [Workspace::close_file]. -pub const TEMPORARY_INTERNAL_REPORTER_FILE: &str = "__BIOME_INTERNAL_FILE__.json"; - /// Global context for an execution of the CLI pub struct CliSession<'app> { /// Instance of [App] used by this run of the CLI diff --git a/crates/biome_cli/src/reporter/checkstyle.rs b/crates/biome_cli/src/reporter/checkstyle.rs index 94941bf6bcc4..2405a90adfc7 100644 --- a/crates/biome_cli/src/reporter/checkstyle.rs +++ b/crates/biome_cli/src/reporter/checkstyle.rs @@ -1,7 +1,7 @@ -use crate::reporter::{Reporter, ReporterVisitor}; +use crate::reporter::{Reporter, ReporterVisitor, ReporterWriter}; use crate::runner::execution::Execution; use crate::{DiagnosticsPayload, TraversalSummary}; -use biome_console::{Console, ConsoleExt, markup}; +use biome_console::markup; use biome_diagnostics::display::SourceFile; use biome_diagnostics::{Error, PrintDescription, Resource, Severity}; use camino::{Utf8Path, Utf8PathBuf}; @@ -10,16 +10,21 @@ use std::io::{self, Write}; pub struct CheckstyleReporter<'a> { pub summary: TraversalSummary, - pub diagnostics_payload: DiagnosticsPayload, + pub diagnostics_payload: &'a DiagnosticsPayload, pub execution: &'a dyn Execution, pub verbose: bool, pub(crate) working_directory: Option, } impl Reporter for CheckstyleReporter<'_> { - fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { - visitor.report_summary(self.execution, self.summary, self.verbose)?; + fn write( + self, + writer: &mut dyn ReporterWriter, + visitor: &mut dyn ReporterVisitor, + ) -> io::Result<()> { + visitor.report_summary(writer, self.execution, self.summary, self.verbose)?; visitor.report_diagnostics( + writer, self.execution, self.diagnostics_payload, self.verbose, @@ -29,19 +34,12 @@ impl Reporter for CheckstyleReporter<'_> { } } -pub struct CheckstyleReporterVisitor<'a> { - console: &'a mut dyn Console, -} - -impl<'a> CheckstyleReporterVisitor<'a> { - pub fn new(console: &'a mut dyn Console) -> Self { - Self { console } - } -} +pub struct CheckstyleReporterVisitor; -impl<'a> ReporterVisitor for CheckstyleReporterVisitor<'a> { +impl ReporterVisitor for CheckstyleReporterVisitor { fn report_summary( &mut self, + _writer: &mut dyn ReporterWriter, _execution: &dyn Execution, _summary: TraversalSummary, _verbose: bool, @@ -51,8 +49,9 @@ impl<'a> ReporterVisitor for CheckstyleReporterVisitor<'a> { fn report_diagnostics( &mut self, + writer: &mut dyn ReporterWriter, _execution: &dyn Execution, - payload: DiagnosticsPayload, + payload: &DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, ) -> io::Result<()> { @@ -109,7 +108,7 @@ impl<'a> ReporterVisitor for CheckstyleReporterVisitor<'a> { writeln!(output, " ")?; } writeln!(output, "")?; - self.console.log(markup! {{ + writer.log(markup! {{ (String::from_utf8_lossy(&output)) }}); Ok(()) diff --git a/crates/biome_cli/src/reporter/github.rs b/crates/biome_cli/src/reporter/github.rs index 3bad67750569..ef6ef3afa8f5 100644 --- a/crates/biome_cli/src/reporter/github.rs +++ b/crates/biome_cli/src/reporter/github.rs @@ -1,21 +1,27 @@ -use crate::reporter::{Reporter, ReporterVisitor}; +use crate::reporter::{Reporter, ReporterVisitor, ReporterWriter}; use crate::runner::execution::Execution; use crate::{DiagnosticsPayload, TraversalSummary}; -use biome_console::{Console, ConsoleExt, markup}; +use biome_console::markup; use biome_diagnostics::PrintGitHubDiagnostic; use camino::{Utf8Path, Utf8PathBuf}; use std::io; pub(crate) struct GithubReporter<'a> { - pub(crate) diagnostics_payload: DiagnosticsPayload, + pub diagnostics_payload: &'a DiagnosticsPayload, pub(crate) execution: &'a dyn Execution, pub(crate) verbose: bool, pub(crate) working_directory: Option, } impl Reporter for GithubReporter<'_> { - fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { + fn write( + self, + writer: &mut dyn ReporterWriter, + + visitor: &mut dyn ReporterVisitor, + ) -> io::Result<()> { visitor.report_diagnostics( + writer, self.execution, self.diagnostics_payload, self.verbose, @@ -24,11 +30,12 @@ impl Reporter for GithubReporter<'_> { Ok(()) } } -pub(crate) struct GithubReporterVisitor<'a>(pub(crate) &'a mut dyn Console); +pub(crate) struct GithubReporterVisitor; -impl ReporterVisitor for GithubReporterVisitor<'_> { +impl ReporterVisitor for GithubReporterVisitor { fn report_summary( &mut self, + _writer: &mut dyn ReporterWriter, _execution: &dyn Execution, _summary: TraversalSummary, _verbose: bool, @@ -38,17 +45,18 @@ impl ReporterVisitor for GithubReporterVisitor<'_> { fn report_diagnostics( &mut self, + writer: &mut dyn ReporterWriter, _execution: &dyn Execution, - diagnostics_payload: DiagnosticsPayload, + diagnostics_payload: &DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, ) -> io::Result<()> { for diagnostic in &diagnostics_payload.diagnostics { if diagnostic.severity() >= diagnostics_payload.diagnostic_level { - if diagnostic.tags().is_verbose() && verbose { - self.0.log(markup! {{PrintGitHubDiagnostic(diagnostic)}}); - } else if !verbose { - self.0.log(markup! {{PrintGitHubDiagnostic(diagnostic)}}); + if !diagnostic.tags().is_verbose() { + writer.log(markup! {{PrintGitHubDiagnostic(diagnostic)}}); + } else if diagnostic.tags().is_verbose() && verbose { + writer.log(markup! {{PrintGitHubDiagnostic(diagnostic)}}); } } } diff --git a/crates/biome_cli/src/reporter/gitlab.rs b/crates/biome_cli/src/reporter/gitlab.rs index 2ff9c5bf08f2..af758005c137 100644 --- a/crates/biome_cli/src/reporter/gitlab.rs +++ b/crates/biome_cli/src/reporter/gitlab.rs @@ -1,8 +1,8 @@ -use crate::reporter::{Reporter, ReporterVisitor}; +use crate::reporter::{Reporter, ReporterVisitor, ReporterWriter}; use crate::runner::execution::Execution; use crate::{DiagnosticsPayload, TraversalSummary}; use biome_console::fmt::{Display, Formatter}; -use biome_console::{Console, ConsoleExt, markup}; +use biome_console::markup; use biome_diagnostics::display::SourceFile; use biome_diagnostics::{Error, PrintDescription, Resource, Severity}; use biome_rowan::{TextRange, TextSize}; @@ -18,16 +18,22 @@ use std::{ pub struct GitLabReporter<'a> { pub(crate) execution: &'a dyn Execution, - pub(crate) diagnostics: DiagnosticsPayload, + pub(crate) diagnostics_payload: &'a DiagnosticsPayload, pub(crate) verbose: bool, pub(crate) working_directory: Option, } impl Reporter for GitLabReporter<'_> { - fn write(self, visitor: &mut dyn ReporterVisitor) -> std::io::Result<()> { + fn write( + self, + writer: &mut dyn ReporterWriter, + + visitor: &mut dyn ReporterVisitor, + ) -> std::io::Result<()> { visitor.report_diagnostics( + writer, self.execution, - self.diagnostics, + self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), )?; @@ -35,8 +41,7 @@ impl Reporter for GitLabReporter<'_> { } } -pub(crate) struct GitLabReporterVisitor<'a> { - console: &'a mut dyn Console, +pub(crate) struct GitLabReporterVisitor { repository_root: Option, } @@ -59,18 +64,16 @@ impl GitLabHasher { } } -impl<'a> GitLabReporterVisitor<'a> { - pub fn new(console: &'a mut dyn Console, repository_root: Option) -> Self { - Self { - console, - repository_root, - } +impl GitLabReporterVisitor { + pub fn new(repository_root: Option) -> Self { + Self { repository_root } } } -impl ReporterVisitor for GitLabReporterVisitor<'_> { +impl ReporterVisitor for GitLabReporterVisitor { fn report_summary( &mut self, + _writer: &mut dyn ReporterWriter, _: &dyn Execution, _: TraversalSummary, _verbose: bool, @@ -80,8 +83,9 @@ impl ReporterVisitor for GitLabReporterVisitor<'_> { fn report_diagnostics( &mut self, + writer: &mut dyn ReporterWriter, _execution: &dyn Execution, - payload: DiagnosticsPayload, + payload: &DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, ) -> std::io::Result<()> { @@ -92,13 +96,13 @@ impl ReporterVisitor for GitLabReporterVisitor<'_> { path: self.repository_root.as_deref(), verbose, }; - self.console.log(markup!({ diagnostics })); + writer.log(markup!({ diagnostics })); Ok(()) } } struct GitLabDiagnostics<'a> { - payload: DiagnosticsPayload, + payload: &'a DiagnosticsPayload, verbose: bool, lock: &'a RwLock, path: Option<&'a Utf8Path>, diff --git a/crates/biome_cli/src/reporter/json.rs b/crates/biome_cli/src/reporter/json.rs index 7492ac1293d5..dac8fe9f2c60 100644 --- a/crates/biome_cli/src/reporter/json.rs +++ b/crates/biome_cli/src/reporter/json.rs @@ -1,7 +1,14 @@ -use crate::reporter::{Reporter, ReporterVisitor}; +use crate::reporter::{Reporter, ReporterVisitor, ReporterWriter}; use crate::runner::execution::Execution; use crate::{DiagnosticsPayload, TraversalSummary}; -use biome_console::fmt::Formatter; +use biome_console::fmt::{Display, Formatter}; +use biome_console::{MarkupBuf, markup}; +use biome_diagnostics::display::{SourceFile, markup_to_string}; +use biome_diagnostics::{ + Category, Error, Location, LogCategory, PrintDescription, Severity, Visit, +}; +use biome_json_factory::make::*; +use biome_json_syntax::{AnyJsonMemberName, AnyJsonValue, JsonRoot, JsonSyntaxKind, T}; use camino::{Utf8Path, Utf8PathBuf}; use serde::Serialize; @@ -9,7 +16,7 @@ use serde::Serialize; #[serde(rename_all = "camelCase")] pub(crate) struct JsonReporterVisitor { summary: TraversalSummary, - diagnostics: Vec, + diagnostics: Vec, command: String, } @@ -21,9 +28,191 @@ impl JsonReporterVisitor { command: String::new(), } } + + pub(crate) fn to_json(&self) -> JsonRoot { + let diagnostics_elements: Vec = + self.diagnostics.iter().map(report_to_json).collect(); + + let diagnostics_separators = + vec![token(T![,]); diagnostics_elements.len().saturating_sub(1)]; + + let root_members = vec![ + self.summary.json_member(), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal( + "diagnostics", + ))), + token(T![:]), + AnyJsonValue::JsonArrayValue(json_array_value( + token(T!['[']), + json_array_element_list(diagnostics_elements, diagnostics_separators), + token(T![']']), + )), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("command"))), + token(T![:]), + AnyJsonValue::JsonStringValue(json_string_value(json_string_literal( + &self.command, + ))), + ), + ]; + + let root_separators = vec![token(T![,]); root_members.len() - 1]; + + json_root( + AnyJsonValue::JsonObjectValue(json_object_value( + token(T!['{']), + json_member_list(root_members, root_separators), + token(T!['}']), + )), + token(JsonSyntaxKind::EOF), + ) + .build() + } +} + +fn location_span_to_json(span: &LocationSpan) -> AnyJsonValue { + let members = vec![ + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("line"))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal(span.line))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("column"))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal(span.column))), + ), + ]; + let separators = vec![token(T![,])]; + + AnyJsonValue::JsonObjectValue(json_object_value( + token(T!['{']), + json_member_list(members, separators), + token(T!['}']), + )) +} + +fn location_report_to_json(location: &LocationReport) -> AnyJsonValue { + let members = vec![ + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("path"))), + token(T![:]), + AnyJsonValue::JsonStringValue(json_string_value(json_string_literal(&location.path))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("start"))), + token(T![:]), + location_span_to_json(&location.start), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("end"))), + token(T![:]), + location_span_to_json(&location.end), + ), + ]; + let separators = vec![token(T![,]); members.len() - 1]; + + AnyJsonValue::JsonObjectValue(json_object_value( + token(T!['{']), + json_member_list(members, separators), + token(T!['}']), + )) +} + +fn suggestion_to_json(suggestion: &JsonSuggestion) -> AnyJsonValue { + let members = vec![ + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("start"))), + token(T![:]), + location_span_to_json(&suggestion.start), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("end"))), + token(T![:]), + location_span_to_json(&suggestion.end), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("text"))), + token(T![:]), + AnyJsonValue::JsonStringValue(json_string_value(json_string_literal(&suggestion.text))), + ), + ]; + let separators = vec![token(T![,]); members.len() - 1]; + + AnyJsonValue::JsonObjectValue(json_object_value( + token(T!['{']), + json_member_list(members, separators), + token(T!['}']), + )) +} + +fn report_to_json(report: &JsonReport) -> AnyJsonValue { + let severity_str = match report.severity { + Severity::Hint => "hint", + Severity::Information => "info", + Severity::Warning => "warning", + Severity::Error => "error", + Severity::Fatal => "fatal", + }; + + let mut members = vec![ + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("severity"))), + token(T![:]), + AnyJsonValue::JsonStringValue(json_string_value(json_string_literal(severity_str))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("message"))), + token(T![:]), + AnyJsonValue::JsonStringValue(json_string_value(json_string_literal(&report.message))), + ), + ]; + + // Add category if present + if let Some(category) = report.category { + members.push(json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("category"))), + token(T![:]), + AnyJsonValue::JsonStringValue(json_string_value(json_string_literal(category.name()))), + )); + } + + // Add location if present + if let Some(ref location) = report.location { + members.push(json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("location"))), + token(T![:]), + location_report_to_json(location), + )); + } + + // Add advices array + let advice_elements: Vec = + report.advices.iter().map(suggestion_to_json).collect(); + let advice_separators = vec![token(T![,]); advice_elements.len().saturating_sub(1)]; + + members.push(json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("advices"))), + token(T![:]), + AnyJsonValue::JsonArrayValue(json_array_value( + token(T!['[']), + json_array_element_list(advice_elements, advice_separators), + token(T![']']), + )), + )); + + let separators = vec![token(T![,]); members.len() - 1]; + + AnyJsonValue::JsonObjectValue(json_object_value( + token(T!['{']), + json_member_list(members, separators), + token(T!['}']), + )) } -impl biome_console::fmt::Display for JsonReporterVisitor { +impl Display for JsonReporterVisitor { fn fmt(&self, fmt: &mut Formatter) -> std::io::Result<()> { let content = serde_json::to_string(&self)?; fmt.write_str(content.as_str()) @@ -32,18 +221,23 @@ impl biome_console::fmt::Display for JsonReporterVisitor { pub struct JsonReporter<'a> { pub execution: &'a dyn Execution, - pub diagnostics: DiagnosticsPayload, + pub diagnostics_payload: &'a DiagnosticsPayload, pub summary: TraversalSummary, pub verbose: bool, pub working_directory: Option, } impl Reporter for JsonReporter<'_> { - fn write(self, visitor: &mut dyn ReporterVisitor) -> std::io::Result<()> { - visitor.report_summary(self.execution, self.summary, self.verbose)?; + fn write( + self, + writer: &mut dyn ReporterWriter, + visitor: &mut dyn ReporterVisitor, + ) -> std::io::Result<()> { + visitor.report_summary(writer, self.execution, self.summary, self.verbose)?; visitor.report_diagnostics( + writer, self.execution, - self.diagnostics, + self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), )?; @@ -55,6 +249,7 @@ impl Reporter for JsonReporter<'_> { impl ReporterVisitor for JsonReporterVisitor { fn report_summary( &mut self, + _writer: &mut dyn ReporterWriter, execution: &dyn Execution, summary: TraversalSummary, _verbose: bool, @@ -67,24 +262,172 @@ impl ReporterVisitor for JsonReporterVisitor { fn report_diagnostics( &mut self, + _writer: &mut dyn ReporterWriter, _execution: &dyn Execution, - payload: DiagnosticsPayload, + payload: &DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, ) -> std::io::Result<()> { - for diagnostic in payload.diagnostics { + for diagnostic in &payload.diagnostics { if diagnostic.severity() >= payload.diagnostic_level { if diagnostic.tags().is_verbose() { if verbose { - self.diagnostics - .push(biome_diagnostics::serde::Diagnostic::new(diagnostic)) + self.diagnostics.push(to_json_report(diagnostic)) } } else { - self.diagnostics - .push(biome_diagnostics::serde::Diagnostic::new(diagnostic)) + self.diagnostics.push(to_json_report(diagnostic)) } } } Ok(()) } } + +fn to_json_report(diagnostic: &biome_diagnostics::Error) -> JsonReport { + let category = diagnostic.category(); + let severity = diagnostic.severity(); + let message = PrintDescription(diagnostic).to_string(); + let location = diagnostic.location(); + let location = to_location(&location).or_else(|| { + let location = location + .resource + .and_then(|location| location.as_file().map(|f| f.to_string()))?; + Some(LocationReport { + path: location, + start: LocationSpan { column: 0, line: 0 }, + end: LocationSpan { column: 0, line: 0 }, + }) + }); + let advices = to_advices(diagnostic); + + JsonReport { + category, + message, + severity, + location, + advices, + } +} + +fn to_advices(diagnostic: &Error) -> Vec { + let mut visitor = SuggestionsVisitor { + suggestions: vec![], + last_diagnostic_length: 0, + current_message: None, + }; + diagnostic.advices(&mut visitor).unwrap(); + + visitor.suggestions +} + +fn to_location(location: &Location) -> Option { + let (Some(span), Some(source_code), Some(resource)) = + (location.span, location.source_code, location.resource) + else { + return None; + }; + let resource = resource.as_file()?; + let source = SourceFile::new(source_code); + let start = source.location(span.start()).ok()?; + let end = source.location(span.end()).ok()?; + Some(LocationReport { + path: resource.to_string(), + start: LocationSpan { + column: start.column_number.get(), + line: start.line_number.get(), + }, + + end: LocationSpan { + column: end.column_number.get(), + line: end.line_number.get(), + }, + }) +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct JsonReport { + category: Option<&'static Category>, + severity: Severity, + message: String, + advices: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + location: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct LocationReport { + path: String, + start: LocationSpan, + end: LocationSpan, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct LocationSpan { + column: usize, + line: usize, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct JsonSuggestion { + start: LocationSpan, + end: LocationSpan, + text: String, +} + +struct SuggestionsVisitor { + suggestions: Vec, + current_message: Option, + last_diagnostic_length: usize, +} + +impl Visit for SuggestionsVisitor { + fn record_log(&mut self, _category: LogCategory, text: &dyn Display) -> std::io::Result<()> { + let message = { + let mut message = MarkupBuf::default(); + let mut fmt = Formatter::new(&mut message); + fmt.write_markup(markup!({ { text } }))?; + markup_to_string(&message).expect("Invalid markup") + }; + let current_diagnostic_length = self.suggestions.len(); + + if self.last_diagnostic_length != current_diagnostic_length { + let last_suggestion = self + .suggestions + .last_mut() + .expect("No suggestions to append to"); + last_suggestion.text = message; + } else if let Some(current_message) = self.current_message.as_mut() { + current_message.push_str(&message); + } else { + self.current_message = Some(message); + } + + Ok(()) + } + + fn record_frame(&mut self, location: Location<'_>) -> std::io::Result<()> { + if let (Some(span), Some(source_code)) = (location.span, location.source_code) { + let source = SourceFile::new(source_code); + let start = source.location(span.start()).expect("Invalid span"); + let end = source.location(span.end()).expect("Invalid span"); + + self.suggestions.push(JsonSuggestion { + end: LocationSpan { + line: end.line_number.get(), + column: end.column_number.get(), + }, + start: LocationSpan { + line: start.line_number.get(), + column: start.column_number.get(), + }, + text: self.current_message.take().unwrap_or_default(), + }) + } + + Ok(()) + } +} diff --git a/crates/biome_cli/src/reporter/junit.rs b/crates/biome_cli/src/reporter/junit.rs index 50ed5209c2dc..45eb7580db5f 100644 --- a/crates/biome_cli/src/reporter/junit.rs +++ b/crates/biome_cli/src/reporter/junit.rs @@ -1,7 +1,7 @@ -use crate::reporter::{Reporter, ReporterVisitor}; +use crate::reporter::{Reporter, ReporterVisitor, ReporterWriter}; use crate::runner::execution::Execution; use crate::{DiagnosticsPayload, TraversalSummary}; -use biome_console::{Console, ConsoleExt, markup}; +use biome_console::markup; use biome_diagnostics::display::SourceFile; use biome_diagnostics::{Error, Resource}; use camino::{Utf8Path, Utf8PathBuf}; @@ -10,7 +10,7 @@ use std::fmt::{Display, Formatter}; use std::io; pub(crate) struct JunitReporter<'a> { - pub(crate) diagnostics_payload: DiagnosticsPayload, + pub(crate) diagnostics_payload: &'a DiagnosticsPayload, pub(crate) execution: &'a dyn Execution, pub(crate) summary: TraversalSummary, pub(crate) verbose: bool, @@ -18,9 +18,15 @@ pub(crate) struct JunitReporter<'a> { } impl Reporter for JunitReporter<'_> { - fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { - visitor.report_summary(self.execution, self.summary, self.verbose)?; + fn write( + self, + writer: &mut dyn ReporterWriter, + + visitor: &mut dyn ReporterVisitor, + ) -> io::Result<()> { + visitor.report_summary(writer, self.execution, self.summary, self.verbose)?; visitor.report_diagnostics( + writer, self.execution, self.diagnostics_payload, self.verbose, @@ -40,18 +46,19 @@ impl Display for JunitDiagnostic<'_> { } } -pub(crate) struct JunitReporterVisitor<'a>(pub(crate) Report, pub(crate) &'a mut dyn Console); +pub(crate) struct JunitReporterVisitor(pub(crate) Report); -impl<'a> JunitReporterVisitor<'a> { - pub(crate) fn new(console: &'a mut dyn Console) -> Self { +impl JunitReporterVisitor { + pub(crate) fn new() -> Self { let report = Report::new("Biome"); - Self(report, console) + Self(report) } } -impl ReporterVisitor for JunitReporterVisitor<'_> { +impl ReporterVisitor for JunitReporterVisitor { fn report_summary( &mut self, + _writer: &mut dyn ReporterWriter, _execution: &dyn Execution, summary: TraversalSummary, _verbose: bool, @@ -64,8 +71,9 @@ impl ReporterVisitor for JunitReporterVisitor<'_> { fn report_diagnostics( &mut self, + writer: &mut dyn ReporterWriter, _execution: &dyn Execution, - payload: DiagnosticsPayload, + payload: &DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, ) -> io::Result<()> { @@ -125,8 +133,8 @@ impl ReporterVisitor for JunitReporterVisitor<'_> { } } - self.1.log(markup! { - {self.0.to_string().unwrap()} + writer.log(markup! { + {self.0.to_string().expect("To serialize report to string")} }); Ok(()) diff --git a/crates/biome_cli/src/reporter/mod.rs b/crates/biome_cli/src/reporter/mod.rs index 2d6c04cf7297..0e019eeb8437 100644 --- a/crates/biome_cli/src/reporter/mod.rs +++ b/crates/biome_cli/src/reporter/mod.rs @@ -10,15 +10,22 @@ pub(crate) mod terminal; use crate::cli_options::MaxDiagnostics; use crate::runner::execution::Execution; +use biome_console::{Console, ConsoleExt, FileBufferConsole, Markup}; use biome_diagnostics::advice::ListAdvice; use biome_diagnostics::{Diagnostic, Error, Severity}; use biome_fs::BiomePath; +use biome_json_factory::make::{ + json_member, json_member_list, json_member_name, json_number_literal, json_number_value, + json_object_value, json_string_literal, token, +}; +use biome_json_syntax::{AnyJsonMemberName, AnyJsonValue, JsonMember, T}; use camino::Utf8Path; use serde::Serialize; use std::collections::BTreeSet; use std::io; use std::time::Duration; +#[derive(Debug)] pub struct DiagnosticsPayload { pub diagnostics: Vec, pub diagnostic_level: Severity, @@ -46,10 +53,94 @@ pub struct TraversalSummary { pub diagnostics_not_printed: u32, } +impl TraversalSummary { + pub(crate) fn json_member(&self) -> JsonMember { + let members = vec![ + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("changed"))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal(self.changed))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal( + "unchanged", + ))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal( + self.unchanged, + ))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("matches"))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal(self.matches))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("errors"))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal(self.errors))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal( + "warnings", + ))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal( + self.warnings, + ))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("infos"))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal(self.infos))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("skipped"))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal(self.skipped))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal( + "suggestedFixesSkipped", + ))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal( + self.suggested_fixes_skipped, + ))), + ), + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal( + "diagnosticsNotPrinted", + ))), + token(T![:]), + AnyJsonValue::JsonNumberValue(json_number_value(json_number_literal( + self.diagnostics_not_printed, + ))), + ), + ]; + + let separators = vec![token(T![,]); members.len() - 1]; + + json_member( + AnyJsonMemberName::JsonMemberName(json_member_name(json_string_literal("summary"))), + token(T![:]), + AnyJsonValue::JsonObjectValue(json_object_value( + token(T!['{']), + json_member_list(members, separators), + token(T!['}']), + )), + ) + } +} + /// When using this trait, the type that implements this trait is the one that holds the read-only information to pass around pub(crate) trait Reporter: Sized { /// Writes the summary using the underling visitor - fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()>; + fn write( + self, + writer: &mut dyn ReporterWriter, + visitor: &mut dyn ReporterVisitor, + ) -> io::Result<()>; } /// When using this trait, the type that implements this trait is the one that will **write** the data, ideally inside a buffer @@ -57,6 +148,7 @@ pub(crate) trait ReporterVisitor { /// Writes the summary in the underling writer fn report_summary( &mut self, + _writer: &mut dyn ReporterWriter, _execution: &dyn Execution, _summary: TraversalSummary, _verbose: bool, @@ -65,6 +157,7 @@ pub(crate) trait ReporterVisitor { /// Writes the paths handled during a run. fn report_handled_paths( &mut self, + _writer: &mut dyn ReporterWriter, _evaluated_paths: BTreeSet, _working_directory: Option<&Utf8Path>, ) -> io::Result<()> { @@ -74,13 +167,65 @@ pub(crate) trait ReporterVisitor { /// Writes a diagnostics fn report_diagnostics( &mut self, + _writer: &mut dyn ReporterWriter, _execution: &dyn Execution, - _payload: DiagnosticsPayload, + _payload: &DiagnosticsPayload, _verbose: bool, _working_directory: Option<&Utf8Path>, ) -> io::Result<()>; } +pub trait ReporterWriter { + fn log(&mut self, message: Markup); + fn error(&mut self, message: Markup); + fn dump(&mut self) -> Option; + fn clear(&mut self); +} + +pub(crate) struct ConsoleReporterWriter<'a, C>(pub(crate) &'a mut C) +where + C: Console + ?Sized; + +impl<'a, C> ReporterWriter for ConsoleReporterWriter<'a, C> +where + C: Console + ?Sized, +{ + fn log(&mut self, message: Markup) { + self.0.log(message); + } + + fn error(&mut self, message: Markup) { + self.0.error(message); + } + + fn dump(&mut self) -> Option { + None + } + + fn clear(&mut self) {} +} + +#[derive(Debug, Default)] +pub(crate) struct FileReporterWriter(FileBufferConsole); + +impl ReporterWriter for FileReporterWriter { + fn log(&mut self, message: Markup) { + self.0.log(message); + } + + fn error(&mut self, message: Markup) { + self.0.error(message); + } + + fn dump(&mut self) -> Option { + self.0.dump() + } + + fn clear(&mut self) { + self.0.clear(); + } +} + #[derive(Debug, Diagnostic)] #[diagnostic( tags(VERBOSE), diff --git a/crates/biome_cli/src/reporter/rdjson.rs b/crates/biome_cli/src/reporter/rdjson.rs index 4fb1107b01bf..414bd7536eb6 100644 --- a/crates/biome_cli/src/reporter/rdjson.rs +++ b/crates/biome_cli/src/reporter/rdjson.rs @@ -1,23 +1,28 @@ -use crate::reporter::{Reporter, ReporterVisitor}; +use crate::reporter::{Reporter, ReporterVisitor, ReporterWriter}; use crate::runner::execution::Execution; use crate::{DiagnosticsPayload, TraversalSummary}; use biome_console::fmt::{Display, Formatter}; -use biome_console::{Console, ConsoleExt, MarkupBuf, markup}; +use biome_console::{MarkupBuf, markup}; use biome_diagnostics::display::{SourceFile, markup_to_string}; use biome_diagnostics::{Error, Location, LogCategory, PrintDescription, Visit}; use camino::{Utf8Path, Utf8PathBuf}; use serde::Serialize; pub(crate) struct RdJsonReporter<'a> { - pub(crate) diagnostics_payload: DiagnosticsPayload, + pub(crate) diagnostics_payload: &'a DiagnosticsPayload, pub(crate) execution: &'a dyn Execution, pub(crate) verbose: bool, pub(crate) working_directory: Option, } impl Reporter for RdJsonReporter<'_> { - fn write(self, visitor: &mut dyn ReporterVisitor) -> std::io::Result<()> { + fn write( + self, + writer: &mut dyn ReporterWriter, + visitor: &mut dyn ReporterVisitor, + ) -> std::io::Result<()> { visitor.report_diagnostics( + writer, self.execution, self.diagnostics_payload, self.verbose, @@ -27,11 +32,12 @@ impl Reporter for RdJsonReporter<'_> { } } -pub(crate) struct RdJsonReporterVisitor<'a>(pub(crate) &'a mut dyn Console); +pub(crate) struct RdJsonReporterVisitor; -impl ReporterVisitor for RdJsonReporterVisitor<'_> { +impl ReporterVisitor for RdJsonReporterVisitor { fn report_summary( &mut self, + _writer: &mut dyn ReporterWriter, _execution: &dyn Execution, _summary: TraversalSummary, _verbose: bool, @@ -41,8 +47,9 @@ impl ReporterVisitor for RdJsonReporterVisitor<'_> { fn report_diagnostics( &mut self, + writer: &mut dyn ReporterWriter, _execution: &dyn Execution, - payload: DiagnosticsPayload, + payload: &DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, ) -> std::io::Result<()> { @@ -76,7 +83,7 @@ impl ReporterVisitor for RdJsonReporterVisitor<'_> { let result = serde_json::to_string_pretty(&report)?; - self.0.log(markup! { + writer.log(markup! { {result} }); diff --git a/crates/biome_cli/src/reporter/sarif.rs b/crates/biome_cli/src/reporter/sarif.rs index f2837048d7df..2788bec0a9c4 100644 --- a/crates/biome_cli/src/reporter/sarif.rs +++ b/crates/biome_cli/src/reporter/sarif.rs @@ -1,10 +1,8 @@ -use std::collections::{BTreeMap, HashSet}; - -use crate::reporter::{Reporter, ReporterVisitor}; +use crate::reporter::{Reporter, ReporterVisitor, ReporterWriter}; use crate::runner::execution::Execution; use crate::{DiagnosticsPayload, TraversalSummary}; use biome_analyze::{GroupCategory, Queryable, RegistryVisitor, Rule, RuleCategory, RuleGroup}; -use biome_console::{Console, ConsoleExt, markup}; +use biome_console::markup; use biome_css_syntax::CssLanguage; use biome_diagnostics::{Error, Location, PrintDescription, Severity, display::SourceFile}; use biome_graphql_syntax::GraphqlLanguage; @@ -14,17 +12,23 @@ use biome_json_syntax::JsonLanguage; use biome_rowan::Language; use camino::{Utf8Path, Utf8PathBuf}; use serde::Serialize; +use std::collections::{BTreeMap, HashSet}; pub(crate) struct SarifReporter<'a> { - pub(crate) diagnostics_payload: DiagnosticsPayload, + pub(crate) diagnostics_payload: &'a DiagnosticsPayload, pub(crate) execution: &'a dyn Execution, pub(crate) verbose: bool, pub(crate) working_directory: Option, } impl Reporter for SarifReporter<'_> { - fn write(self, visitor: &mut dyn ReporterVisitor) -> std::io::Result<()> { + fn write( + self, + writer: &mut dyn ReporterWriter, + visitor: &mut dyn ReporterVisitor, + ) -> std::io::Result<()> { visitor.report_diagnostics( + writer, self.execution, self.diagnostics_payload, self.verbose, @@ -35,14 +39,12 @@ impl Reporter for SarifReporter<'_> { } pub(crate) struct SarifReporterVisitor<'a> { - console: &'a mut dyn Console, rule_descriptions: BTreeMap<&'static str, &'a str>, } impl<'a> SarifReporterVisitor<'a> { - pub fn new(console: &'a mut dyn Console) -> Self { + pub fn new() -> Self { let mut visitor = Self { - console, rule_descriptions: BTreeMap::new(), }; @@ -124,6 +126,7 @@ impl RegistryVisitor for SarifReporterVisitor<'_> { impl ReporterVisitor for SarifReporterVisitor<'_> { fn report_summary( &mut self, + _writer: &mut dyn ReporterWriter, _execution: &dyn Execution, _summary: TraversalSummary, _verbose: bool, @@ -133,8 +136,9 @@ impl ReporterVisitor for SarifReporterVisitor<'_> { fn report_diagnostics( &mut self, + writer: &mut dyn ReporterWriter, _execution: &dyn Execution, - payload: DiagnosticsPayload, + payload: &DiagnosticsPayload, verbose: bool, working_directory: Option<&Utf8Path>, ) -> std::io::Result<()> { @@ -192,7 +196,7 @@ impl ReporterVisitor for SarifReporterVisitor<'_> { let result = serde_json::to_string_pretty(&report)?; - self.console.log(markup! { + writer.log(markup! { {result} }); diff --git a/crates/biome_cli/src/reporter/summary.rs b/crates/biome_cli/src/reporter/summary.rs index 92b5810e9ac3..24203526741d 100644 --- a/crates/biome_cli/src/reporter/summary.rs +++ b/crates/biome_cli/src/reporter/summary.rs @@ -1,11 +1,13 @@ use crate::reporter::terminal::ConsoleTraversalSummary; -use crate::reporter::{EvaluatedPathsDiagnostic, FixedPathsDiagnostic, Reporter, ReporterVisitor}; +use crate::reporter::{ + EvaluatedPathsDiagnostic, FixedPathsDiagnostic, Reporter, ReporterVisitor, ReporterWriter, +}; use crate::runner::execution::Execution; use crate::{DiagnosticsPayload, TraversalSummary}; use biome_analyze::profiling::DisplayProfiles; use biome_console::fmt::{Display, Formatter}; -use biome_console::{Console, ConsoleExt, MarkupBuf, markup}; +use biome_console::{MarkupBuf, markup}; use biome_diagnostics::advice::ListAdvice; use biome_diagnostics::{ Advices, Category, Diagnostic, LogCategory, PrintDiagnostic, Resource, Severity, Visit, @@ -20,7 +22,7 @@ use std::io; pub(crate) struct SummaryReporter<'a> { pub(crate) summary: TraversalSummary, - pub(crate) diagnostics_payload: DiagnosticsPayload, + pub(crate) diagnostics_payload: &'a DiagnosticsPayload, pub(crate) execution: &'a dyn Execution, pub(crate) evaluated_paths: BTreeSet, pub(crate) working_directory: Option, @@ -28,52 +30,61 @@ pub(crate) struct SummaryReporter<'a> { } impl Reporter for SummaryReporter<'_> { - fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { + fn write( + self, + writer: &mut dyn ReporterWriter, + visitor: &mut dyn ReporterVisitor, + ) -> io::Result<()> { visitor.report_diagnostics( + writer, self.execution, self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), )?; if self.verbose { - visitor - .report_handled_paths(self.evaluated_paths, self.working_directory.as_deref())?; + visitor.report_handled_paths( + writer, + self.evaluated_paths, + self.working_directory.as_deref(), + )?; } - visitor.report_summary(self.execution, self.summary, self.verbose)?; + visitor.report_summary(writer, self.execution, self.summary, self.verbose)?; Ok(()) } } -pub(crate) struct SummaryReporterVisitor<'a>(pub(crate) &'a mut dyn Console); +pub(crate) struct SummaryReporterVisitor; -impl ReporterVisitor for SummaryReporterVisitor<'_> { +impl ReporterVisitor for SummaryReporterVisitor { fn report_summary( &mut self, + writer: &mut dyn ReporterWriter, execution: &dyn Execution, summary: TraversalSummary, verbose: bool, ) -> io::Result<()> { if execution.is_check() && summary.suggested_fixes_skipped > 0 { - self.0.log(markup! { + writer.log(markup! { "Skipped "{summary.suggested_fixes_skipped}" suggested fixes.\n" "If you wish to apply the suggested (unsafe) fixes, use the command ""biome check --write --unsafe\n" }) } if !execution.is_ci() && summary.diagnostics_not_printed > 0 { - self.0.log(markup! { + writer.log(markup! { "The number of diagnostics exceeds the limit allowed. Use ""--max-diagnostics"" to increase it.\n" "Diagnostics not shown: "{summary.diagnostics_not_printed}"." }) } - self.0.log(markup! { + writer.log(markup! { {ConsoleTraversalSummary(execution, &summary, verbose)} }); let profiles = biome_analyze::profiling::drain_sorted_by_total(false); if !profiles.is_empty() { - self.0.log(markup! {{ DisplayProfiles(profiles, None) }}); + writer.log(markup! {{ DisplayProfiles(profiles, None) }}); } Ok(()) @@ -81,6 +92,7 @@ impl ReporterVisitor for SummaryReporterVisitor<'_> { fn report_handled_paths( &mut self, + writer: &mut dyn ReporterWriter, evaluated_paths: BTreeSet, working_directory: Option<&Utf8Path>, ) -> io::Result<()> { @@ -121,10 +133,10 @@ impl ReporterVisitor for SummaryReporterVisitor<'_> { }, }; - self.0.log(markup! { + writer.log(markup! { {PrintDiagnostic::verbose(&evaluated_paths_diagnostic)} }); - self.0.log(markup! { + writer.log(markup! { {PrintDiagnostic::verbose(&fixed_paths_diagnostic)} }); @@ -133,8 +145,9 @@ impl ReporterVisitor for SummaryReporterVisitor<'_> { fn report_diagnostics( &mut self, + writer: &mut dyn ReporterWriter, execution: &dyn Execution, - diagnostics_payload: DiagnosticsPayload, + diagnostics_payload: &DiagnosticsPayload, verbose: bool, working_directory: Option<&Utf8Path>, ) -> io::Result<()> { @@ -192,7 +205,7 @@ impl ReporterVisitor for SummaryReporterVisitor<'_> { } } - self.0.log(markup! {{files_to_diagnostics}}); + writer.log(markup! {{files_to_diagnostics}}); Ok(()) } diff --git a/crates/biome_cli/src/reporter/terminal.rs b/crates/biome_cli/src/reporter/terminal.rs index e1eba41da1c4..afd43aac6142 100644 --- a/crates/biome_cli/src/reporter/terminal.rs +++ b/crates/biome_cli/src/reporter/terminal.rs @@ -1,12 +1,12 @@ use crate::reporter::{ DiagnosticsPayload, EvaluatedPathsDiagnostic, FixedPathsDiagnostic, Reporter, ReporterVisitor, - TraversalSummary, + ReporterWriter, TraversalSummary, }; use crate::runner::execution::Execution; use biome_analyze::profiling; use biome_analyze::profiling::DisplayProfiles; use biome_console::fmt::Formatter; -use biome_console::{Console, ConsoleExt, fmt, markup}; +use biome_console::{fmt, markup}; use biome_diagnostics::PrintDiagnostic; use biome_diagnostics::advice::ListAdvice; use biome_fs::BiomePath; @@ -17,7 +17,7 @@ use std::time::Duration; pub(crate) struct ConsoleReporter<'a> { pub(crate) summary: TraversalSummary, - pub(crate) diagnostics_payload: DiagnosticsPayload, + pub(crate) diagnostics_payload: &'a DiagnosticsPayload, pub(crate) execution: &'a dyn Execution, pub(crate) evaluated_paths: BTreeSet, pub(crate) working_directory: Option, @@ -25,51 +25,60 @@ pub(crate) struct ConsoleReporter<'a> { } impl<'a> Reporter for ConsoleReporter<'a> { - fn write(self, visitor: &mut dyn ReporterVisitor) -> io::Result<()> { + fn write( + self, + writer: &mut dyn ReporterWriter, + visitor: &mut dyn ReporterVisitor, + ) -> io::Result<()> { visitor.report_diagnostics( + writer, self.execution, self.diagnostics_payload, self.verbose, self.working_directory.as_deref(), )?; if self.verbose { - visitor - .report_handled_paths(self.evaluated_paths, self.working_directory.as_deref())?; + visitor.report_handled_paths( + writer, + self.evaluated_paths, + self.working_directory.as_deref(), + )?; } - visitor.report_summary(self.execution, self.summary, self.verbose)?; + visitor.report_summary(writer, self.execution, self.summary, self.verbose)?; Ok(()) } } -pub(crate) struct ConsoleReporterVisitor<'a>(pub(crate) &'a mut dyn Console); +pub(crate) struct ConsoleReporterVisitor; -impl ReporterVisitor for ConsoleReporterVisitor<'_> { +impl ReporterVisitor for ConsoleReporterVisitor { fn report_summary( &mut self, + writer: &mut dyn ReporterWriter, execution: &dyn Execution, summary: TraversalSummary, verbose: bool, ) -> io::Result<()> { if execution.is_check() && summary.suggested_fixes_skipped > 0 { - self.0.log(markup! { + writer.log(markup! { "Skipped "{summary.suggested_fixes_skipped}" suggested fixes.\n" "If you wish to apply the suggested (unsafe) fixes, use the command ""biome check --write --unsafe\n" }) } if !execution.is_ci() && summary.diagnostics_not_printed > 0 { - self.0.log(markup! { + writer.log(markup! { "The number of diagnostics exceeds the limit allowed. Use ""--max-diagnostics"" to increase it.\n" "Diagnostics not shown: "{summary.diagnostics_not_printed}"." }) } - self.0.log(markup! { + writer.log(markup! { {ConsoleTraversalSummary(execution, &summary, verbose)} }); let profiles = profiling::drain_sorted_by_total(false); if !profiles.is_empty() { - self.0.log(markup! {{ DisplayProfiles(profiles, None) }}); + writer.log(markup! {{ DisplayProfiles(profiles, None) }}); } Ok(()) @@ -77,6 +86,7 @@ impl ReporterVisitor for ConsoleReporterVisitor<'_> { fn report_handled_paths( &mut self, + writer: &mut dyn ReporterWriter, evaluated_paths: BTreeSet, working_directory: Option<&Utf8Path>, ) -> io::Result<()> { @@ -117,10 +127,10 @@ impl ReporterVisitor for ConsoleReporterVisitor<'_> { }, }; - self.0.log(markup! { + writer.log(markup! { {PrintDiagnostic::verbose(&evaluated_paths_diagnostic)} }); - self.0.log(markup! { + writer.log(markup! { {PrintDiagnostic::verbose(&fixed_paths_diagnostic)} }); @@ -129,24 +139,23 @@ impl ReporterVisitor for ConsoleReporterVisitor<'_> { fn report_diagnostics( &mut self, + writer: &mut dyn ReporterWriter, execution: &dyn Execution, - diagnostics_payload: DiagnosticsPayload, + diagnostics_payload: &DiagnosticsPayload, verbose: bool, _working_directory: Option<&Utf8Path>, ) -> io::Result<()> { for diagnostic in &diagnostics_payload.diagnostics { if execution.is_search() { - self.0.log(markup! {{PrintDiagnostic::search(diagnostic)}}); + writer.log(markup! {{PrintDiagnostic::search(diagnostic)}}); continue; } if diagnostic.severity() >= diagnostics_payload.diagnostic_level { if diagnostic.tags().is_verbose() && verbose { - self.0 - .error(markup! {{PrintDiagnostic::verbose(diagnostic)}}); + writer.error(markup! {{PrintDiagnostic::verbose(diagnostic)}}); } else { - self.0 - .error(markup! {{PrintDiagnostic::simple(diagnostic)}}); + writer.error(markup! {{PrintDiagnostic::simple(diagnostic)}}); } } } diff --git a/crates/biome_cli/src/runner/execution.rs b/crates/biome_cli/src/runner/execution.rs index 332095bbf71e..933cc1623d21 100644 --- a/crates/biome_cli/src/runner/execution.rs +++ b/crates/biome_cli/src/runner/execution.rs @@ -29,14 +29,17 @@ pub(crate) trait Execution: Send + Sync + std::panic::RefUnwindSafe { } fn get_max_diagnostics(&self, cli_options: &CliOptions) -> u32 { - if cli_options.reporter.is_default() { - cli_options.max_diagnostics.into() - } else { + if cli_options + .cli_reporter + .iter() + .any(|reporter| !reporter.is_default()) + { info!( - "Removing the limit of --max-diagnostics, because of a reporter different from the default one: {}", - cli_options.reporter + "Removing the limit of --max-diagnostics, because of a reporter list contains a reporter different from the default one." ); u32::MAX + } else { + cli_options.max_diagnostics.into() } } diff --git a/crates/biome_cli/src/runner/finalizer.rs b/crates/biome_cli/src/runner/finalizer.rs index fe074a88ff67..f6952969b2f7 100644 --- a/crates/biome_cli/src/runner/finalizer.rs +++ b/crates/biome_cli/src/runner/finalizer.rs @@ -28,7 +28,6 @@ pub trait Finalizer { } pub(crate) struct FinalizePayload<'a, I> { - pub(crate) project_key: ProjectKey, pub(crate) fs: &'a dyn FileSystem, pub(crate) workspace: &'a dyn Workspace, pub(crate) scan_duration: Option, diff --git a/crates/biome_cli/src/runner/impls/finalizers/default.rs b/crates/biome_cli/src/runner/impls/finalizers/default.rs index ef540aa4ae15..3dd0df01e162 100644 --- a/crates/biome_cli/src/runner/impls/finalizers/default.rs +++ b/crates/biome_cli/src/runner/impls/finalizers/default.rs @@ -1,6 +1,4 @@ -use crate::cli_options::CliReporter; -use crate::diagnostics::ReportDiagnostic; -use crate::reporter::Reporter; +use crate::cli_options::CliReporterKind; use crate::reporter::checkstyle::CheckstyleReporter; use crate::reporter::github::{GithubReporter, GithubReporterVisitor}; use crate::reporter::gitlab::{GitLabReporter, GitLabReporterVisitor}; @@ -10,13 +8,15 @@ use crate::reporter::rdjson::{RdJsonReporter, RdJsonReporterVisitor}; use crate::reporter::sarif::{SarifReporter, SarifReporterVisitor}; use crate::reporter::summary::{SummaryReporter, SummaryReporterVisitor}; use crate::reporter::terminal::{ConsoleReporter, ConsoleReporterVisitor}; +use crate::reporter::{ConsoleReporterWriter, FileReporterWriter, Reporter, ReporterWriter}; use crate::runner::finalizer::{FinalizePayload, Finalizer}; use crate::runner::impls::commands::traversal::TraverseResult; -use crate::{CliDiagnostic, DiagnosticsPayload, TEMPORARY_INTERNAL_REPORTER_FILE}; -use biome_console::{ConsoleExt, markup}; -use biome_diagnostics::{Resource, SerdeJsonError}; -use biome_fs::BiomePath; -use biome_service::workspace::{CloseFileParams, FileContent, FormatFileParams, OpenFileParams}; +use crate::{CliDiagnostic, DiagnosticsPayload}; +use biome_console::markup; +use biome_diagnostics::{PrintDiagnostic, Resource}; +use biome_fs::OpenOptions; +use biome_json_formatter::context::JsonFormatOptions; +use biome_rowan::AstNode; use std::cmp::Ordering; pub(crate) struct DefaultFinalizer; @@ -26,7 +26,6 @@ impl Finalizer for DefaultFinalizer { fn finalize(payload: FinalizePayload<'_, Self::Input>) -> Result<(), CliDiagnostic> { let FinalizePayload { - project_key, fs, workspace, scan_duration, @@ -69,139 +68,227 @@ impl Finalizer for DefaultFinalizer { max_diagnostics: cli_options.max_diagnostics, }; - let reporter_mode = ReportMode::from(&cli_options.reporter); + let mut console_reporter_writer = ConsoleReporterWriter(console); + let mut file_reporter_writer = FileReporterWriter::default(); - match reporter_mode { - ReportMode::Terminal { with_summary } => { - if with_summary { - let reporter = SummaryReporter { - summary, - diagnostics_payload, - execution, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - evaluated_paths, - }; - reporter.write(&mut SummaryReporterVisitor(console))?; - } else { - let reporter = ConsoleReporter { - summary, - diagnostics_payload, - execution, - evaluated_paths, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut ConsoleReporterVisitor(console))?; + if !cli_options.cli_reporter.is_empty() { + for cli_reporter in &cli_options.cli_reporter { + match cli_reporter.kind { + CliReporterKind::Default => { + let reporter = ConsoleReporter { + summary, + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + evaluated_paths: evaluated_paths.clone(), + }; + if cli_reporter.is_file_report() { + reporter + .write(&mut file_reporter_writer, &mut ConsoleReporterVisitor)?; + } else { + reporter + .write(&mut console_reporter_writer, &mut ConsoleReporterVisitor)?; + } + } + CliReporterKind::Summary => { + let reporter = SummaryReporter { + summary, + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + evaluated_paths: evaluated_paths.clone(), + }; + if cli_reporter.is_file_report() { + reporter + .write(&mut file_reporter_writer, &mut SummaryReporterVisitor)?; + } else { + reporter + .write(&mut console_reporter_writer, &mut SummaryReporterVisitor)?; + } + } + CliReporterKind::Json | CliReporterKind::JsonPretty => { + console_reporter_writer.error(markup! { + "The ""--json"" option is ""unstable/experimental"" and its output might change between patches/minor releases." + }); + let reporter = JsonReporter { + summary, + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + let mut buffer = JsonReporterVisitor::new(summary); + reporter.write(&mut console_reporter_writer, &mut buffer)?; + let root = buffer.to_json(); + if cli_reporter.kind == CliReporterKind::JsonPretty { + let formatted = biome_json_formatter::format_node( + JsonFormatOptions::default(), + root.syntax(), + ) + .expect("To format the JSON report") + .print() + .expect("To print the JSON report"); + + console_reporter_writer.log(markup! { + {formatted.as_code()} + }); + } else { + let code = root.to_string(); + console_reporter_writer.log(markup! { + {code} + }); + } + } + CliReporterKind::GitHub => { + let reporter = GithubReporter { + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + if cli_reporter.is_file_report() { + reporter + .write(&mut file_reporter_writer, &mut GithubReporterVisitor)?; + } else { + reporter + .write(&mut console_reporter_writer, &mut GithubReporterVisitor)?; + } + } + CliReporterKind::GitLab => { + let reporter = GitLabReporter { + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + if cli_reporter.is_file_report() { + reporter.write( + &mut file_reporter_writer, + &mut GitLabReporterVisitor::new(workspace.fs().working_directory()), + )?; + } else { + reporter.write( + &mut console_reporter_writer, + &mut GitLabReporterVisitor::new(workspace.fs().working_directory()), + )?; + } + } + CliReporterKind::Junit => { + let reporter = JunitReporter { + summary, + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + + if cli_reporter.is_file_report() { + reporter.write( + &mut file_reporter_writer, + &mut JunitReporterVisitor::new(), + )?; + } else { + reporter.write( + &mut console_reporter_writer, + &mut JunitReporterVisitor::new(), + )?; + } + } + CliReporterKind::Checkstyle => { + let reporter = CheckstyleReporter { + summary, + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + if cli_reporter.is_file_report() { + reporter.write( + &mut file_reporter_writer, + &mut crate::reporter::checkstyle::CheckstyleReporterVisitor, + )?; + } else { + reporter.write( + &mut console_reporter_writer, + &mut crate::reporter::checkstyle::CheckstyleReporterVisitor, + )?; + } + } + CliReporterKind::RdJson => { + let reporter = RdJsonReporter { + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + if cli_reporter.is_file_report() { + reporter + .write(&mut file_reporter_writer, &mut RdJsonReporterVisitor)?; + } else { + reporter + .write(&mut console_reporter_writer, &mut RdJsonReporterVisitor)?; + } + } + CliReporterKind::Sarif => { + let reporter = SarifReporter { + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + }; + + if cli_reporter.is_file_report() { + reporter.write( + &mut file_reporter_writer, + &mut SarifReporterVisitor::new(), + )?; + } else { + reporter.write( + &mut console_reporter_writer, + &mut SarifReporterVisitor::new(), + )?; + } + } } - } - ReportMode::Json { pretty } => { - console.error(markup! { - "The ""--json"" option is ""unstable/experimental"" and its output might change between patches/minor releases." - }); - let reporter = JsonReporter { - summary, - diagnostics: diagnostics_payload, - execution, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - let mut buffer = JsonReporterVisitor::new(summary); - reporter.write(&mut buffer)?; - if pretty { - let content = serde_json::to_string(&buffer).map_err(|error| { - CliDiagnostic::Report(ReportDiagnostic::Serialization( - SerdeJsonError::from(error), - )) - })?; - let report_file = BiomePath::new(TEMPORARY_INTERNAL_REPORTER_FILE); - workspace.open_file(OpenFileParams { - project_key, - content: FileContent::from_client(content), - path: report_file.clone(), - document_file_source: None, - persist_node_cache: false, - inline_config: None, - })?; - let code = workspace.format_file(FormatFileParams { - project_key, - path: report_file.clone(), - inline_config: None, - })?; - console.log(markup! { - {code.as_code()} - }); - workspace.close_file(CloseFileParams { - project_key, - path: report_file, - })?; - } else { - console.log(markup! { - {buffer} - }); + + if let Some(destination) = cli_reporter.destination.as_deref() + && let Some(output) = file_reporter_writer.dump() + { + let open_options = OpenOptions::default().write(true).create(true); + let mut file = match fs.open_with_options(destination, open_options) { + Ok(file) => file, + Err(err) => { + let diagnostics = CliDiagnostic::from(err); + console_reporter_writer.error(markup! { + {PrintDiagnostic::simple(&diagnostics)} + }); + continue; + } + }; + + let result = file.set_content(output.as_bytes()); + if let Err(err) = result { + let diagnostics = CliDiagnostic::from(err); + console_reporter_writer.error(markup! { + {PrintDiagnostic::simple(&diagnostics)} + }) + } + + file_reporter_writer.clear(); } } - ReportMode::GitHub => { - let reporter = GithubReporter { - diagnostics_payload, - execution, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut GithubReporterVisitor(console))?; - } - ReportMode::GitLab => { - let reporter = GitLabReporter { - diagnostics: diagnostics_payload, - execution, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut GitLabReporterVisitor::new( - console, - workspace.fs().working_directory(), - ))?; - } - ReportMode::Junit => { - let reporter = JunitReporter { - summary, - diagnostics_payload, - execution, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut JunitReporterVisitor::new(console))?; - } - ReportMode::Checkstyle => { - let reporter = CheckstyleReporter { - summary, - diagnostics_payload, - execution, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write( - &mut crate::reporter::checkstyle::CheckstyleReporterVisitor::new(console), - )?; - } - ReportMode::RdJson => { - let reporter = RdJsonReporter { - diagnostics_payload, - execution, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut RdJsonReporterVisitor(console))?; - } - ReportMode::Sarif => { - let reporter = SarifReporter { - diagnostics_payload, - execution, - verbose: cli_options.verbose, - working_directory: fs.working_directory().clone(), - }; - reporter.write(&mut SarifReporterVisitor::new(console))?; - } + } else { + let reporter = ConsoleReporter { + summary, + diagnostics_payload: &diagnostics_payload, + execution, + verbose: cli_options.verbose, + working_directory: fs.working_directory().clone(), + evaluated_paths: evaluated_paths.clone(), + }; + reporter.write(&mut console_reporter_writer, &mut ConsoleReporterVisitor)?; } // Processing emitted error diagnostics, exit with a non-zero code @@ -237,51 +324,52 @@ impl Finalizer for () { } } -/// Tells to the execution of the traversal how the information should be reported -#[derive(Copy, Clone, Debug)] -pub enum ReportMode { - /// Reports information straight to the console, it's the default mode - Terminal { with_summary: bool }, - /// Reports information in JSON format - Json { pretty: bool }, - /// Reports information for GitHub - GitHub, - /// JUnit output - /// Ref: https://github.com/testmoapp/junitxml?tab=readme-ov-file#basic-junit-xml-structure - Junit, - /// Reports information in the [GitLab Code Quality](https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool) format. - GitLab, - /// Reports diagnostics in [Checkstyle XML format](https://checkstyle.org/). - Checkstyle, - /// Reports information in [reviewdog JSON format](https://deepwiki.com/reviewdog/reviewdog/3.2-reviewdog-diagnostic-format) - RdJson, - /// Reports diagnostics using the SARIF format - Sarif, -} +// +// /// Tells to the execution of the traversal how the information should be reported +// #[derive(Copy, Clone, Debug)] +// pub enum ReportMode { +// /// Reports information straight to the console, it's the default mode +// Terminal { with_summary: bool }, +// /// Reports information in JSON format +// Json { pretty: bool }, +// /// Reports information for GitHub +// GitHub, +// /// JUnit output +// /// Ref: https://github.com/testmoapp/junitxml?tab=readme-ov-file#basic-junit-xml-structure +// Junit, +// /// Reports information in the [GitLab Code Quality](https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool) format. +// GitLab, +// /// Reports diagnostics in [Checkstyle XML format](https://checkstyle.org/). +// Checkstyle, +// /// Reports information in [reviewdog JSON format](https://deepwiki.com/reviewdog/reviewdog/3.2-reviewdog-diagnostic-format) +// RdJson, +// /// Reports diagnostics using the SARIF format +// Sarif, +// } -impl Default for ReportMode { - fn default() -> Self { - Self::Terminal { - with_summary: false, - } - } -} - -impl From<&CliReporter> for ReportMode { - fn from(value: &CliReporter) -> Self { - match value { - CliReporter::Default => Self::Terminal { - with_summary: false, - }, - CliReporter::Summary => Self::Terminal { with_summary: true }, - CliReporter::Json => Self::Json { pretty: false }, - CliReporter::JsonPretty => Self::Json { pretty: true }, - CliReporter::GitHub => Self::GitHub, - CliReporter::Junit => Self::Junit, - CliReporter::GitLab => Self::GitLab {}, - CliReporter::Checkstyle => Self::Checkstyle, - CliReporter::RdJson => Self::RdJson, - CliReporter::Sarif => Self::Sarif, - } - } -} +// impl Default for ReportMode { +// fn default() -> Self { +// Self::Terminal { +// with_summary: false, +// } +// } +// } +// +// impl From<&CliReporterKind> for ReportMode { +// fn from(value: &CliReporterKind) -> Self { +// match value { +// CliReporterKind::Default => Self::Terminal { +// with_summary: false, +// }, +// CliReporterKind::Summary => Self::Terminal { with_summary: true }, +// CliReporterKind::Json => Self::Json { pretty: false }, +// CliReporterKind::JsonPretty => Self::Json { pretty: true }, +// CliReporterKind::GitHub => Self::GitHub, +// CliReporterKind::Junit => Self::Junit, +// CliReporterKind::GitLab => Self::GitLab {}, +// CliReporterKind::Checkstyle => Self::Checkstyle, +// CliReporterKind::RdJson => Self::RdJson, +// CliReporterKind::Sarif => Self::Sarif, +// } +// } +// } diff --git a/crates/biome_cli/src/runner/mod.rs b/crates/biome_cli/src/runner/mod.rs index 315ba40d1699..afa632454ad8 100644 --- a/crates/biome_cli/src/runner/mod.rs +++ b/crates/biome_cli/src/runner/mod.rs @@ -315,7 +315,6 @@ pub(crate) trait CommandRunner { Self::Finalizer::finalize(FinalizePayload { cli_options, - project_key, execution: execution.as_ref(), fs, console, diff --git a/crates/biome_cli/tests/cases/mod.rs b/crates/biome_cli/tests/cases/mod.rs index 51a21d3e815d..e0f45961440f 100644 --- a/crates/biome_cli/tests/cases/mod.rs +++ b/crates/biome_cli/tests/cases/mod.rs @@ -25,6 +25,7 @@ mod linter_domains; mod linter_groups_plain; mod migrate_v2; mod monorepo; +mod multiple_reporters; mod overrides_formatter; mod overrides_linter; mod overrides_max_file_size; diff --git a/crates/biome_cli/tests/cases/multiple_reporters.rs b/crates/biome_cli/tests/cases/multiple_reporters.rs new file mode 100644 index 000000000000..4a477e48809f --- /dev/null +++ b/crates/biome_cli/tests/cases/multiple_reporters.rs @@ -0,0 +1,168 @@ +use crate::run_cli; +use crate::snap_test::{SnapshotPayload, assert_cli_snapshot}; +use biome_console::BufferConsole; +use biome_fs::MemoryFileSystem; +use bpaf::Args; +use camino::Utf8Path; + +const MAIN_1: &str = r#"debugger"#; + +#[test] +fn one_report_to_console_one_to_file() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "check", + "--reporter=rdjson", + "--reporter-file=file.json", + "--reporter=default", + file_path1.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "one_report_to_console_one_to_file", + fs, + console, + result, + )); +} + +#[test] +fn two_report_files() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "check", + "--reporter=rdjson", + "--reporter-file=file.json", + "--reporter=summary", + "--reporter-file=file.txt", + file_path1.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "two_report_files", + fs, + console, + result, + )); +} + +#[test] +fn two_report_to_console() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "check", + "--reporter=rdjson", + "--reporter=summary", + file_path1.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "two_report_to_console", + fs, + console, + result, + )); +} + +#[test] +fn first_file_then_reporter_name() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "check", + "--reporter-file=file.json", + "--reporter=rdjson", + file_path1.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "first_file_then_reporter_name", + fs, + console, + result, + )); +} + +#[test] +fn only_report_file() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from(["check", "--reporter-file=file.txt", file_path1.as_str()].as_slice()), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "only_report_file", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/cases/reporter_checkstyle.rs b/crates/biome_cli/tests/cases/reporter_checkstyle.rs index 5d9d59114e8f..71cba999e1ca 100644 --- a/crates/biome_cli/tests/cases/reporter_checkstyle.rs +++ b/crates/biome_cli/tests/cases/reporter_checkstyle.rs @@ -168,3 +168,40 @@ fn reports_diagnostics_checkstyle_format_command() { result, )); } + +#[test] +fn reports_diagnostics_checkstyle_check_command_file() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let file_path2 = Utf8Path::new("index.ts"); + fs.insert(file_path2.into(), MAIN_2.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "check", + "--reporter=checkstyle", + "--reporter-file=file.json", + file_path1.as_str(), + file_path2.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "reports_diagnostics_checkstyle_check_command_file", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/cases/reporter_junit.rs b/crates/biome_cli/tests/cases/reporter_junit.rs index f2cc14b2aa60..efb6450b87fe 100644 --- a/crates/biome_cli/tests/cases/reporter_junit.rs +++ b/crates/biome_cli/tests/cases/reporter_junit.rs @@ -168,3 +168,40 @@ fn reports_diagnostics_junit_format_command() { result, )); } + +#[test] +fn reports_diagnostics_junit_check_command_file() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let file_path2 = Utf8Path::new("index.ts"); + fs.insert(file_path2.into(), MAIN_2.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "check", + "--reporter=junit", + "--reporter-file=file.xml", + file_path1.as_str(), + file_path2.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "reports_diagnostics_junit_check_command_file", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/cases/reporter_rdjson.rs b/crates/biome_cli/tests/cases/reporter_rdjson.rs index 455f9db20fac..1f9cf3cc9033 100644 --- a/crates/biome_cli/tests/cases/reporter_rdjson.rs +++ b/crates/biome_cli/tests/cases/reporter_rdjson.rs @@ -168,3 +168,40 @@ fn reports_diagnostics_rdjson_format_command() { result, )); } + +#[test] +fn reports_diagnostics_rdjson_check_command_file() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let file_path2 = Utf8Path::new("index.ts"); + fs.insert(file_path2.into(), MAIN_2.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "check", + "--reporter=rdjson", + "--reporter-file=file.json", + file_path1.as_str(), + file_path2.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "reports_diagnostics_rdjson_check_command_file", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/cases/reporter_sarif.rs b/crates/biome_cli/tests/cases/reporter_sarif.rs index 2a26ebf1fba3..caf5f6f85714 100644 --- a/crates/biome_cli/tests/cases/reporter_sarif.rs +++ b/crates/biome_cli/tests/cases/reporter_sarif.rs @@ -168,3 +168,40 @@ fn reports_diagnostics_sarif_format_command() { result, )); } + +#[test] +fn reports_diagnostics_sarif_check_command_file() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let file_path2 = Utf8Path::new("index.ts"); + fs.insert(file_path2.into(), MAIN_2.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "format", + "--reporter=sarif", + "--reporter-file=file.json", + file_path1.as_str(), + file_path2.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "reports_diagnostics_sarif_check_command_file", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/cases/reporter_summary.rs b/crates/biome_cli/tests/cases/reporter_summary.rs index ae5896085f60..a24e2bfe5f6a 100644 --- a/crates/biome_cli/tests/cases/reporter_summary.rs +++ b/crates/biome_cli/tests/cases/reporter_summary.rs @@ -286,3 +286,45 @@ fn reports_diagnostics_summary_check_verbose_command() { result, )); } + +#[test] +fn reports_diagnostics_summary_check_verbose_command_destination() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let file_path2 = Utf8Path::new("index.ts"); + fs.insert(file_path2.into(), MAIN_2.as_bytes()); + + let file_path3 = Utf8Path::new("index.css"); + fs.insert(file_path3.into(), MAIN_3.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "check", + "--reporter=summary", + "--reporter-file=file.txt", + "--verbose", + file_path1.as_str(), + file_path2.as_str(), + file_path3.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "reports_diagnostics_summary_check_verbose_command_destination", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/cases/reporter_terminal.rs b/crates/biome_cli/tests/cases/reporter_terminal.rs index 0f1be8db22ea..4115f0ce89c6 100644 --- a/crates/biome_cli/tests/cases/reporter_terminal.rs +++ b/crates/biome_cli/tests/cases/reporter_terminal.rs @@ -137,3 +137,45 @@ fn reports_diagnostics_check_write_command_verbose() { result, )); } + +#[test] +fn reports_diagnostics_check_write_command_file() { + let fs = MemoryFileSystem::default(); + let mut console = BufferConsole::default(); + + let file_path1 = Utf8Path::new("main.ts"); + fs.insert(file_path1.into(), MAIN_1.as_bytes()); + + let file_path2 = Utf8Path::new("index.ts"); + fs.insert(file_path2.into(), MAIN_2.as_bytes()); + + let file_path3 = Utf8Path::new("index.css"); + fs.insert(file_path3.into(), MAIN_3.as_bytes()); + + let (fs, result) = run_cli( + fs, + &mut console, + Args::from( + [ + "check", + "--max-diagnostics=5", + "--reporter=default", + "--reporter-file=file.txt", + file_path1.as_str(), + file_path2.as_str(), + file_path3.as_str(), + ] + .as_slice(), + ), + ); + + assert!(result.is_err(), "run_cli returned {result:?}"); + + assert_cli_snapshot(SnapshotPayload::new( + module_path!(), + "reports_diagnostics_check_write_command_file", + fs, + console, + result, + )); +} diff --git a/crates/biome_cli/tests/snapshots/main_cases_multiple_reporters/first_file_then_reporter_name.snap b/crates/biome_cli/tests/snapshots/main_cases_multiple_reporters/first_file_then_reporter_name.snap new file mode 100644 index 000000000000..b021da85fdf5 --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_cases_multiple_reporters/first_file_then_reporter_name.snap @@ -0,0 +1,59 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: redactor(content) +--- +## `file.json` + +```json +{ + "source": { + "name": "Biome", + "url": "https://biomejs.dev" + }, + "diagnostics": [ + { + "code": { + "url": "https://biomejs.dev/linter/rules/no-debugger", + "value": "lint/suspicious/noDebugger" + }, + "location": { + "path": "main.ts", + "range": { + "end": { + "column": 9, + "line": 1 + }, + "start": { + "column": 1, + "line": 1 + } + } + }, + "message": "This is an unexpected use of the debugger statement." + }, + { + "code": { + "value": "format" + }, + "message": "Formatter would have printed the following content:" + } + ] +} +``` + +## `main.ts` + +```ts +debugger +``` + +# Termination Message + +```block +check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. + + + +``` diff --git a/crates/biome_cli/tests/snapshots/main_cases_multiple_reporters/one_report_to_console_one_to_file.snap b/crates/biome_cli/tests/snapshots/main_cases_multiple_reporters/one_report_to_console_one_to_file.snap new file mode 100644 index 000000000000..66f32f869f31 --- /dev/null +++ b/crates/biome_cli/tests/snapshots/main_cases_multiple_reporters/one_report_to_console_one_to_file.snap @@ -0,0 +1,93 @@ +--- +source: crates/biome_cli/tests/snap_test.rs +expression: redactor(content) +--- +## `file.json` + +```json +{ + "source": { + "name": "Biome", + "url": "https://biomejs.dev" + }, + "diagnostics": [ + { + "code": { + "url": "https://biomejs.dev/linter/rules/no-debugger", + "value": "lint/suspicious/noDebugger" + }, + "location": { + "path": "main.ts", + "range": { + "end": { + "column": 9, + "line": 1 + }, + "start": { + "column": 1, + "line": 1 + } + } + }, + "message": "This is an unexpected use of the debugger statement." + }, + { + "code": { + "value": "format" + }, + "message": "Formatter would have printed the following content:" + } + ] +} +``` + +## `main.ts` + +```ts +debugger +``` + +# Termination Message + +```block +check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. + + + +``` + +# Emitted Messages + +```block +main.ts:1:1 lint/suspicious/noDebugger FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × This is an unexpected use of the debugger statement. + + > 1 │ debugger + │ ^^^^^^^^ + + i Unsafe fix: Remove debugger statement + + 1 │ debugger + │ -------- + +``` + +```block +main.ts format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Formatter would have printed the following content: + + 1 │ - debugger + 1 │ + debugger; + 2 │ + + + +``` + +```block +Checked 1 file in