mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
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:
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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user