Skip to content

Commit 8a0eb2a

Browse files
shellscapecamc314
andauthored
feat(oxlint): add stylish formatter (#8607)
👋 This implements a reporter for `--format` on `oxlint` which aims to be visually similar to https://eslint.org/docs/latest/use/formatters/#stylish Please note that this is my first time working with Rust and my knowledge is very limited. I'm unlikely to understand best-practice or best-pattern references outside of what clippy/cargo lint has already had me change. If this needs modification, please help me out by making code suggestions that can be merged to this PR. Resolves #8422 --------- Co-authored-by: Cameron <cameron.clark@hey.com>
1 parent 178c232 commit 8a0eb2a

File tree

2 files changed

+151
-2
lines changed

2 files changed

+151
-2
lines changed

apps/oxlint/src/output_formatter/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ mod checkstyle;
22
mod default;
33
mod github;
44
mod json;
5+
mod stylish;
56
mod unix;
67

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

1011
use checkstyle::CheckStyleOutputFormatter;
1112
use github::GithubOutputFormatter;
13+
use stylish::StylishOutputFormatter;
1214
use unix::UnixOutputFormatter;
1315

1416
use oxc_diagnostics::reporter::DiagnosticReporter;
@@ -24,6 +26,7 @@ pub enum OutputFormat {
2426
Json,
2527
Unix,
2628
Checkstyle,
29+
Stylish,
2730
}
2831

2932
impl FromStr for OutputFormat {
@@ -36,13 +39,13 @@ impl FromStr for OutputFormat {
3639
"unix" => Ok(Self::Unix),
3740
"checkstyle" => Ok(Self::Checkstyle),
3841
"github" => Ok(Self::Github),
42+
"stylish" => Ok(Self::Stylish),
3943
_ => Err(format!("'{s}' is not a known format")),
4044
}
4145
}
4246
}
4347

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

4851
fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter>;
@@ -64,10 +67,10 @@ impl OutputFormatter {
6467
OutputFormat::Github => Box::new(GithubOutputFormatter),
6568
OutputFormat::Unix => Box::<UnixOutputFormatter>::default(),
6669
OutputFormat::Default => Box::new(DefaultOutputFormatter),
70+
OutputFormat::Stylish => Box::<StylishOutputFormatter>::default(),
6771
}
6872
}
6973

70-
// print all rules which are currently supported by oxlint
7174
pub fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
7275
self.internal_formatter.all_rules(writer);
7376
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
use std::io::Write;
2+
3+
use oxc_diagnostics::{
4+
reporter::{DiagnosticReporter, Info},
5+
Error, Severity,
6+
};
7+
use rustc_hash::FxHashMap;
8+
9+
use crate::output_formatter::InternalFormatter;
10+
11+
#[derive(Debug, Default)]
12+
pub struct StylishOutputFormatter;
13+
14+
impl InternalFormatter for StylishOutputFormatter {
15+
fn all_rules(&mut self, writer: &mut dyn Write) {
16+
writeln!(writer, "flag --rules with flag --format=stylish is not allowed").unwrap();
17+
}
18+
19+
fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter> {
20+
Box::new(StylishReporter::default())
21+
}
22+
}
23+
24+
#[derive(Default)]
25+
struct StylishReporter {
26+
diagnostics: Vec<Error>,
27+
}
28+
29+
impl DiagnosticReporter for StylishReporter {
30+
fn finish(&mut self) -> Option<String> {
31+
Some(format_stylish(&self.diagnostics))
32+
}
33+
34+
fn render_error(&mut self, error: Error) -> Option<String> {
35+
self.diagnostics.push(error);
36+
None
37+
}
38+
}
39+
40+
fn format_stylish(diagnostics: &[Error]) -> String {
41+
if diagnostics.is_empty() {
42+
return String::new();
43+
}
44+
45+
let mut output = String::new();
46+
let mut total_errors = 0;
47+
let mut total_warnings = 0;
48+
49+
let mut grouped: FxHashMap<String, Vec<&Error>> = FxHashMap::default();
50+
let mut sorted = diagnostics.iter().collect::<Vec<_>>();
51+
52+
sorted.sort_by_key(|diagnostic| Info::new(diagnostic).line);
53+
54+
for diagnostic in sorted {
55+
let info = Info::new(diagnostic);
56+
grouped.entry(info.filename).or_default().push(diagnostic);
57+
}
58+
59+
for diagnostics in grouped.values() {
60+
let diagnostic = diagnostics[0];
61+
let info = Info::new(diagnostic);
62+
let filename = info.filename;
63+
let filename = if let Some(path) =
64+
std::env::current_dir().ok().and_then(|d| d.join(&filename).canonicalize().ok())
65+
{
66+
path.display().to_string()
67+
} else {
68+
filename
69+
};
70+
let max_len_width = diagnostics
71+
.iter()
72+
.filter_map(|diagnostic| diagnostic.labels())
73+
.flat_map(std::iter::Iterator::collect::<Vec<_>>)
74+
.map(|label| format!("{}:{}", label.offset(), label.len()).len())
75+
.max()
76+
.unwrap_or(0);
77+
78+
output.push_str(&format!("\n\u{1b}[4m{filename}\u{1b}[0m\n"));
79+
80+
for diagnostic in diagnostics {
81+
match diagnostic.severity() {
82+
Some(Severity::Error) => total_errors += 1,
83+
_ => total_warnings += 1,
84+
}
85+
86+
let severity_str = if diagnostic.severity() == Some(Severity::Error) {
87+
"\u{1b}[31merror\u{1b}[0m"
88+
} else {
89+
"\u{1b}[33mwarning\u{1b}[0m"
90+
};
91+
92+
if let Some(label) = diagnostic.labels().expect("should have labels").next() {
93+
let rule = diagnostic.code().map_or_else(String::new, |code| code.to_string());
94+
let position = format!("{}:{}", label.offset(), label.len());
95+
output.push_str(
96+
&format!(" \u{1b}[2m{position:max_len_width$}\u{1b}[0m {severity_str} {diagnostic} \u{1b}[2m{rule}\u{1b}[0m\n"),
97+
);
98+
}
99+
}
100+
}
101+
102+
let total = total_errors + total_warnings;
103+
if total > 0 {
104+
let summary_color = if total_errors > 0 { "\u{1b}[31m" } else { "\u{1b}[33m" };
105+
output.push_str(&format!(
106+
"\n{summary_color}✖ {total} problem{} ({total_errors} error{}, {total_warnings} warning{})\u{1b}[0m\n",
107+
if total == 1 { "" } else { "s" },
108+
if total_errors == 1 { "" } else { "s" },
109+
if total_warnings == 1 { "" } else { "s" }
110+
));
111+
}
112+
113+
output
114+
}
115+
116+
#[cfg(test)]
117+
mod test {
118+
use super::*;
119+
use oxc_diagnostics::{NamedSource, OxcDiagnostic};
120+
use oxc_span::Span;
121+
122+
#[test]
123+
fn test_stylish_reporter() {
124+
let mut reporter = StylishReporter::default();
125+
126+
let error = OxcDiagnostic::error("error message")
127+
.with_label(Span::new(0, 8))
128+
.with_source_code(NamedSource::new("file.js", "code"));
129+
130+
let warning = OxcDiagnostic::warn("warning message")
131+
.with_label(Span::new(0, 8))
132+
.with_source_code(NamedSource::new("file.js", "code"));
133+
134+
reporter.render_error(error);
135+
reporter.render_error(warning);
136+
137+
let output = reporter.finish().unwrap();
138+
139+
assert!(output.contains("error message"), "Output should contain 'error message'");
140+
assert!(output.contains("warning message"), "Output should contain 'warning message'");
141+
assert!(output.contains("\u{2716}"), "Output should contain the ✖ character");
142+
assert!(output.contains("2 problems"), "Output should mention total problems");
143+
assert!(output.contains("1 error"), "Output should mention error count");
144+
assert!(output.contains("1 warning"), "Output should mention warning count");
145+
}
146+
}

0 commit comments

Comments
 (0)