perf(diagnostics): batch graphical reports (#25710)

Batch graphical diagnostics so reports sharing a source reuse line
indexing, and route oxlint diagnostic batches through the new API.

AI assistance: Codex was used. The contributor remains responsible for
this change.

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a></sub>
This commit is contained in:
Boshen authored and GitHub committed 2026-08-16 19:06:16 +08:00
1 parent f004852aac
commit 9c8abab7a7
6 files changed
+321 -39

No files matched your search

@@ -130,6 +130,33 @@ impl DiagnosticReporter for GraphicalReporter {
self.handler.render_report(&mut output, error.as_ref()).unwrap();
Some(output)
}
fn render_errors(&mut self, errors: Vec<Error>, emit: &mut dyn FnMut(&str)) {
let mut output = String::new();
self.handler
.render_reports(
&mut output,
errors.iter().map(|error| error.as_ref() as &dyn oxc_diagnostics::Diagnostic),
)
.unwrap();
emit(&output);
}
fn render_errors_until(
&mut self,
errors: Vec<Error>,
keep: &mut dyn FnMut(Option<&str>, &str) -> bool,
) {
self.handler
.render_reports_until(
errors.iter().map(|error| error.as_ref() as &dyn oxc_diagnostics::Diagnostic),
&mut |diagnostic, rendered| {
let source_name = diagnostic.source_code().and_then(|source| source.name());
keep(source_name, rendered)
},
)
.unwrap();
}
}
pub(super) fn get_diagnostic_result_output(result: &DiagnosticResult) -> String {
@@ -12,7 +12,7 @@ use std::fmt;
use owo_colors::OwoColorize;
use super::handler::{GraphicalReportHandler, LinkStyle};
use crate::{Diagnostic, Severity};
use crate::{Diagnostic, Severity, source_impls::SpanScanner};
impl GraphicalReportHandler {
/// Render a [`Diagnostic`].
@@ -24,11 +24,83 @@ impl GraphicalReportHandler {
&self,
f: &mut impl fmt::Write,
diagnostic: &dyn Diagnostic,
) -> fmt::Result {
let source = diagnostic.source_code();
let mut scanner = source.map(|source| SpanScanner::new(source.data(), 1, 1));
let source_name = source.and_then(|source| source.name());
self.render_report_with_scanner(f, diagnostic, scanner.as_mut(), source_name)
}
/// Render [`Diagnostic`]s in order, reusing line indexes for shared sources.
///
/// # Errors
///
/// Returns an error when writing the rendered reports fails.
pub fn render_reports<'a>(
&self,
f: &mut impl fmt::Write,
diagnostics: impl IntoIterator<Item = &'a dyn Diagnostic>,
) -> fmt::Result {
let mut scanner: Option<SpanScanner<'a>> = None;
for diagnostic in diagnostics {
let source = diagnostic.source_code();
match source {
Some(source)
if scanner.as_ref().is_some_and(|scanner| scanner.is_for(source.data())) => {}
Some(source) => scanner = Some(SpanScanner::new(source.data(), 1, 1)),
None => scanner = None,
}
let source_name = source.and_then(|source| source.name());
self.render_report_with_scanner(f, diagnostic, scanner.as_mut(), source_name)?;
}
Ok(())
}
/// Render [`Diagnostic`]s until `keep` rejects a rendered report.
///
/// # Errors
///
/// Returns an error when writing the rendered reports fails.
pub fn render_reports_until<'a>(
&self,
diagnostics: impl IntoIterator<Item = &'a dyn Diagnostic>,
keep: &mut dyn FnMut(&dyn Diagnostic, &str) -> bool,
) -> fmt::Result {
let mut scanner: Option<SpanScanner<'a>> = None;
let mut output = String::new();
for diagnostic in diagnostics {
let source = diagnostic.source_code();
match source {
Some(source)
if scanner.as_ref().is_some_and(|scanner| scanner.is_for(source.data())) => {}
Some(source) => scanner = Some(SpanScanner::new(source.data(), 1, 1)),
None => scanner = None,
}
let source_name = source.and_then(|source| source.name());
output.clear();
self.render_report_with_scanner(
&mut output,
diagnostic,
scanner.as_mut(),
source_name,
)?;
if !keep(diagnostic, &output) {
break;
}
}
Ok(())
}
fn render_report_with_scanner(
&self,
f: &mut impl fmt::Write,
diagnostic: &dyn Diagnostic,
scanner: Option<&mut SpanScanner<'_>>,
source_name: Option<&str>,
) -> fmt::Result {
writeln!(f)?;
self.render_title(f, diagnostic)?;
let src = diagnostic.source_code();
self.render_snippets(f, diagnostic, src)?;
self.render_snippets(f, diagnostic, scanner, source_name)?;
self.render_footer(f, diagnostic)?;
Ok(())
}
@@ -221,7 +293,50 @@ impl GraphicalReportHandler {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use oxc_span::Span;
use super::*;
use crate::{Error, GraphicalTheme, NamedSource, OxcDiagnostic};
#[test]
fn batch_render_matches_individual_reports() {
let source =
Arc::new(NamedSource::new("input.ts", "first();\nsecond();\nthird();\nfourth();\n"));
let other_source = Arc::new(NamedSource::new("other.ts", "alpha();\nbeta();\n"));
let diagnostics: Vec<Error> = vec![
OxcDiagnostic::warn("later")
.with_label(Span::new(29, 35))
.with_source_code(Arc::clone(&source)),
OxcDiagnostic::error("multi-label")
.with_labels([Span::new(0, 5), Span::new(19, 24)])
.with_source_code(Arc::clone(&source)),
OxcDiagnostic::warn("earlier")
.with_label(Span::new(9, 15))
.with_source_code(Arc::clone(&source)),
OxcDiagnostic::error("without source").into(),
OxcDiagnostic::warn("different source")
.with_label(Span::new(9, 13))
.with_source_code(other_source),
];
let handler = GraphicalReportHandler::new_themed(GraphicalTheme::none()).with_links(false);
let mut expected = String::new();
for diagnostic in &diagnostics {
handler.render_report(&mut expected, diagnostic.as_ref()).unwrap();
}
let mut actual = String::new();
handler
.render_reports(
&mut actual,
diagnostics.iter().map(|diagnostic| diagnostic.as_ref() as &dyn Diagnostic),
)
.unwrap();
assert_eq!(actual, expected);
}
#[test]
#[cfg_attr(
@@ -18,7 +18,7 @@ use super::{
span::{FancySpan, LabelRenderMode},
};
use crate::{
Diagnostic, LabeledSpan, SourceCode,
Diagnostic, LabeledSpan,
source_impls::{SpanContents, SpanScanner},
};
@@ -27,18 +27,15 @@ impl GraphicalReportHandler {
&self,
f: &mut impl fmt::Write,
diagnostic: &dyn Diagnostic,
opt_source: Option<&dyn SourceCode>,
scanner: Option<&mut SpanScanner<'_>>,
source_name: Option<&str>,
) -> fmt::Result {
let Some(source) = opt_source else { return Ok(()) };
let Some(scanner) = scanner else { return Ok(()) };
let labels = diagnostic.labels();
if labels.is_empty() {
return Ok(());
}
// Share one forward scan across every span lookup below (one per label
// plus one per merge attempt).
let mut scanner = SpanScanner::new(source.data(), 1, 1);
let source_name = source.name();
let mut read = |span| scanner.read_span(span);
if let [label] = labels {
+26
View File
@@ -54,6 +54,32 @@ pub trait DiagnosticReporter {
true
}
/// Render diagnostics in order, delegating to [`render_error`](Self::render_error) by default.
fn render_errors(&mut self, errors: Vec<Error>, emit: &mut dyn FnMut(&str)) {
for error in errors {
if let Some(rendered) = self.render_error(error) {
emit(&rendered);
}
}
}
/// Render diagnostics until `keep` rejects a rendered report.
fn render_errors_until(
&mut self,
errors: Vec<Error>,
keep: &mut dyn FnMut(Option<&str>, &str) -> bool,
) {
for error in errors {
let source_name =
error.source_code().and_then(|source| source.name()).map(ToString::to_string);
if let Some(rendered) = self.render_error(error)
&& !keep(source_name.as_deref(), &rendered)
{
break;
}
}
}
/// Render a diagnostic into this reporter's desired format. For example, a JSONLinesReporter
/// might return a stringified JSON object on a single line. Returns [`None`] to skip reporting
/// of this diagnostic.
+141 -29
View File
@@ -148,7 +148,7 @@ impl DiagnosticService {
let supports_minified_file_fallback = self.reporter.supports_minified_file_fallback();
while let Ok(diagnostics) = self.receiver.recv() {
let mut is_minified = false;
let mut diagnostics_to_render = Vec::with_capacity(diagnostics.len());
for diagnostic in diagnostics {
match diagnostic.severity() {
Some(Severity::Warning) => {
@@ -164,40 +164,51 @@ impl DiagnosticService {
Some(Severity::Advice) => {}
}
if self.silent || is_minified {
continue;
if !self.silent {
diagnostics_to_render.push(diagnostic);
}
}
let path = diagnostic
.source_code()
.and_then(|source| source.name())
.map(ToString::to_string);
if diagnostics_to_render.is_empty() {
continue;
}
if let Some(err_str) = self.reporter.render_error(diagnostic) {
// Skip large output and print only once.
// Setting to 1200 because graphical output may contain ansi escape codes and other decorations.
if supports_minified_file_fallback
&& err_str.lines().any(|line| line.len() >= 1200)
{
let mut diagnostic =
OxcDiagnostic::warn("File is too long to fit on the screen");
if let Some(path) = path {
diagnostic =
diagnostic.with_help(format!("{path} seems like a minified file"));
}
let minified_diagnostic = diagnostic.into();
if let Some(err_str) = self.reporter.render_error(minified_diagnostic) {
let mut is_minified = false;
let mut path = None;
if supports_minified_file_fallback {
self.reporter.render_errors_until(
diagnostics_to_render,
&mut |source_name, rendered| {
// Setting to 1200 because graphical output may contain ansi escape codes
// and other decorations.
if rendered.lines().any(|line| line.len() >= 1200) {
is_minified = true;
path = source_name.map(ToString::to_string);
false
} else {
writer
.write_all(err_str.as_bytes())
.write_all(rendered.as_bytes())
.or_else(Self::check_for_writer_error)
.unwrap();
true
}
is_minified = true;
continue;
}
},
);
} else {
self.reporter.render_errors(diagnostics_to_render, &mut |rendered| {
writer
.write_all(rendered.as_bytes())
.or_else(Self::check_for_writer_error)
.unwrap();
});
}
if is_minified {
let mut diagnostic = OxcDiagnostic::warn("File is too long to fit on the screen");
if let Some(path) = path {
diagnostic = diagnostic.with_help(format!("{path} seems like a minified file"));
}
if let Some(err_str) = self.reporter.render_error(diagnostic.into()) {
writer
.write_all(err_str.as_bytes())
.or_else(Self::check_for_writer_error)
@@ -330,10 +341,16 @@ fn strict_canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::{
path::PathBuf,
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
};
use crate::{
Error, OxcDiagnostic,
Error, NamedSource, OxcDiagnostic,
reporter::{DiagnosticReporter, DiagnosticResult},
service::from_file_path,
};
@@ -442,4 +459,99 @@ mod tests {
assert_eq!(output, format!("{}\n", "x".repeat(1200)));
}
struct BatchReporter(Arc<Mutex<Vec<usize>>>);
impl DiagnosticReporter for BatchReporter {
fn finish(&mut self, _result: &DiagnosticResult) -> Option<String> {
None
}
fn render_errors(&mut self, errors: Vec<Error>, emit: &mut dyn FnMut(&str)) {
self.0.lock().unwrap().push(errors.len());
let output =
errors.into_iter().map(|error| error.to_string()).collect::<Vec<_>>().join(",");
emit(&output);
}
fn supports_minified_file_fallback(&self) -> bool {
false
}
fn render_error(&mut self, _error: Error) -> Option<String> {
None
}
}
#[test]
fn renders_a_filtered_batch() {
let batch_sizes = Arc::new(Mutex::new(Vec::new()));
let (service, sender) =
DiagnosticService::new(Box::new(BatchReporter(Arc::clone(&batch_sizes))));
let mut service = service.with_quiet(true);
sender
.send(vec![
OxcDiagnostic::warn("hidden warning").into(),
OxcDiagnostic::error("visible error").into(),
])
.unwrap();
drop(sender);
let mut output = Vec::new();
let result = service.run(&mut output);
assert_eq!(String::from_utf8(output).unwrap(), "visible error");
assert_eq!(*batch_sizes.lock().unwrap(), [1]);
assert_eq!(result.warnings_count(), 1);
assert_eq!(result.errors_count(), 1);
}
struct MinifiedBatchReporter(Arc<AtomicUsize>);
impl DiagnosticReporter for MinifiedBatchReporter {
fn finish(&mut self, _result: &DiagnosticResult) -> Option<String> {
None
}
fn render_error(&mut self, error: Error) -> Option<String> {
let message = error.to_string();
if message == "File is too long to fit on the screen" {
Some(format!("{message}: {}\n", error.help().unwrap_or_default()))
} else {
self.0.fetch_add(1, Ordering::Relaxed);
Some(if message == "second" {
format!("{}\n", "x".repeat(1200))
} else {
format!("{message}\n")
})
}
}
}
#[test]
fn stops_an_oversized_batch_at_the_minified_file_diagnostic() {
let rendered = Arc::new(AtomicUsize::new(0));
let (mut service, sender) =
DiagnosticService::new(Box::new(MinifiedBatchReporter(Arc::clone(&rendered))));
sender
.send(vec![
OxcDiagnostic::warn("first")
.with_source_code(NamedSource::new("normal.js", "source")),
OxcDiagnostic::warn("second")
.with_source_code(NamedSource::new("minified.js", "source")),
OxcDiagnostic::warn("third").into(),
])
.unwrap();
drop(sender);
let mut output = Vec::new();
let result = service.run(&mut output);
assert_eq!(
String::from_utf8(output).unwrap(),
"first\nFile is too long to fit on the screen: minified.js seems like a minified file\n"
);
assert_eq!(rendered.load(Ordering::Relaxed), 2);
assert_eq!(result.warnings_count(), 3);
}
}
@@ -733,6 +733,11 @@ impl<'a> SpanScanner<'a> {
}
}
/// Whether this scanner indexes `input`.
pub fn is_for(&self, input: &[u8]) -> bool {
std::ptr::eq(self.index.input, input)
}
/// Read a span while scanning only source bytes no earlier query scanned.
pub fn read_span(&mut self, span: Span) -> Option<SpanContents<'a>> {
let request = SpanRequest::new(span);