Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/all-pumas-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@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 erros in console, you can now use both reporters:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: "erros" → "errors"

-If you run Biome on GitHub, take advantage of the reporter and still see the erros in console, you can now use both reporters:
+If you run Biome on GitHub, take advantage of the reporter and still see the errors in console, you can now use both reporters:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
If you run Biome on GitHub, take advantage of the reporter and still see the erros in console, you can now use both reporters:
If you run Biome on GitHub, take advantage of the reporter and still see the errors in console, you can now use both reporters:
🤖 Prompt for AI Agents
In @.changeset/all-pumas-stop.md at line 9, Fix the typo in the changeset text
by changing the word "erros" to "errors" in the sentence "If you run Biome on
GitHub, take advantage of the reporter and still see the erros in console, you
can now use both reporters:" so it reads "...still see the errors in
console...".


```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. Form example, have the `default` reporter written on terminal, and the `rdjson` reporter written on file:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo: "Form" → "From"

As flagged by static analysis.

-You can combine these two features. Form example, have the `default` reporter written on terminal, and the `rdjson` reporter written on file:
+You can combine these two features. For example, have the `default` reporter written on terminal, and the `rdjson` reporter written on file:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
You can combine these two features. Form example, have the `default` reporter written on terminal, and the `rdjson` reporter written on file:
You can combine these two features. For example, have the `default` reporter written on terminal, and the `rdjson` reporter written on file:
🧰 Tools
🪛 LanguageTool

[uncategorized] ~25-~25: “Form” (shape/structure, to make) seems less likely than “from” (“originating from”).
Context: ...`` You can combine these two features. Form example, have the default reporter wr...

(AI_HYDRA_LEO_CP_FORM_FROM)

🤖 Prompt for AI Agents
In @.changeset/all-pumas-stop.md at line 25, Fix the typo in the sentence that
currently reads "Form example, have the `default` reporter written on terminal,
and the `rdjson` reporter written on file:" by changing "Form" to "From" so it
reads "From 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.**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mismatched emphasis markers and grammar issue

Single * at the start, double ** at the end. Also, "otherwise" needs separation from the clause.

-*The `--reporter` and `--reporter-file` flags must appear next to each other, otherwise an error is thrown.**
+**The `--reporter` and `--reporter-file` flags must appear next to each other; otherwise, an error is thrown.**
🧰 Tools
🪛 LanguageTool

[typographical] ~31-~31: The word “otherwise” is an adverb that can’t be used like a conjunction, and therefore needs to be separated from the sentence.
Context: ...er-file` flags must appear next to each other, otherwise an error is thrown.**

(THUS_SENTENCE)

🤖 Prompt for AI Agents
In @.changeset/all-pumas-stop.md at line 31, Fix the mismatched Markdown
emphasis and the grammar in the sentence describing the flags: use matching
emphasis markers (e.g., wrap the whole sentence in double asterisks **...** or
single *...*) and add proper punctuation before "otherwise" (for example a
semicolon or comma), so the line reads like "**The `--reporter` and
`--reporter-file` flags must appear next to each other; otherwise, an error is
thrown.**" or with single asterisks if preferred.




57 changes: 37 additions & 20 deletions crates/biome_cli/src/cli_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CliReporter>,

/// 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(
Expand Down Expand Up @@ -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<Utf8PathBuf>,
}

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,
Expand All @@ -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<Self, Self::Err> {
match s {
"default" => Ok(Self::Default),
"json" => Ok(Self::Json),
"json-pretty" => Ok(Self::JsonPretty),
"summary" => Ok(Self::Summary),
Expand All @@ -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"),
}
}
}
Expand Down
10 changes: 7 additions & 3 deletions crates/biome_cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -696,11 +696,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
Expand Down
33 changes: 16 additions & 17 deletions crates/biome_cli/src/reporter/checkstyle.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<Utf8PathBuf>,
}

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,
Expand All @@ -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,
Expand All @@ -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<()> {
Expand Down Expand Up @@ -109,7 +108,7 @@ impl<'a> ReporterVisitor for CheckstyleReporterVisitor<'a> {
writeln!(output, " </file>")?;
}
writeln!(output, "</checkstyle>")?;
self.console.log(markup! {{
writer.log(markup! {{
(String::from_utf8_lossy(&output))
}});
Ok(())
Expand Down
26 changes: 17 additions & 9 deletions crates/biome_cli/src/reporter/github.rs
Original file line number Diff line number Diff line change
@@ -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<Utf8PathBuf>,
}

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,
Expand All @@ -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,
Expand All @@ -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)}});
writer.log(markup! {{PrintGitHubDiagnostic(diagnostic)}});
} else if !verbose {
self.0.log(markup! {{PrintGitHubDiagnostic(diagnostic)}});
writer.log(markup! {{PrintGitHubDiagnostic(diagnostic)}});
}
}
}
Comment on lines 54 to 62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Logic bug: non-verbose diagnostics skipped when verbose=true.

The condition else if !verbose means non-verbose diagnostics are only emitted when verbose mode is off. This appears inverted—non-verbose diagnostics should always be emitted.

Compare with the pattern in sarif.rs (lines 150-170) or checkstyle.rs (line 61) which use if is_verbose && !verbose { continue; }.

🐛 Proposed fix
 for diagnostic in &diagnostics_payload.diagnostics {
     if diagnostic.severity() >= diagnostics_payload.diagnostic_level {
-        if diagnostic.tags().is_verbose() && verbose {
-            writer.log(markup! {{PrintGitHubDiagnostic(diagnostic)}});
-        } else if !verbose {
+        if diagnostic.tags().is_verbose() {
+            if verbose {
+                writer.log(markup! {{PrintGitHubDiagnostic(diagnostic)}});
+            }
+        } else {
             writer.log(markup! {{PrintGitHubDiagnostic(diagnostic)}});
         }
     }
 }
🤖 Prompt for AI Agents
In `@crates/biome_cli/src/reporter/github.rs` around lines 54 - 62, The loop
currently skips non-verbose diagnostics when verbose=true; change the logic so
verbose-only diagnostics are skipped when verbose is false instead. In the loop
over diagnostics_payload.diagnostics, replace the nested if/else with: if
diagnostic.tags().is_verbose() && !verbose then continue; after that
unconditionally call writer.log(markup! {{PrintGitHubDiagnostic(diagnostic)}})
whenever diagnostic.severity() >= diagnostics_payload.diagnostic_level. This
mirrors the is_verbose && !verbose check pattern used in sarif.rs/checkstyle.rs
and ensures PrintGitHubDiagnostic is always emitted for non-verbose diagnostics.

Expand Down
38 changes: 21 additions & 17 deletions crates/biome_cli/src/reporter/gitlab.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -18,25 +18,30 @@ 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<Utf8PathBuf>,
}

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(),
)?;
Ok(())
}
}

pub(crate) struct GitLabReporterVisitor<'a> {
console: &'a mut dyn Console,
pub(crate) struct GitLabReporterVisitor {
repository_root: Option<Utf8PathBuf>,
}

Expand All @@ -59,18 +64,16 @@ impl GitLabHasher {
}
}

impl<'a> GitLabReporterVisitor<'a> {
pub fn new(console: &'a mut dyn Console, repository_root: Option<Utf8PathBuf>) -> Self {
Self {
console,
repository_root,
}
impl GitLabReporterVisitor {
pub fn new(repository_root: Option<Utf8PathBuf>) -> 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,
Expand All @@ -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<()> {
Expand All @@ -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<GitLabHasher>,
path: Option<&'a Utf8Path>,
Expand Down
Loading
Loading