Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 5 additions & 4 deletions apps/oxlint/src/output_formatter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ mod checkstyle;
mod default;
mod github;
mod json;
mod stylish;
mod unix;

use std::io::{BufWriter, Stdout, Write};
use std::str::FromStr;

use checkstyle::CheckStyleOutputFormatter;
use github::GithubOutputFormatter;
use stylish::StylishOutputFormatter;
use unix::UnixOutputFormatter;

use oxc_diagnostics::reporter::DiagnosticReporter;
Expand All @@ -18,12 +20,11 @@ use crate::output_formatter::{default::DefaultOutputFormatter, json::JsonOutputF
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum OutputFormat {
Default,
/// GitHub Check Annotation
Comment thread
camc314 marked this conversation as resolved.
/// <https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message>
Github,
Json,
Unix,
Checkstyle,
Stylish,
}

impl FromStr for OutputFormat {
Expand All @@ -36,13 +37,13 @@ impl FromStr for OutputFormat {
"unix" => Ok(Self::Unix),
"checkstyle" => Ok(Self::Checkstyle),
"github" => Ok(Self::Github),
"stylish" => Ok(Self::Stylish),
_ => Err(format!("'{s}' is not a known format")),
}
}
}

trait InternalFormatter {
// print all rules which are currently supported by oxlint
fn all_rules(&mut self, writer: &mut dyn Write);

fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter>;
Expand All @@ -64,10 +65,10 @@ impl OutputFormatter {
OutputFormat::Github => Box::new(GithubOutputFormatter),
OutputFormat::Unix => Box::<UnixOutputFormatter>::default(),
OutputFormat::Default => Box::new(DefaultOutputFormatter),
OutputFormat::Stylish => Box::<StylishOutputFormatter>::default(),
}
}

// print all rules which are currently supported by oxlint
pub fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
self.internal_formatter.all_rules(writer);
}
Expand Down
148 changes: 148 additions & 0 deletions apps/oxlint/src/output_formatter/stylish.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use std::io::Write;

use oxc_diagnostics::{
reporter::{DiagnosticReporter, Info},
Error, Severity,
};
use rustc_hash::FxHashMap;

use crate::output_formatter::InternalFormatter;

#[derive(Debug, Default)]
pub struct StylishOutputFormatter;

impl InternalFormatter for StylishOutputFormatter {
fn all_rules(&mut self, writer: &mut dyn Write) {
writeln!(writer, "flag --rules with flag --format=stylish is not allowed").unwrap();
}

fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
Box::new(StylishReporter::default())
}
}

#[derive(Default)]
struct StylishReporter {
diagnostics: Vec<Error>,
}

impl DiagnosticReporter for StylishReporter {
fn finish(&mut self) -> Option<String> {
Some(format_stylish(&self.diagnostics))
}

fn render_error(&mut self, error: Error) -> Option<String> {
self.diagnostics.push(error);
None
}
}

fn format_stylish(diagnostics: &[Error]) -> String {
if diagnostics.is_empty() {
return String::new();
}

let mut output = String::new();
let mut total_errors = 0;
let mut total_warnings = 0;

let mut grouped: FxHashMap<String, Vec<&Error>> = FxHashMap::default();
let sorted = diagnostics.iter().collect::<Vec<_>>();
let mut sorted = sorted.clone();
Comment thread
shellscape marked this conversation as resolved.
Outdated

sorted.sort_by_key(|diagnostic| Info::new(diagnostic).line);

for diagnostic in sorted {
let info = Info::new(diagnostic);
grouped.entry(info.filename).or_default().push(diagnostic);
}

for diagnostics in grouped.values() {
let diagnostic = diagnostics[0];
// output.push_str(&format!("\n\u{1b}[4mFile\u{1b}[0m\n"));
let info = Info::new(diagnostic);
let filename = info.filename;
let filename = if let Some(path) =
std::env::current_dir().ok().and_then(|d| d.join(&filename).canonicalize().ok())
Comment thread
Boshen marked this conversation as resolved.
{
path.display().to_string()
} else {
filename
};
let max_len_width = diagnostics
.iter()
.filter_map(|diagnostic| diagnostic.labels())
.flat_map(std::iter::Iterator::collect::<Vec<_>>)
.map(|label| format!("{}:{}", label.offset(), label.len()).len())
.max()
.unwrap_or(0);

output.push_str(&format!("\n\u{1b}[4m{filename}\u{1b}[0m\n"));

for diagnostic in diagnostics {
match diagnostic.severity() {
Some(Severity::Error) => total_errors += 1,
_ => total_warnings += 1,
}

let severity_str = if diagnostic.severity() == Some(Severity::Error) {
"\u{1b}[31merror\u{1b}[0m"
} else {
"\u{1b}[33mwarning\u{1b}[0m"
};

if let Some(label) = diagnostic.labels().expect("should have labels").next() {
let rule = diagnostic.code().map_or_else(String::new, |code| code.to_string());
let position = format!("{}:{}", label.offset(), label.len());
output.push_str(
&format!(" \u{1b}[2m{position:max_len_width$}\u{1b}[0m {severity_str} {diagnostic} \u{1b}[2m{rule}\u{1b}[0m\n"),
);
}
}
}

let total = total_errors + total_warnings;
if total > 0 {
let summary_color = if total_errors > 0 { "\u{1b}[31m" } else { "\u{1b}[33m" };
output.push_str(&format!(
"\n{summary_color}✖ {total} problem{} ({total_errors} error{}, {total_warnings} warning{})\u{1b}[0m\n",
if total == 1 { "" } else { "s" },
if total_errors == 1 { "" } else { "s" },
if total_warnings == 1 { "" } else { "s" }
));
}

output
}

#[cfg(test)]
mod test {
use super::*;
use oxc_diagnostics::{NamedSource, OxcDiagnostic};
use oxc_span::Span;

#[test]
fn test_stylish_reporter() {
let mut reporter = StylishReporter::default();

let error = OxcDiagnostic::error("error message")
.with_label(Span::new(0, 8))
.with_source_code(NamedSource::new("file.js", "code"));

let warning = OxcDiagnostic::warn("warning message")
.with_label(Span::new(0, 8))
.with_source_code(NamedSource::new("file.js", "code"));

reporter.render_error(error);
reporter.render_error(warning);

let output = reporter.finish().unwrap();

assert!(output.contains("error message"), "Output should contain 'error message'");
assert!(output.contains("warning message"), "Output should contain 'warning message'");
assert!(output.contains("\u{2716}"), "Output should contain the ✖ character");
assert!(output.contains("2 problems"), "Output should mention total problems");
assert!(output.contains("1 error"), "Output should mention error count");
assert!(output.contains("1 warning"), "Output should mention warning count");
}
}