mirror of
https://github.com/danbulant/oxc
synced 2026-05-22 05:38:54 +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 default;
|
||||||
mod github;
|
mod github;
|
||||||
mod json;
|
mod json;
|
||||||
|
mod stylish;
|
||||||
mod unix;
|
mod unix;
|
||||||
|
|
||||||
use std::io::{BufWriter, Stdout, Write};
|
use std::io::{BufWriter, Stdout, Write};
|
||||||
|
|
@ -9,6 +10,7 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use checkstyle::CheckStyleOutputFormatter;
|
use checkstyle::CheckStyleOutputFormatter;
|
||||||
use github::GithubOutputFormatter;
|
use github::GithubOutputFormatter;
|
||||||
|
use stylish::StylishOutputFormatter;
|
||||||
use unix::UnixOutputFormatter;
|
use unix::UnixOutputFormatter;
|
||||||
|
|
||||||
use oxc_diagnostics::reporter::DiagnosticReporter;
|
use oxc_diagnostics::reporter::DiagnosticReporter;
|
||||||
|
|
@ -24,6 +26,7 @@ pub enum OutputFormat {
|
||||||
Json,
|
Json,
|
||||||
Unix,
|
Unix,
|
||||||
Checkstyle,
|
Checkstyle,
|
||||||
|
Stylish,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for OutputFormat {
|
impl FromStr for OutputFormat {
|
||||||
|
|
@ -36,13 +39,13 @@ impl FromStr for OutputFormat {
|
||||||
"unix" => Ok(Self::Unix),
|
"unix" => Ok(Self::Unix),
|
||||||
"checkstyle" => Ok(Self::Checkstyle),
|
"checkstyle" => Ok(Self::Checkstyle),
|
||||||
"github" => Ok(Self::Github),
|
"github" => Ok(Self::Github),
|
||||||
|
"stylish" => Ok(Self::Stylish),
|
||||||
_ => Err(format!("'{s}' is not a known format")),
|
_ => Err(format!("'{s}' is not a known format")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trait InternalFormatter {
|
trait InternalFormatter {
|
||||||
// print all rules which are currently supported by oxlint
|
|
||||||
fn all_rules(&mut self, writer: &mut dyn Write);
|
fn all_rules(&mut self, writer: &mut dyn Write);
|
||||||
|
|
||||||
fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter>;
|
fn get_diagnostic_reporter(&self) -> Box<dyn DiagnosticReporter>;
|
||||||
|
|
@ -64,10 +67,10 @@ impl OutputFormatter {
|
||||||
OutputFormat::Github => Box::new(GithubOutputFormatter),
|
OutputFormat::Github => Box::new(GithubOutputFormatter),
|
||||||
OutputFormat::Unix => Box::<UnixOutputFormatter>::default(),
|
OutputFormat::Unix => Box::<UnixOutputFormatter>::default(),
|
||||||
OutputFormat::Default => Box::new(DefaultOutputFormatter),
|
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>) {
|
pub fn all_rules(&mut self, writer: &mut BufWriter<Stdout>) {
|
||||||
self.internal_formatter.all_rules(writer);
|
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