diff --git a/apps/oxlint/src/output_formatter/stylish.rs b/apps/oxlint/src/output_formatter/stylish.rs index 8973dacb7c9a8..e88ccb3639ebc 100644 --- a/apps/oxlint/src/output_formatter/stylish.rs +++ b/apps/oxlint/src/output_formatter/stylish.rs @@ -17,14 +17,20 @@ impl InternalFormatter for StylishOutputFormatter { } } -#[derive(Default)] struct StylishReporter { diagnostics: Vec, + no_color: bool, +} + +impl Default for StylishReporter { + fn default() -> Self { + Self { diagnostics: Vec::new(), no_color: std::env::var("NO_COLOR").is_ok() } + } } impl DiagnosticReporter for StylishReporter { fn finish(&mut self, _: &DiagnosticResult) -> Option { - Some(format_stylish(&self.diagnostics)) + Some(self.format_stylish()) } fn render_error(&mut self, error: Error) -> Option { @@ -33,82 +39,112 @@ impl DiagnosticReporter for StylishReporter { } } -fn format_stylish(diagnostics: &[Error]) -> String { - if diagnostics.is_empty() { - return String::new(); +impl StylishReporter { + #[cfg(test)] + fn with_no_color(mut self, no_color: bool) -> Self { + self.no_color = no_color; + self } - let mut output = String::new(); - let mut total_errors = 0; - let mut total_warnings = 0; + fn format_stylish(&self) -> String { + if self.diagnostics.is_empty() { + return String::new(); + } - let mut grouped: FxHashMap> = FxHashMap::default(); - let mut sorted = diagnostics.iter().collect::>(); + let no_color = self.no_color; - sorted.sort_by_key(|diagnostic| Info::new(diagnostic).start.line); + let mut output = String::new(); + let mut total_errors = 0; + let mut total_warnings = 0; - for diagnostic in sorted { - let info = Info::new(diagnostic); - grouped.entry(info.filename).or_default().push(diagnostic); - } + let mut grouped: FxHashMap> = FxHashMap::default(); + let mut sorted = self.diagnostics.iter().collect::>(); - 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() - .map(|diagnostic| { - let start = Info::new(diagnostic).start; - format!("{}:{}", start.line, start.column).len() - }) - .max() - .unwrap_or(0); - - writeln!(output, "\n\u{1b}[4m{filename}\u{1b}[0m").unwrap(); - - for diagnostic in diagnostics { - match diagnostic.severity() { - Some(Severity::Error) => total_errors += 1, - _ => total_warnings += 1, - } + sorted.sort_by_key(|diagnostic| Info::new(diagnostic).start.line); + + for diagnostic in sorted { + let info = Info::new(diagnostic); + grouped.entry(info.filename).or_default().push(diagnostic); + } - let severity_str = if diagnostic.severity() == Some(Severity::Error) { - "\u{1b}[31merror\u{1b}[0m" + 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 { - "\u{1b}[33mwarning\u{1b}[0m" + filename }; + let max_len_width = diagnostics + .iter() + .map(|diagnostic| { + let start = Info::new(diagnostic).start; + format!("{}:{}", start.line, start.column).len() + }) + .max() + .unwrap_or(0); + + if no_color { + writeln!(output, "\n{filename}").unwrap(); + } else { + writeln!(output, "\n\u{1b}[4m{filename}\u{1b}[0m").unwrap(); + } - let info = Info::new(diagnostic); - let rule = diagnostic.code().map_or_else(String::new, |code| code.to_string()); - let position = format!("{}:{}", info.start.line, info.start.column); - writeln!( - output, - " \u{1b}[2m{position:max_len_width$}\u{1b}[0m {severity_str} {diagnostic} \u{1b}[2m{rule}\u{1b}[0m" - ).unwrap(); + 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) { + if no_color { "error" } else { "\u{1b}[31merror\u{1b}[0m" } + } else { + if no_color { "warning" } else { "\u{1b}[33mwarning\u{1b}[0m" } + }; + + let info = Info::new(diagnostic); + let rule = diagnostic.code().map_or_else(String::new, |code| code.to_string()); + let position = format!("{}:{}", info.start.line, info.start.column); + if no_color { + writeln!( + output, + " {position:max_len_width$} {severity_str} {diagnostic} {rule}" + ) + .unwrap(); + } else { + writeln!( + output, + " \u{1b}[2m{position:max_len_width$}\u{1b}[0m {severity_str} {diagnostic} \u{1b}[2m{rule}\u{1b}[0m" + ).unwrap(); + } + } } - } - let total = total_errors + total_warnings; - if total > 0 { - let summary_color = if total_errors > 0 { "\u{1b}[31m" } else { "\u{1b}[33m" }; - writeln!( + let total = total_errors + total_warnings; + if total > 0 { + let summary_color = if no_color { + "" + } else if total_errors > 0 { + "\u{1b}[31m" + } else { + "\u{1b}[33m" + }; + let summary_end_color = if no_color { "" } else { "\u{1b}[0m" }; + + writeln!( output, - "\n{summary_color}✖ {total} problem{} ({total_errors} error{}, {total_warnings} warning{})\u{1b}[0m", + "\n{summary_color}✖ {total} problem{} ({total_errors} error{}, {total_warnings} warning{}){summary_end_color}", if total == 1 { "" } else { "s" }, if total_errors == 1 { "" } else { "s" }, if total_warnings == 1 { "" } else { "s" } ).unwrap(); - } + } - output + output + } } #[cfg(test)] @@ -119,7 +155,7 @@ mod test { #[test] fn test_stylish_reporter() { - let mut reporter = StylishReporter::default(); + let mut reporter = StylishReporter::default().with_no_color(true); let error = OxcDiagnostic::error("error message") .with_label(Span::new(0, 8)) @@ -140,5 +176,24 @@ mod test { 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"); + assert!( + !output.contains("\u{1b}[31m\u{2716}"), + "Output should not color the ✖ character red" + ); + } + + #[test] + fn test_stylish_reporter_colored() { + let mut reporter = StylishReporter::default().with_no_color(false); + + let error = OxcDiagnostic::error("error message") + .with_label(Span::new(0, 8)) + .with_source_code(NamedSource::new("file.js", "code")); + + reporter.render_error(error); + + let output = reporter.finish(&DiagnosticResult::default()).unwrap(); + + assert!(output.contains("\u{1b}[31m\u{2716}"), "Output should color the ✖ character red"); } }