mirror of
https://github.com/danbulant/oxc
synced 2026-05-20 12:48:38 +00:00
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>
This commit is contained in:
parent
178c2322f5
commit
8a0eb2abb7
2 changed files with 151 additions and 2 deletions
|
|
@ -2,6 +2,7 @@ mod checkstyle;
|
|||
mod default;
|
||||
mod github;
|
||||
mod json;
|
||||
mod stylish;
|
||||
mod unix;
|
||||
|
||||
use std::io::{BufWriter, Stdout, Write};
|
||||
|
|
@ -9,6 +10,7 @@ use std::str::FromStr;
|
|||
|
||||
use checkstyle::CheckStyleOutputFormatter;
|
||||
use github::GithubOutputFormatter;
|
||||
use stylish::StylishOutputFormatter;
|
||||
use unix::UnixOutputFormatter;
|
||||
|
||||
use oxc_diagnostics::reporter::DiagnosticReporter;
|
||||
|
|
@ -24,6 +26,7 @@ pub enum OutputFormat {
|
|||
Json,
|
||||
Unix,
|
||||
Checkstyle,
|
||||
Stylish,
|
||||
}
|
||||
|
||||
impl FromStr for OutputFormat {
|
||||
|
|
@ -36,13 +39,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>;
|
||||
|
|
@ -64,10 +67,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);
|
||||
}
|
||||
|
|
|
|||
146
apps/oxlint/src/output_formatter/stylish.rs
Normal file
146
apps/oxlint/src/output_formatter/stylish.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
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 mut sorted = diagnostics.iter().collect::<Vec<_>>();
|
||||
|
||||
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];
|
||||
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())
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue